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.