From 66e3ee28b6071c318e78ed2b51eba3c35a3cb139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 1 Apr 2015 10:55:13 +0200 Subject: [PATCH] Started work on plugin lifecycle management Plugins may be loaded, unloaded, activated and deactivated. Errors while trying to load a plugin or initializing an implementation will only result in it staying deactive but registered in the system, allowing it to be further processed e.g. by a plugin manager --- src/octoprint/plugin/core.py | 288 ++++++++++++++++++++++++------- src/octoprint/server/__init__.py | 21 ++- 2 files changed, 243 insertions(+), 66 deletions(-) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 38f0551d..05858b3a 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -82,6 +82,18 @@ class PluginInfo(object): attr_init = '__plugin_init__' """ Module attribute which to call when loading the plugin. """ + attr_load = '__plugin_load__' + + attr_unload = '__plugin_unload__' + + attr_enable = '__plugin_enable__' + + attr_disable = '__plugin_disable__' + + attr_activate = '__plugin_activate__' + + attr_deactivate = '__plugin_deactivate__' + def __init__(self, key, location, instance, name=None, version=None, description=None, author=None, url=None, license=None): self.key = key self.location = location @@ -266,15 +278,40 @@ class PluginInfo(object): return self._get_instance_attribute(self.__class__.attr_check, default=lambda: True) @property - def init(self): + def load(self): """ - Method for initializing the plugin module. Will be taken from the init attribute of the plugin module as defined - in :attr:`attr_init` if available, otherwise a lambda always returning True is returned. + Method for loading the plugin module. Will be taken from the load attribute of the plugin module as defined + in :attr:`attr_load` if available, otherwise a no-operation lambda will be returned. Returns: callable: Init method for the plugin module. """ - return self._get_instance_attribute(self.__class__.attr_init, default=lambda: True) + load = self._get_instance_attribute(self.__class__.attr_load, default=None) + if load is None: + load = self._get_instance_attribute(self.__class__.attr_init, default=lambda:True) + return load + + @property + def unload(self): + return self._get_instance_attribute(self.__class__.attr_unload, default=lambda: True) + + @property + def activate(self): + return self._get_instance_attribute(self.__class__.attr_activate, default=lambda: True) + + @property + def deactivate(self): + return self._get_instance_attribute(self.__class__.attr_deactivate, default=lambda: True) + + @property + def enable(self): + self.enabled = True + return self._get_instance_attribute(self.__class__.attr_enable, default=lambda: True) + + @property + def disable(self): + self.enabled = False + return self._get_instance_attribute(self.__class__.attr_disable, default=lambda: True) def _get_instance_attribute(self, attr, default=None, defaults=None): if not hasattr(self.instance, attr): @@ -313,8 +350,19 @@ class PluginManager(object): self.plugin_implementations = defaultdict(set) self.plugin_implementations_by_type = defaultdict(list) + self.implementation_injects = dict() + self.implementation_inject_factories = [] + self.disabled_plugins = dict() + self.on_plugin_loaded = lambda *args, **kwargs: None + self.on_plugin_unloaded = lambda *args, **kwargs: None + self.on_plugin_activated = lambda *args, **kwargs: None + self.on_plugin_deactivated = lambda *args, **kwargs: None + self.on_plugin_enabled = lambda *args, **kwargs: None + self.on_plugin_disabled = lambda *args, **kwargs: None + self.on_plugin_implementations_initialized = lambda *args, **kwargs: None + self.registered_clients = [] self.reload_plugins() @@ -355,7 +403,7 @@ class PluginManager(object): # plugin is already defined, ignore it continue - plugin = self._load_plugin_from_module(key, folder=folder) + plugin = self._import_plugin_from_module(key, folder=folder) if plugin: plugin.origin = ("folder", folder) if readonly: @@ -400,7 +448,7 @@ class PluginManager(object): license=module_pkginfo.license )) - plugin = self._load_plugin_from_module(key, **kwargs) + plugin = self._import_plugin_from_module(key, **kwargs) if plugin: plugin.origin = ("entry_point", group, module_name) @@ -412,7 +460,7 @@ class PluginManager(object): return plugins, disabled_plugins - def _load_plugin_from_module(self, key, folder=None, module_name=None, name=None, version=None, summary=None, author=None, url=None, license=None): + def _import_plugin_from_module(self, key, folder=None, module_name=None, name=None, version=None, summary=None, author=None, url=None, license=None): # TODO error handling try: if folder: @@ -425,7 +473,7 @@ class PluginManager(object): self.logger.warn("Could not locate plugin {key}") return None - plugin = self._load_plugin(key, *module, name=name, version=version, summary=summary, author=author, url=url, license=license) + plugin = self._import_plugin(key, *module, name=name, version=version, summary=summary, author=author, url=url, license=license) if plugin: if plugin.check(): return plugin @@ -434,7 +482,7 @@ class PluginManager(object): return None - def _load_plugin(self, key, f, filename, description, name=None, version=None, summary=None, author=None, url=None, license=None): + def _import_plugin(self, key, f, filename, description, name=None, version=None, summary=None, author=None, url=None, license=None): try: instance = imp.load_module(key, f, filename, description) return PluginInfo(key, filename, instance, name=name, version=version, description=summary, author=author, url=url, license=license) @@ -447,27 +495,12 @@ class PluginManager(object): def reload_plugins(self): self.logger.info("Loading plugins from {folders} and installed plugin packages...".format(folders=", ".join(map(lambda x: x[0] if isinstance(x, tuple) else str(x), self.plugin_folders)))) - self.plugins, self.disabled_plugins = self._find_plugins() + plugins, disabled_plugins = self._find_plugins() - for name, plugin in self.plugins.items(): - try: - # initialize the plugin - plugin.init() + self.disabled_plugins = disabled_plugins - # evaluate registered hooks - for hook, callback in plugin.hooks.items(): - self.plugin_hooks[hook].append((name, callback)) - - # evaluate registered implementations - for plugin_type in self.plugin_types: - implementations = plugin.get_implementations(plugin_type) - self.plugin_implementations_by_type[plugin_type] += ( (name, implementation) for implementation in implementations ) - - plugin_implementations = plugin.get_implementations() - if len(plugin_implementations): - self.plugin_implementations[name].update(plugin_implementations) - except: - self.logger.exception("There was an error loading plugin %s" % name) + for name, plugin in plugins.items(): + self.load_plugin(name, plugin) if len(self.plugins) <= 0: self.logger.info("No plugins found") @@ -478,49 +511,184 @@ class PluginManager(object): hooks=sum(map(lambda x: len(x), self.plugin_hooks.values())) )) + @property + def all_plugins(self): + plugins = dict(self.plugins) + plugins.update(self.disabled_plugins) + return plugins + + def load_plugin(self, name, plugin): + try: + plugin.load() + self._activate_plugin(name, plugin) + except: + self.logger.exception("There was an error loading plugin %s" % name) + self.disabled_plugins[name] = plugin + else: + self.plugins[name] = plugin + self.on_plugin_loaded(name, plugin) + self.logger.debug("Loaded plugin {name}: {plugin}".format(**locals())) + + def unload_plugin(self, name): + if not name in self.plugins: + if name in self.disabled_plugins: + plugin = self.disabled_plugins[name] + else: + self.logger.warn("Trying to unload unknown plugin {name}".format(**locals())) + return + else: + plugin = self.plugins[name] + + try: + self._deactivate_plugin(name, plugin) + plugin.unload() + if name in self.plugins: + del self.plugins[name] + if name in self.disabled_plugins: + del self.disabled_plugins[name] + except: + self.logger.exception("There was an error unloading plugin {name}".format(**locals())) + else: + self.on_plugin_unloaded(name, plugin) + self.logger.debug("Unloaded plugin {name}: {plugin}".format(**locals())) + + def enable_plugin(self, name): + if not name in self.disabled_plugins: + self.logger.warn("Tried to enable plugin {name}, however it is not disabled".format(**locals())) + return + + plugin = self.disabled_plugins[name] + + try: + del self.disabled_plugins[name] + plugin.enable() + self._activate_plugin(name, plugin) + self.initialize_implementation_of_plugin(name, plugin) + self.plugins[name] = plugin + except: + self.logger.exception("There was an error while enabling plugin {name}".format(**locals())) + self.disabled_plugins[name] = plugin + else: + self.on_plugin_enabled(name, plugin) + self.logger.debug("Enabled plugin {name}: {plugin}".format(**locals())) + + def disable_plugin(self, name): + if not name in self.plugins: + self.logger.warn("Tried to disable plugin {name}, however it is not enabled".format(**locals())) + return + + plugin = self.plugins[name] + + try: + del self.plugins[name] + plugin.disable() + self._deactivate_plugin(name, plugin) + self.disabled_plugins[name] = plugin + except: + self.logger.exception("There was an error while disabling plugin {name}".format(**locals())) + self.plugins[name] = plugin + else: + self.on_plugin_disabled(name, plugin) + self.logger.debug("Disabled plugin {name}: {plugin}".format(**locals())) + + def _activate_plugin(self, name, plugin): + plugin.activate() + + # evaluate registered hooks + for hook, callback in plugin.hooks.items(): + self.plugin_hooks[hook].append((name, callback)) + + # evaluate registered implementations + for plugin_type in self.plugin_types: + implementations = plugin.get_implementations(plugin_type) + self.plugin_implementations_by_type[plugin_type] += ( (name, implementation) for implementation in implementations ) + + plugin_implementations = plugin.get_implementations() + if len(plugin_implementations): + self.plugin_implementations[name].update(plugin_implementations) + + self.on_plugin_activated(name, plugin) + + def _deactivate_plugin(self, name, plugin): + plugin.deactivate() + for hook, callback in plugin.hooks.items(): + self.plugin_hooks[hook].remove((name, callback)) + + for plugin_type in self.plugin_types: + implementations = plugin.get_implementations(plugin_type) + map(lambda x: self.plugin_implementations_by_type[plugin_type].remove(x), + ( (name, implementation) for implementation in implementations )) + + del self.plugin_implementations[name] + self.on_plugin_deactivated(name, plugin) + def initialize_implementations(self, additional_injects=None, additional_inject_factories=None): + for name, implementations in self.plugin_implementations.items(): + plugin = self.plugins[name] + self.initialize_implementations_of_plugin(name, plugin, + additional_injects=additional_injects, + additional_inject_factories=additional_inject_factories) + + self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations))) + + def initialize_implementations_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None): + if not plugin.implementations: + return + + for implementation in plugin.implementations: + self.initialize_implementation(name, plugin, implementation, + additional_injects=additional_injects, + additional_inject_factories=additional_inject_factories) + + def initialize_implementation(self, name, plugin, implementation, additional_injects=None, additional_inject_factories=None): if additional_injects is None: additional_injects = dict() if additional_inject_factories is None: additional_inject_factories = [] - for name, implementations in self.plugin_implementations.items(): - plugin = self.plugins[name] - for implementation in implementations: + injects = self.implementation_injects + injects.update(additional_injects) + + inject_factories = self.implementation_inject_factories + inject_factories += additional_inject_factories + + try: + kwargs = dict(injects) + + kwargs.update(dict( + identifier=name, + plugin_name=plugin.name, + plugin_version=plugin.version, + basefolder=os.path.realpath(plugin.location), + logger=logging.getLogger(self.logging_prefix + name), + )) + + # inject the additional_injects + for arg, value in kwargs.items(): + setattr(implementation, "_" + arg, value) + + # inject any injects produced in the additional_inject_factories + for factory in inject_factories: try: - kwargs = dict(additional_injects) - - kwargs.update(dict( - identifier=name, - plugin_name=plugin.name, - plugin_version=plugin.version, - basefolder=os.path.realpath(plugin.location), - logger=logging.getLogger(self.logging_prefix + name), - )) - - # inject the additional_injects - for arg, value in kwargs.items(): - setattr(implementation, "_" + arg, value) - - # inject any injects produced in the additional_inject_factories - for factory in additional_inject_factories: - try: - return_value = factory(name, implementation) - except: - self.logger.exception("Exception while executing injection factory %r" % factory) - else: - if return_value is not None: - if isinstance(return_value, dict): - for arg, value in return_value.items(): - setattr(implementation, "_" + arg, value) - - implementation.initialize() + return_value = factory(name, implementation) except: - self.logger.exception("Exception while initializing plugin") - # TODO disable plugin! + self.logger.exception("Exception while executing injection factory %r" % factory) + else: + if return_value is not None: + if isinstance(return_value, dict): + for arg, value in return_value.items(): + setattr(implementation, "_" + arg, value) + + implementation.initialize() + except: + self.logger.exception("Exception while initializing plugin {name}, disabling it".format(**locals())) + self._deactivate_plugin(name, plugin) + else: + self.on_plugin_implementations_initialized(name, plugin) self.logger.debug("Initialized {count} plugin mixin implementation(s)".format(count=len(self.plugin_implementations))) + def log_all_plugins(self, show_bundled=True, bundled_str=(" (bundled)", ""), show_location=True, location_str=" = {location}", show_enabled=True, enabled_str=(" ", "!")): all_plugins = self.plugins.values() + self.disabled_plugins.values() diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index b04be2ff..3a3da891 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -586,12 +586,21 @@ class Server(): set_preprocessors=set_preprocessors) return dict(settings=plugin_settings) - pluginManager.initialize_implementations( - additional_inject_factories=[ - octoprint_plugin_inject_factory, - settings_plugin_inject_factory - ] - ) + pluginManager.implementation_inject_factories=[octoprint_plugin_inject_factory, settings_plugin_inject_factory] + pluginManager.initialize_implementations() + + def on_plugin_event_factory(text): + def on_plugin_event(name, plugin): + logger.info(text.format(**locals())) + return on_plugin_event + + pluginManager.on_plugin_loaded = on_plugin_event_factory("Loaded plugin {name}: {plugin}") + pluginManager.on_plugin_unloaded = on_plugin_event_factory("Unloaded plugin {name}: {plugin}") + pluginManager.on_plugin_activated = on_plugin_event_factory("Activated plugin {name}: {plugin}") + pluginManager.on_plugin_deactivated = on_plugin_event_factory("Deactivated plugin {name}: {plugin}") + pluginManager.on_plugin_enabled = on_plugin_event_factory("Enabled plugin {name}: {plugin}") + pluginManager.on_plugin_disabled = on_plugin_event_factory("Disabled plugin {name}: {plugin}") + pluginManager.log_all_plugins() slicingManager.initialize()