From 97aecdf4cff1bdad2ec9e58bb5ec95578eea452d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 2 Apr 2015 23:02:42 +0200 Subject: [PATCH] First throw at working plugin lifecycle management Plugins may be enabled and disabled during runtime. If they are of types which allow hot loading, this will be done. Otherwise they will be marked as pending and updated after a restart. Same for installation and uninstallation. --- src/octoprint/plugin/__init__.py | 6 +- src/octoprint/plugin/core.py | 281 ++++++++++++++++++++---------- src/octoprint/plugin/types.py | 10 +- src/octoprint/server/__init__.py | 230 +++++++++++++++++------- src/octoprint/settings.py | 4 +- src/octoprint/slicing/__init__.py | 8 +- 6 files changed, 371 insertions(+), 168 deletions(-) diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 5b2bdea0..5813aac3 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -88,11 +88,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en if plugin_entry_points is None: plugin_entry_points = "octoprint.plugin" if plugin_disabled_list is None: - all_plugin_settings = settings().get(["plugins"]) - plugin_disabled_list = [] - for key in all_plugin_settings: - if "enabled" in all_plugin_settings[key] and not all_plugin_settings[key]: - plugin_disabled_list.append(key) + plugin_disabled_list = settings().get(["plugins", "_disabled"]) _instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points, logging_prefix="octoprint.plugins.", plugin_disabled_list=plugin_disabled_list) else: diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index ed47b9a0..0d8880c9 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import os import imp -from collections import defaultdict +from collections import defaultdict, namedtuple import logging @@ -113,6 +113,7 @@ class PluginInfo(object): self.origin = None self.enabled = True self.bundled = False + self.loaded = False self._name = name self._version = version @@ -385,26 +386,34 @@ class PluginManager(object): 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() + self.reload_plugins(startup=True, initialize_implementations=False) - def _find_plugins(self): - plugins = dict() - disabled_plugins = dict() + @property + def all_plugins(self): + plugins = dict(self.plugins) + plugins.update(self.disabled_plugins) + return plugins + + def find_plugins(self, existing=None): + if existing is None: + existing = self.all_plugins + + result = dict() if self.plugin_folders: - self._add_plugins_from_folders(self.plugin_folders, plugins, disabled_plugins) + result.update(self._find_plugins_from_folders(self.plugin_folders, existing)) if self.plugin_entry_points: - self._add_plugins_from_entry_points(self.plugin_entry_points, plugins, disabled_plugins) - return plugins, disabled_plugins + result.update(self._find_plugins_from_entry_points(self.plugin_entry_points, existing)) + return result + + def _find_plugins_from_folders(self, folders, existing): + result = dict() - def _add_plugins_from_folders(self, folders, plugins, disabled_plugins): for folder in folders: readonly = False if isinstance(folder, (list, tuple)): @@ -427,7 +436,7 @@ class PluginManager(object): else: continue - if key in plugins: + if key in existing or key in result: # plugin is already defined, ignore it continue @@ -437,28 +446,31 @@ class PluginManager(object): if readonly: plugin.bundled = True - if self._is_plugin_disabled(key): - plugin.enabled = False - disabled_plugins[key] = plugin - else: - plugins[key] = plugin + plugin.enabled = False - return plugins, disabled_plugins + result[key] = plugin + + return result + + def _find_plugins_from_entry_points(self, groups, existing): + result = dict() - def _add_plugins_from_entry_points(self, groups, plugins, disabled_plugins): import pkg_resources import pkginfo + # let's make sure we have a current working set + working_set = pkg_resources.WorkingSet() + if not isinstance(groups, (list, tuple)): groups = [groups] for group in groups: - for entry_point in pkg_resources.iter_entry_points(group=group, name=None): + for entry_point in working_set.iter_entry_points(group=group, name=None): key = entry_point.name module_name = entry_point.module_name version = entry_point.dist.version - if key in plugins: + if key in existing or key in result: # plugin is already defined, ignore it continue @@ -479,14 +491,10 @@ class PluginManager(object): plugin = self._import_plugin_from_module(key, **kwargs) if plugin: plugin.origin = ("entry_point", group, module_name) + plugin.enabled = False + result[key] = plugin - if self._is_plugin_disabled(key): - plugin.enabled = False - disabled_plugins[key] = plugin - else: - plugins[key] = plugin - - return plugins, disabled_plugins + return result 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 @@ -502,12 +510,14 @@ class PluginManager(object): return None 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 - else: - self.logger.warn("Plugin \"{plugin}\" did not pass check".format(plugin=str(plugin))) - return None + if plugin is None: + return None + + if plugin.check(): + return plugin + else: + self.logger.warn("Plugin \"{plugin}\" did not pass check".format(plugin=str(plugin))) + return None def _import_plugin(self, key, f, filename, description, name=None, version=None, summary=None, author=None, url=None, license=None): @@ -515,22 +525,27 @@ class PluginManager(object): 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) except: - self.logger.exception("Error loading plugin {key}, disabling it".format(key=key)) + self.logger.exception("Error loading plugin {key}".format(key=key)) return None def _is_plugin_disabled(self, key): return key in self.plugin_disabled_list or key.endswith('disabled') - def reload_plugins(self): + def reload_plugins(self, startup=False, initialize_implementations=True): 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)) )) - plugins, disabled_plugins = self._find_plugins() - self.disabled_plugins = disabled_plugins + plugins = self.find_plugins() + self.disabled_plugins.update(plugins) for name, plugin in plugins.items(): - self.load_plugin(name, plugin) + try: + self.load_plugin(name, plugin, startup=startup, initialize_implementation=initialize_implementations) + if not self._is_plugin_disabled(name): + self.enable_plugin(name, plugin=plugin, initialize_implementation=initialize_implementations, startup=startup) + except (PluginLifecycleException, PluginNeedsRestart): + pass if len(self.plugins) <= 0: self.logger.info("No plugins found") @@ -541,111 +556,158 @@ 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=None, startup=False, initialize_implementation=True): + if not name in self.all_plugins: + self.logger.warn("Trying to load an unknown plugin {name}".format(**locals())) + return + + if plugin is None: + plugin = self.all_plugins[name] - def load_plugin(self, name, plugin): try: plugin.load() plugin.validate() - self._activate_plugin(name, plugin) + self.on_plugin_loaded(name, plugin) + + plugin.loaded = True + + self.logger.debug("Loaded plugin {name}: {plugin}".format(**locals())) + except PluginLifecycleException as e: + raise e 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] + if not name in self.all_plugins: + self.logger.warn("Trying to unload unknown plugin {name}".format(**locals())) + return + + plugin = self.all_plugins[name] try: - self._deactivate_plugin(name, plugin) + if plugin.enabled: + self.disable_plugin(name, plugin=plugin) + plugin.unload() + self.on_plugin_unloaded(name, plugin) + if name in self.plugins: del self.plugins[name] + if name in self.disabled_plugins: del self.disabled_plugins[name] + + plugin.loaded = False + + self.logger.debug("Unloaded plugin {name}: {plugin}".format(**locals())) + except PluginLifecycleException as e: + raise e 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): + # make sure the plugin is NOT in the list of enabled plugins but in the list of disabled plugins + if name in self.plugins: + del self.plugins[name] + if not name in self.disabled_plugins: + self.disabled_plugins[name] = plugin + + def enable_plugin(self, name, plugin=None, initialize_implementation=True, startup=False): 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] + if plugin is None: + plugin = self.disabled_plugins[name] + + if not startup and plugin.implementation and isinstance(plugin.implementation, RestartNeedingPlugin): + raise PluginNeedsRestart(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 PluginLifecycleException as e: + raise e except: self.logger.exception("There was an error while enabling plugin {name}".format(**locals())) - self.disabled_plugins[name] = plugin + return False else: + if name in self.disabled_plugins: + del self.disabled_plugins[name] + self.plugins[name] = plugin + plugin.enabled = True + + if plugin.implementation: + if initialize_implementation: + if not self.initialize_implementation_of_plugin(name, plugin): + return False + plugin.implementation.on_plugin_enabled() self.on_plugin_enabled(name, plugin) + self.logger.debug("Enabled plugin {name}: {plugin}".format(**locals())) - def disable_plugin(self, name): + return True + + def disable_plugin(self, name, plugin=None): 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] + if plugin is None: + plugin = self.plugins[name] + + if plugin.implementation and isinstance(plugin.implementation, RestartNeedingPlugin): + raise PluginNeedsRestart(name) try: - del self.plugins[name] plugin.disable() self._deactivate_plugin(name, plugin) - self.disabled_plugins[name] = plugin + except PluginLifecycleException as e: + raise e except: self.logger.exception("There was an error while disabling plugin {name}".format(**locals())) - self.plugins[name] = plugin + return False else: + if name in self.plugins: + del self.plugins[name] + self.disabled_plugins[name] = plugin + plugin.enabled = False + + if plugin.implementation: + plugin.implementation.on_plugin_disabled() self.on_plugin_disabled(name, plugin) + self.logger.debug("Disabled plugin {name}: {plugin}".format(**locals())) - def _activate_plugin(self, name, plugin): - plugin.activate() + return True + def _activate_plugin(self, name, plugin): # evaluate registered hooks for hook, callback in plugin.hooks.items(): self.plugin_hooks[hook].append((name, callback)) # evaluate registered implementation if plugin.implementation: + if isinstance(plugin.implementation, RestartNeedingPlugin): + plugin.hotchangeable = False + for plugin_type in self.plugin_types: if isinstance(plugin.implementation, plugin_type): self.plugin_implementations_by_type[plugin_type].append((name, plugin.implementation)) self.plugin_implementations[name] = plugin.implementation - 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)) + try: + self.plugin_hooks[hook].remove((name, callback)) + except ValueError: + # that's ok, the plugin was just not registered for the hook + pass if plugin.implementation is not None: - del self.plugin_implementations[name] + if name in self.plugin_implementations: + del self.plugin_implementations[name] + for plugin_type in self.plugin_types: try: self.plugin_implementations_by_type[plugin_type].remove((name, plugin.implementation)) @@ -653,8 +715,6 @@ class PluginManager(object): # that's ok, the plugin was just not registered for the type pass - self.on_plugin_deactivated(name, plugin) - def initialize_implementations(self, additional_injects=None, additional_inject_factories=None): for name, plugin in self.plugins.items(): self.initialize_implementation_of_plugin(name, plugin, @@ -667,7 +727,7 @@ class PluginManager(object): if plugin.implementation is None: return - self.initialize_implementation(name, plugin, plugin.implementation, + return self.initialize_implementation(name, plugin, plugin.implementation, additional_injects=additional_injects, additional_inject_factories=additional_inject_factories) @@ -710,18 +770,22 @@ class PluginManager(object): for arg, value in return_value.items(): setattr(implementation, "_" + arg, value) - result = implementation.initialize() - if result is not None and not result: - self.logger.warn("Initialization of {name} returned False, disabling it".format(**locals())) - self._deactivate_plugin(name, plugin) - return - except: - self.logger.exception("Exception while initializing plugin {name}, disabling it".format(**locals())) + implementation.initialize() + + except Exception as e: self._deactivate_plugin(name, plugin) + plugin.enabled = False + + if isinstance(e, PluginLifecycleException): + raise e + else: + self.logger.exception("Exception while initializing plugin {name}, disabling it".format(**locals())) + return False else: self.on_plugin_implementations_initialized(name, plugin) self.logger.debug("Initialized plugin mixin implementation for plugin {name}".format(**locals())) + return True def log_all_plugins(self, show_bundled=True, bundled_str=(" (bundled)", ""), show_location=True, location_str=" = {location}", show_enabled=True, enabled_str=(" ", "!")): @@ -927,3 +991,38 @@ class Plugin(object): Called by the plugin core after performing all injections. Override this to initialize your implementation. """ pass + + def on_plugin_enabled(self): + pass + + def on_plugin_disabled(self): + pass + +class RestartNeedingPlugin(Plugin): + pass + +class PluginNeedsRestart(BaseException): + def __init__(self, name): + super(BaseException, self).__init__() + self.name = name + self.message = "Plugin {name} cannot be enabled or disabled after system startup".format(**locals()) + +class PluginLifecycleException(BaseException): + def __init__(self, name, reason, message): + super(BaseException, self).__init__() + self.name = name + self.reason = reason + + self.message = message.format(**locals()) + +class PluginCantInitialize(PluginLifecycleException): + def __init__(self, name, reason): + super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be initialized: {message}") + +class PluginCantEnable(PluginLifecycleException): + def __init__(self, name, reason): + super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be enabled: {message}") + +class PluginCantDisable(PluginLifecycleException): + def __init__(self, name, reason): + super(PluginLifecycleException, self).__init__(name, reason, "Plugin {name} cannot be disabled: {message}") diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 2f38879d..0f88e45e 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -18,7 +18,7 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" -from .core import Plugin +from .core import (Plugin, RestartNeedingPlugin) class OctoPrintPlugin(Plugin): @@ -68,6 +68,8 @@ class OctoPrintPlugin(Plugin): pass +class ReloadNeedingPlugin(Plugin): + pass class StartupPlugin(OctoPrintPlugin): """ @@ -111,7 +113,7 @@ class ShutdownPlugin(OctoPrintPlugin): pass -class AssetPlugin(OctoPrintPlugin): +class AssetPlugin(OctoPrintPlugin, ReloadNeedingPlugin): """ The ``AssetPlugin`` mixin allows plugins to define additional static assets such as Javascript or CSS files to be automatically embedded into the pages delivered by the server to be used within the client sided part of @@ -164,7 +166,7 @@ class AssetPlugin(OctoPrintPlugin): return dict() -class TemplatePlugin(OctoPrintPlugin): +class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): """ Using the ``TemplatePlugin`` mixin plugins may inject their own components into the OctoPrint web interface. @@ -549,7 +551,7 @@ class SimpleApiPlugin(OctoPrintPlugin): return None -class BlueprintPlugin(OctoPrintPlugin): +class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin): """ The ``BlueprintPlugin`` mixin allows plugins to define their own full fledged endpoints for whatever purpose, be it a more sophisticated API than what is possible via the :class:`SimpleApiPlugin` or a custom web frontend. diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 82469099..fc095826 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -539,9 +539,15 @@ class Server(): self._logConf = logConf self._server = None + self._logger = None + + self._lifecycle_callbacks = defaultdict(list) + + self._template_searchpaths = [] + def run(self): if not self._allowRoot: - self._checkForRoot() + self._check_for_root() global printer global printerProfileManager @@ -566,12 +572,12 @@ class Server(): settings(init=True, basedir=self._basedir, configfile=self._configfile) # then initialize logging - self._initLogging(self._debug, self._logConf) - logger = logging.getLogger(__name__) + self._setup_logging(self._debug, self._logConf) + self._logger = logging.getLogger(__name__) def exception_logger(exc_type, exc_value, exc_tb): - logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb)) + self._logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb)) sys.excepthook = exception_logger - logger.info("Starting OctoPrint %s" % DISPLAY_VERSION) + self._logger.info("Starting OctoPrint %s" % DISPLAY_VERSION) # then initialize the plugin manager pluginManager = octoprint.plugin.plugin_manager(init=True) @@ -614,37 +620,25 @@ class Server(): 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}") - + lifecycleManager = LifecycleManager(self, pluginManager) pluginManager.log_all_plugins() + + # initialize slicing manager and register it for changes in the registered plugins slicingManager.initialize() + lifecycleManager.add_callback(["enabled", "disabled"], lambda name, plugin: slicingManager.reload_slicers()) - # configure additional template folders for jinja2 - template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) - additional_template_folders = [] - for plugin in template_plugins: - folder = plugin.get_template_folder() - if folder is not None: - additional_template_folders.append(plugin.get_template_folder()) - - import jinja2 - jinja_loader = jinja2.ChoiceLoader([ - app.jinja_loader, - jinja2.FileSystemLoader(additional_template_folders) - ]) - app.jinja_loader = jinja_loader - del jinja2 - app.jinja_env.add_extension("jinja2.ext.do") + # setup jinja2 + self._setup_jinja2() + def template_enabled(name, plugin): + if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.TemplatePlugin): + return + self._register_additional_template_plugin(plugin.implementation) + def template_disabled(name, plugin): + if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.TemplatePlugin): + return + self._unregister_additional_template_plugin(plugin.implementation) + lifecycleManager.add_callback("enabled", template_enabled) + lifecycleManager.add_callback("disabled", template_disabled) # configure timelapse octoprint.timelapse.configureTimelapse() @@ -660,7 +654,7 @@ class Server(): clazz = octoprint.util.get_class(userManagerName) userManager = clazz() except AttributeError, e: - logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) + self._logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName) app.wsgi_app = util.ReverseProxied( app.wsgi_app, @@ -696,36 +690,20 @@ class Server(): app.debug = self._debug - from octoprint.server.api import api - from octoprint.server.apps import apps - # register API blueprint - app.register_blueprint(api, url_prefix="/api") - app.register_blueprint(apps, url_prefix="/apps") - - # also register any blueprints defined in BlueprintPlugins - blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin) - for plugin in blueprint_plugins: - name = plugin._identifier - blueprint = plugin.get_blueprint() - if blueprint is None: - continue - - if plugin.is_blueprint_protected(): - from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler - blueprint.before_request(apiKeyRequestHandler) - blueprint.after_request(corsResponseHandler) - - url_prefix = "/plugin/{name}".format(name=name) - app.register_blueprint(blueprint, url_prefix=url_prefix) - logger.debug("Registered API of plugin {name} under URL prefix {url_prefix}".format(name=name, url_prefix=url_prefix)) + self._setup_blueprints() + def blueprint_enabled(name, plugin): + if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.BlueprintPlugin): + return + self._register_blueprint_plugin(plugin.implementation) + lifecycleManager.add_callback(["enabled"], blueprint_enabled) ## Tornado initialization starts here ioloop = IOLoop() ioloop.install() - self._router = SockJSRouter(self._createSocketConnection, "/sockjs") + self._router = SockJSRouter(self._create_socket_connection, "/sockjs") upload_suffixes = dict(name=settings().get(["server", "uploads", "nameSuffix"]), path=settings().get(["server", "uploads", "pathSuffix"])) self._tornado_app = Application(self._router.urls + [ @@ -759,9 +737,16 @@ class Server(): "on_startup", args=(self._host, self._port)) + def call_on_startup(name, plugin): + implementation = plugin.get_implementation(octoprint.plugin.StartupPlugin) + if implementation is None: + return + implementation.on_startup(self._host, self._port) + lifecycleManager.add_callback("enabled", call_on_startup) + # prepare our after startup function def on_after_startup(): - logger.info("Listening on http://%s:%d" % (self._host, self._port)) + self._logger.info("Listening on http://%s:%d" % (self._host, self._port)) # now this is somewhat ugly, but the issue is the following: startup plugins might want to do things for # which they need the server to be already alive (e.g. for being able to resolve urls, such as favicons @@ -771,13 +756,21 @@ class Server(): def work(): octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin, "on_after_startup") + + def call_on_after_startup(name, plugin): + implementation = plugin.get_implementation(octoprint.plugin.StartupPlugin) + if implementation is None: + return + implementation.on_after_startup() + lifecycleManager.add_callback("enabled", call_on_after_startup) + import threading threading.Thread(target=work).start() ioloop.add_callback(on_after_startup) # prepare our shutdown function def on_shutdown(): - logger.info("Goodbye!") + self._logger.info("Goodbye!") observer.stop() observer.join() octoprint.plugin.call_plugin(octoprint.plugin.ShutdownPlugin, @@ -789,18 +782,18 @@ class Server(): except KeyboardInterrupt: pass except: - logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!") - logger.exception("Stacktrace follows:") + self._logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!") + self._logger.exception("Stacktrace follows:") - def _createSocketConnection(self, session): + def _create_socket_connection(self, session): global printer, fileManager, analysisQueue, userManager, eventManager return util.sockjs.PrinterStateConnection(printer, fileManager, analysisQueue, userManager, eventManager, pluginManager, session) - def _checkForRoot(self): + def _check_for_root(self): if "geteuid" in dir(os) and os.geteuid() == 0: exit("You should not run OctoPrint as root!") - def _initLogging(self, debug, logConf=None): + def _setup_logging(self, debug, logConf=None): defaultConfig = { "version": 1, "formatters": { @@ -871,6 +864,115 @@ class Server(): logging.getLogger("SERIAL").setLevel(logging.DEBUG) logging.getLogger("SERIAL").debug("Enabling serial logging") + def _setup_jinja2(self): + app.jinja_env.add_extension("jinja2.ext.do") + + # configure additional template folders for jinja2 + import jinja2 + filesystem_loader = jinja2.FileSystemLoader([]) + filesystem_loader.searchpath = self._template_searchpaths + + jinja_loader = jinja2.ChoiceLoader([ + app.jinja_loader, + filesystem_loader + ]) + app.jinja_loader = jinja_loader + del jinja2 + + self._register_template_plugins() + + def _register_template_plugins(self): + template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) + for plugin in template_plugins: + self._register_additional_template_plugin(plugin) + + def _register_additional_template_plugin(self, plugin): + folder = plugin.get_template_folder() + if folder is not None and not folder in self._template_searchpaths: + self._template_searchpaths.append(folder) + + def _unregister_additional_template_plugin(self, plugin): + folder = plugin.get_template_folder() + if folder is not None and folder in self._template_searchpaths: + self._template_searchpaths.remove(folder) + + def _setup_blueprints(self): + from octoprint.server.api import api + from octoprint.server.apps import apps + + app.register_blueprint(api, url_prefix="/api") + app.register_blueprint(apps, url_prefix="/apps") + + # also register any blueprints defined in BlueprintPlugins + self._register_blueprint_plugins() + + def _register_blueprint_plugins(self): + blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin) + for plugin in blueprint_plugins: + self._register_blueprint_plugin(plugin) + + def _register_blueprint_plugin(self, plugin): + name = plugin._identifier + blueprint = plugin.get_blueprint() + if blueprint is None: + return + + if plugin.is_blueprint_protected(): + from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler + blueprint.before_request(apiKeyRequestHandler) + blueprint.after_request(corsResponseHandler) + + url_prefix = "/plugin/{name}".format(name=name) + app.register_blueprint(blueprint, url_prefix=url_prefix) + + if self._logger: + self._logger.debug("Registered API of plugin {name} under URL prefix {url_prefix}".format(name=name, url_prefix=url_prefix)) + +class LifecycleManager(object): + def __init__(self, server, plugin_manager): + self._server = server + self._plugin_manager = plugin_manager + + self._plugin_lifecycle_callbacks = defaultdict(list) + self._logger = logging.getLogger(__name__) + + def on_plugin_event_factory(lifecycle_event, text): + def on_plugin_event(name, plugin): + self.on_plugin_event(lifecycle_event, name, plugin) + self._logger.debug(text.format(**locals())) + return on_plugin_event + + self._plugin_manager.on_plugin_loaded = on_plugin_event_factory("loaded", "Loaded plugin {name}: {plugin}") + self._plugin_manager.on_plugin_unloaded = on_plugin_event_factory("unloaded", "Unloaded plugin {name}: {plugin}") + self._plugin_manager.on_plugin_activated = on_plugin_event_factory("activated", "Activated plugin {name}: {plugin}") + self._plugin_manager.on_plugin_deactivated = on_plugin_event_factory("deactivated", "Deactivated plugin {name}: {plugin}") + self._plugin_manager.on_plugin_enabled = on_plugin_event_factory("enabled", "Enabled plugin {name}: {plugin}") + self._plugin_manager.on_plugin_disabled = on_plugin_event_factory("disabled", "Disabled plugin {name}: {plugin}") + + def on_plugin_event(self, event, name, plugin): + for lifecycle_callback in self._plugin_lifecycle_callbacks[event]: + lifecycle_callback(name, plugin) + + def add_callback(self, events, callback): + if isinstance(events, (str, unicode)): + events = [events] + + for event in events: + self._plugin_lifecycle_callbacks[event].append(callback) + + def remove_callback(self, callback, events=None): + if events is None: + for event in self._plugin_lifecycle_callbacks: + if callback in self._plugin_lifecycle_callbacks[event]: + self._plugin_lifecycle_callbacks[event].remove(callback) + else: + if isinstance(events, (str, unicode)): + events = [events] + + for event in events: + if callback in self._plugin_lifecycle_callbacks[event]: + self._plugin_lifecycle_callbacks[event].remove(callback) + if __name__ == "__main__": server = Server() server.run() diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 5d3d5793..7ed93369 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -224,7 +224,9 @@ default_settings = { { "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok (B|T\d*):)" }, { "name": "Suppress M27 requests/responses", "regex": "(Send: M27)|(Recv: SD printing byte)" } ], - "plugins": {}, + "plugins": { + "_disabled": [] + }, "scripts": { "gcode": { "afterPrintCancelled": "; disable motors\nM84\n\n;disable all heaters\n{% snippet 'disable_hotends' %}\nM140 S0\n\n;disable fan\nM106 S0", diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index d5c74e2d..1a804a8b 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -113,16 +113,18 @@ class SlicingManager(object): Initializes the slicing manager by loading and initializing all available :class:`~octoprint.plugin.SlicerPlugin` implementations. """ - self._load_slicers() + self.reload_slicers() - def _load_slicers(self): + def reload_slicers(self): """ Retrieves all registered :class:`~octoprint.plugin.SlicerPlugin` implementations and registers them as available slicers. """ plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin) + slicers = dict() for plugin in plugins: - self._slicers[plugin.get_slicer_properties()["type"]] = plugin + slicers[plugin.get_slicer_properties()["type"]] = plugin + self._slicers = slicers @property def slicing_enabled(self):