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)
This commit is contained in:
Brian S. Stephan 2016-01-17 10:56:39 -06:00
parent 0ba889bf75
commit 6fe6797281
1 changed files with 91 additions and 0 deletions

View File

@ -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.