From 6fe67972816cd19078c691b243c10f0f8743c1e5 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 17 Jan 2016 10:56:39 -0600 Subject: [PATCH] ircbot: implement !reload for plugins in addition to being a convenient unload plugin, load plugin, this also uses importlib.reload() to kick the interpreter to reload changed code inside the plugin's package, if the user requests to reload the entire package. this seems safe so far (famous last words) --- dr_botzo/ircbot/bot.py | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/dr_botzo/ircbot/bot.py b/dr_botzo/ircbot/bot.py index 35225c8..45b7473 100644 --- a/dr_botzo/ircbot/bot.py +++ b/dr_botzo/ircbot/bot.py @@ -3,6 +3,7 @@ import bisect import collections import copy +import importlib import logging import re from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler @@ -356,6 +357,10 @@ class IRCBot(irc.client.SimpleIRCClient): getattr(self, 'handle_unload'), -20) self.connection.reactor.add_global_regex_handler('privmsg', r'^!unload\s+([\S]+)$', getattr(self, 'handle_unload'), -20) + self.connection.reactor.add_global_regex_handler('pubmsg', r'^!reload\s+([\S]+)$', + getattr(self, 'handle_reload'), -20) + self.connection.reactor.add_global_regex_handler('privmsg', r'^!reload\s+([\S]+)$', + getattr(self, 'handle_reload'), -20) # load XML-RPC server self.xmlrpc = SimpleXMLRPCServer((settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT), @@ -662,6 +667,92 @@ class IRCBot(irc.client.SimpleIRCClient): for module_name, module in sys.modules.items(): log.debug(" %s", module_name) + def handle_reload(self, connection, event, match): + """Handle IRC requests to reload a plugin (module or package). + + This allows code for a plugin (and potentially also its sibling modules in the package) + to be reloaded on demand, in the event that changes were made. + + :param connection: connection for this reload request + :type connection: LenientServerConnection + :param event: associated irc event object + :type event: Event + :param match: the matched regex (via add_global_regex_handler) + :type match: Match + """ + + has_perm = ircbotlib.has_permission(event.source, 'ircbot.manage_loaded_plugins') + log.debug("has permission to reload?: %s", str(has_perm)) + if has_perm: + plugin_path = match.group(1) + log.debug("calling _reload_plugin on %s", plugin_path) + self._reload_plugin(connection, event, plugin_path) + + def _reload_plugin(self, connection, event, plugin_path, feedback=True): + """Execute the built-in reload() on a plugin (module) or it and its siblings (package). + + :param connection: connection for this load request + :type connection: LenientServerConnection + :param event: associated irc event object + :type event: Event + :param plugin_path: path (likely a relative one) of the plugin to attempt to reload + :type plugin_path: str + :param feedback: whether or not to send messages to IRC regarding the reload attempt + :type feedback: bool + """ + + log.debug("trying to unload plugin %s", plugin_path) + + dest = ircbotlib.reply_destination_for_event(event) + + # loop over the sys.modules entries, for debugging + log.debug("loaded modules:") + for module_name, module in sys.modules.items(): + log.debug(" %s", module_name) + + # make sure what was provided was a legitimate plugin + # (so that we don't go reloading totally arbitrary code) + search_path = None + for path, plugin in self.plugins: + # first check exact path + if path == plugin_path: + log.debug("requested %s is %s, so we will try to reload %s", + plugin_path, path, plugin_path) + search_path = plugin_path + if plugin_path[-1] == '.' and path.startswith(plugin_path): + log.debug("requested %s is substring of %s, so we will try to reload all %s items", + plugin_path, path, plugin_path) + search_path = plugin_path[:-1] + + # we need to know the plugin names to do some extra logic with reload + plugin_names = [x[0] for x in self.plugins] + + # do reload(s) + if search_path: + plugins_to_reload = [] + for module_name, module in sys.modules.items(): + if module_name.startswith(search_path): + if module_name in plugin_names: + # we need to take the plugin reload path for this, but not + # before all other code is reloaded, so we save this for the end + # if we did this here, the ircplugin might still reference the old + # version of its dependencies + plugins_to_reload.append(module_name) + else: + log.debug("reloading %s", module_name) + importlib.reload(module) + + if feedback: + self.privmsg(dest, "'{0:s}' reloaded.".format(module_name)) + + # the references to event handlers will be stale (reload() doesn't go that deep) + # so we need to tear down and reinitialize plugins + for plugin_to_reload in plugins_to_reload: + log.debug("unloading plugin %s", plugin_to_reload) + self._unload_plugin(event, plugin_to_reload, False) + log.debug("loading plugin %s", plugin_to_reload) + self._load_plugin(connection, event, plugin_to_reload, False) + def handle_unload(self, connection, event, match): """Handle IRC requests to unload a plugin.