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.
This commit is contained in:
Gina Häußge 2015-04-02 23:02:42 +02:00
parent f0b48a6b43
commit 97aecdf4cf
6 changed files with 371 additions and 168 deletions

View file

@ -88,11 +88,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
if plugin_entry_points is None: if plugin_entry_points is None:
plugin_entry_points = "octoprint.plugin" plugin_entry_points = "octoprint.plugin"
if plugin_disabled_list is None: if plugin_disabled_list is None:
all_plugin_settings = settings().get(["plugins"]) plugin_disabled_list = settings().get(["plugins", "_disabled"])
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)
_instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points, logging_prefix="octoprint.plugins.", plugin_disabled_list=plugin_disabled_list) _instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points, logging_prefix="octoprint.plugins.", plugin_disabled_list=plugin_disabled_list)
else: else:

View file

@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import os import os
import imp import imp
from collections import defaultdict from collections import defaultdict, namedtuple
import logging import logging
@ -113,6 +113,7 @@ class PluginInfo(object):
self.origin = None self.origin = None
self.enabled = True self.enabled = True
self.bundled = False self.bundled = False
self.loaded = False
self._name = name self._name = name
self._version = version self._version = version
@ -385,26 +386,34 @@ class PluginManager(object):
self.on_plugin_loaded = lambda *args, **kwargs: None self.on_plugin_loaded = lambda *args, **kwargs: None
self.on_plugin_unloaded = 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_enabled = lambda *args, **kwargs: None
self.on_plugin_disabled = lambda *args, **kwargs: None self.on_plugin_disabled = lambda *args, **kwargs: None
self.on_plugin_implementations_initialized = lambda *args, **kwargs: None self.on_plugin_implementations_initialized = lambda *args, **kwargs: None
self.registered_clients = [] self.registered_clients = []
self.reload_plugins() self.reload_plugins(startup=True, initialize_implementations=False)
def _find_plugins(self): @property
plugins = dict() def all_plugins(self):
disabled_plugins = dict() 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: 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: if self.plugin_entry_points:
self._add_plugins_from_entry_points(self.plugin_entry_points, plugins, disabled_plugins) result.update(self._find_plugins_from_entry_points(self.plugin_entry_points, existing))
return plugins, disabled_plugins 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: for folder in folders:
readonly = False readonly = False
if isinstance(folder, (list, tuple)): if isinstance(folder, (list, tuple)):
@ -427,7 +436,7 @@ class PluginManager(object):
else: else:
continue continue
if key in plugins: if key in existing or key in result:
# plugin is already defined, ignore it # plugin is already defined, ignore it
continue continue
@ -437,28 +446,31 @@ class PluginManager(object):
if readonly: if readonly:
plugin.bundled = True plugin.bundled = True
if self._is_plugin_disabled(key): plugin.enabled = False
plugin.enabled = False
disabled_plugins[key] = plugin
else:
plugins[key] = plugin
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 pkg_resources
import pkginfo import pkginfo
# let's make sure we have a current working set
working_set = pkg_resources.WorkingSet()
if not isinstance(groups, (list, tuple)): if not isinstance(groups, (list, tuple)):
groups = [groups] groups = [groups]
for group in 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 key = entry_point.name
module_name = entry_point.module_name module_name = entry_point.module_name
version = entry_point.dist.version version = entry_point.dist.version
if key in plugins: if key in existing or key in result:
# plugin is already defined, ignore it # plugin is already defined, ignore it
continue continue
@ -479,14 +491,10 @@ class PluginManager(object):
plugin = self._import_plugin_from_module(key, **kwargs) plugin = self._import_plugin_from_module(key, **kwargs)
if plugin: if plugin:
plugin.origin = ("entry_point", group, module_name) plugin.origin = ("entry_point", group, module_name)
plugin.enabled = False
result[key] = plugin
if self._is_plugin_disabled(key): return result
plugin.enabled = False
disabled_plugins[key] = plugin
else:
plugins[key] = plugin
return plugins, disabled_plugins
def _import_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 # TODO error handling
@ -502,12 +510,14 @@ class PluginManager(object):
return None return None
plugin = self._import_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 is None:
if plugin.check(): return None
return plugin
else: if plugin.check():
self.logger.warn("Plugin \"{plugin}\" did not pass check".format(plugin=str(plugin))) return plugin
return None 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): 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) 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) return PluginInfo(key, filename, instance, name=name, version=version, description=summary, author=author, url=url, license=license)
except: 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 return None
def _is_plugin_disabled(self, key): def _is_plugin_disabled(self, key):
return key in self.plugin_disabled_list or key.endswith('disabled') 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( 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)) 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(): 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: if len(self.plugins) <= 0:
self.logger.info("No plugins found") self.logger.info("No plugins found")
@ -541,111 +556,158 @@ class PluginManager(object):
hooks=sum(map(lambda x: len(x), self.plugin_hooks.values())) hooks=sum(map(lambda x: len(x), self.plugin_hooks.values()))
)) ))
@property def load_plugin(self, name, plugin=None, startup=False, initialize_implementation=True):
def all_plugins(self): if not name in self.all_plugins:
plugins = dict(self.plugins) self.logger.warn("Trying to load an unknown plugin {name}".format(**locals()))
plugins.update(self.disabled_plugins) return
return plugins
if plugin is None:
plugin = self.all_plugins[name]
def load_plugin(self, name, plugin):
try: try:
plugin.load() plugin.load()
plugin.validate() 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: except:
self.logger.exception("There was an error loading plugin %s" % name) 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): def unload_plugin(self, name):
if not name in self.plugins: if not name in self.all_plugins:
if name in self.disabled_plugins: self.logger.warn("Trying to unload unknown plugin {name}".format(**locals()))
plugin = self.disabled_plugins[name] return
else:
self.logger.warn("Trying to unload unknown plugin {name}".format(**locals())) plugin = self.all_plugins[name]
return
else:
plugin = self.plugins[name]
try: try:
self._deactivate_plugin(name, plugin) if plugin.enabled:
self.disable_plugin(name, plugin=plugin)
plugin.unload() plugin.unload()
self.on_plugin_unloaded(name, plugin)
if name in self.plugins: if name in self.plugins:
del self.plugins[name] del self.plugins[name]
if name in self.disabled_plugins: if name in self.disabled_plugins:
del self.disabled_plugins[name] del self.disabled_plugins[name]
plugin.loaded = False
self.logger.debug("Unloaded plugin {name}: {plugin}".format(**locals()))
except PluginLifecycleException as e:
raise e
except: except:
self.logger.exception("There was an error unloading plugin {name}".format(**locals())) 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: if not name in self.disabled_plugins:
self.logger.warn("Tried to enable plugin {name}, however it is not disabled".format(**locals())) self.logger.warn("Tried to enable plugin {name}, however it is not disabled".format(**locals()))
return 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: try:
del self.disabled_plugins[name]
plugin.enable() plugin.enable()
self._activate_plugin(name, plugin) self._activate_plugin(name, plugin)
self.initialize_implementation_of_plugin(name, plugin) except PluginLifecycleException as e:
self.plugins[name] = plugin raise e
except: except:
self.logger.exception("There was an error while enabling plugin {name}".format(**locals())) self.logger.exception("There was an error while enabling plugin {name}".format(**locals()))
self.disabled_plugins[name] = plugin return False
else: 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.on_plugin_enabled(name, plugin)
self.logger.debug("Enabled plugin {name}: {plugin}".format(**locals())) 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: if not name in self.plugins:
self.logger.warn("Tried to disable plugin {name}, however it is not enabled".format(**locals())) self.logger.warn("Tried to disable plugin {name}, however it is not enabled".format(**locals()))
return 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: try:
del self.plugins[name]
plugin.disable() plugin.disable()
self._deactivate_plugin(name, plugin) self._deactivate_plugin(name, plugin)
self.disabled_plugins[name] = plugin except PluginLifecycleException as e:
raise e
except: except:
self.logger.exception("There was an error while disabling plugin {name}".format(**locals())) self.logger.exception("There was an error while disabling plugin {name}".format(**locals()))
self.plugins[name] = plugin return False
else: 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.on_plugin_disabled(name, plugin)
self.logger.debug("Disabled plugin {name}: {plugin}".format(**locals())) self.logger.debug("Disabled plugin {name}: {plugin}".format(**locals()))
def _activate_plugin(self, name, plugin): return True
plugin.activate()
def _activate_plugin(self, name, plugin):
# evaluate registered hooks # evaluate registered hooks
for hook, callback in plugin.hooks.items(): for hook, callback in plugin.hooks.items():
self.plugin_hooks[hook].append((name, callback)) self.plugin_hooks[hook].append((name, callback))
# evaluate registered implementation # evaluate registered implementation
if plugin.implementation: if plugin.implementation:
if isinstance(plugin.implementation, RestartNeedingPlugin):
plugin.hotchangeable = False
for plugin_type in self.plugin_types: for plugin_type in self.plugin_types:
if isinstance(plugin.implementation, plugin_type): if isinstance(plugin.implementation, plugin_type):
self.plugin_implementations_by_type[plugin_type].append((name, plugin.implementation)) self.plugin_implementations_by_type[plugin_type].append((name, plugin.implementation))
self.plugin_implementations[name] = plugin.implementation self.plugin_implementations[name] = plugin.implementation
self.on_plugin_activated(name, plugin)
def _deactivate_plugin(self, name, plugin): def _deactivate_plugin(self, name, plugin):
plugin.deactivate()
for hook, callback in plugin.hooks.items(): 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: 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: for plugin_type in self.plugin_types:
try: try:
self.plugin_implementations_by_type[plugin_type].remove((name, plugin.implementation)) 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 # that's ok, the plugin was just not registered for the type
pass pass
self.on_plugin_deactivated(name, plugin)
def initialize_implementations(self, additional_injects=None, additional_inject_factories=None): def initialize_implementations(self, additional_injects=None, additional_inject_factories=None):
for name, plugin in self.plugins.items(): for name, plugin in self.plugins.items():
self.initialize_implementation_of_plugin(name, plugin, self.initialize_implementation_of_plugin(name, plugin,
@ -667,7 +727,7 @@ class PluginManager(object):
if plugin.implementation is None: if plugin.implementation is None:
return return
self.initialize_implementation(name, plugin, plugin.implementation, return self.initialize_implementation(name, plugin, plugin.implementation,
additional_injects=additional_injects, additional_injects=additional_injects,
additional_inject_factories=additional_inject_factories) additional_inject_factories=additional_inject_factories)
@ -710,18 +770,22 @@ class PluginManager(object):
for arg, value in return_value.items(): for arg, value in return_value.items():
setattr(implementation, "_" + arg, value) setattr(implementation, "_" + arg, value)
result = implementation.initialize() implementation.initialize()
if result is not None and not result:
self.logger.warn("Initialization of {name} returned False, disabling it".format(**locals())) except Exception as e:
self._deactivate_plugin(name, plugin)
return
except:
self.logger.exception("Exception while initializing plugin {name}, disabling it".format(**locals()))
self._deactivate_plugin(name, plugin) 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: else:
self.on_plugin_implementations_initialized(name, plugin) self.on_plugin_implementations_initialized(name, plugin)
self.logger.debug("Initialized plugin mixin implementation for plugin {name}".format(**locals())) 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=(" ", "!")): 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. Called by the plugin core after performing all injections. Override this to initialize your implementation.
""" """
pass 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}")

View file

@ -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" __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): class OctoPrintPlugin(Plugin):
@ -68,6 +68,8 @@ class OctoPrintPlugin(Plugin):
pass pass
class ReloadNeedingPlugin(Plugin):
pass
class StartupPlugin(OctoPrintPlugin): class StartupPlugin(OctoPrintPlugin):
""" """
@ -111,7 +113,7 @@ class ShutdownPlugin(OctoPrintPlugin):
pass 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 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 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() return dict()
class TemplatePlugin(OctoPrintPlugin): class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin):
""" """
Using the ``TemplatePlugin`` mixin plugins may inject their own components into the OctoPrint web interface. Using the ``TemplatePlugin`` mixin plugins may inject their own components into the OctoPrint web interface.
@ -549,7 +551,7 @@ class SimpleApiPlugin(OctoPrintPlugin):
return None return None
class BlueprintPlugin(OctoPrintPlugin): class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin):
""" """
The ``BlueprintPlugin`` mixin allows plugins to define their own full fledged endpoints for whatever purpose, 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. be it a more sophisticated API than what is possible via the :class:`SimpleApiPlugin` or a custom web frontend.

View file

@ -539,9 +539,15 @@ class Server():
self._logConf = logConf self._logConf = logConf
self._server = None self._server = None
self._logger = None
self._lifecycle_callbacks = defaultdict(list)
self._template_searchpaths = []
def run(self): def run(self):
if not self._allowRoot: if not self._allowRoot:
self._checkForRoot() self._check_for_root()
global printer global printer
global printerProfileManager global printerProfileManager
@ -566,12 +572,12 @@ class Server():
settings(init=True, basedir=self._basedir, configfile=self._configfile) settings(init=True, basedir=self._basedir, configfile=self._configfile)
# then initialize logging # then initialize logging
self._initLogging(self._debug, self._logConf) self._setup_logging(self._debug, self._logConf)
logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
def exception_logger(exc_type, exc_value, exc_tb): 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 sys.excepthook = exception_logger
logger.info("Starting OctoPrint %s" % DISPLAY_VERSION) self._logger.info("Starting OctoPrint %s" % DISPLAY_VERSION)
# then initialize the plugin manager # then initialize the plugin manager
pluginManager = octoprint.plugin.plugin_manager(init=True) 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.implementation_inject_factories=[octoprint_plugin_inject_factory, settings_plugin_inject_factory]
pluginManager.initialize_implementations() pluginManager.initialize_implementations()
def on_plugin_event_factory(text): lifecycleManager = LifecycleManager(self, pluginManager)
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() pluginManager.log_all_plugins()
# initialize slicing manager and register it for changes in the registered plugins
slicingManager.initialize() slicingManager.initialize()
lifecycleManager.add_callback(["enabled", "disabled"], lambda name, plugin: slicingManager.reload_slicers())
# configure additional template folders for jinja2 # setup jinja2
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin) self._setup_jinja2()
additional_template_folders = [] def template_enabled(name, plugin):
for plugin in template_plugins: if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.TemplatePlugin):
folder = plugin.get_template_folder() return
if folder is not None: self._register_additional_template_plugin(plugin.implementation)
additional_template_folders.append(plugin.get_template_folder()) def template_disabled(name, plugin):
if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.TemplatePlugin):
import jinja2 return
jinja_loader = jinja2.ChoiceLoader([ self._unregister_additional_template_plugin(plugin.implementation)
app.jinja_loader, lifecycleManager.add_callback("enabled", template_enabled)
jinja2.FileSystemLoader(additional_template_folders) lifecycleManager.add_callback("disabled", template_disabled)
])
app.jinja_loader = jinja_loader
del jinja2
app.jinja_env.add_extension("jinja2.ext.do")
# configure timelapse # configure timelapse
octoprint.timelapse.configureTimelapse() octoprint.timelapse.configureTimelapse()
@ -660,7 +654,7 @@ class Server():
clazz = octoprint.util.get_class(userManagerName) clazz = octoprint.util.get_class(userManagerName)
userManager = clazz() userManager = clazz()
except AttributeError, e: 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 = util.ReverseProxied(
app.wsgi_app, app.wsgi_app,
@ -696,36 +690,20 @@ class Server():
app.debug = self._debug app.debug = self._debug
from octoprint.server.api import api
from octoprint.server.apps import apps
# register API blueprint # register API blueprint
app.register_blueprint(api, url_prefix="/api") self._setup_blueprints()
app.register_blueprint(apps, url_prefix="/apps") def blueprint_enabled(name, plugin):
if plugin.implementation is None or not isinstance(plugin.implementation, octoprint.plugin.BlueprintPlugin):
# also register any blueprints defined in BlueprintPlugins return
blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin) self._register_blueprint_plugin(plugin.implementation)
for plugin in blueprint_plugins: lifecycleManager.add_callback(["enabled"], blueprint_enabled)
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))
## Tornado initialization starts here ## Tornado initialization starts here
ioloop = IOLoop() ioloop = IOLoop()
ioloop.install() 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"])) upload_suffixes = dict(name=settings().get(["server", "uploads", "nameSuffix"]), path=settings().get(["server", "uploads", "pathSuffix"]))
self._tornado_app = Application(self._router.urls + [ self._tornado_app = Application(self._router.urls + [
@ -759,9 +737,16 @@ class Server():
"on_startup", "on_startup",
args=(self._host, self._port)) 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 # prepare our after startup function
def on_after_startup(): 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 # 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 # 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(): def work():
octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin, octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin,
"on_after_startup") "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 import threading
threading.Thread(target=work).start() threading.Thread(target=work).start()
ioloop.add_callback(on_after_startup) ioloop.add_callback(on_after_startup)
# prepare our shutdown function # prepare our shutdown function
def on_shutdown(): def on_shutdown():
logger.info("Goodbye!") self._logger.info("Goodbye!")
observer.stop() observer.stop()
observer.join() observer.join()
octoprint.plugin.call_plugin(octoprint.plugin.ShutdownPlugin, octoprint.plugin.call_plugin(octoprint.plugin.ShutdownPlugin,
@ -789,18 +782,18 @@ class Server():
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
except: 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!") 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!")
logger.exception("Stacktrace follows:") self._logger.exception("Stacktrace follows:")
def _createSocketConnection(self, session): def _create_socket_connection(self, session):
global printer, fileManager, analysisQueue, userManager, eventManager global printer, fileManager, analysisQueue, userManager, eventManager
return util.sockjs.PrinterStateConnection(printer, fileManager, analysisQueue, userManager, eventManager, pluginManager, session) 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: if "geteuid" in dir(os) and os.geteuid() == 0:
exit("You should not run OctoPrint as root!") exit("You should not run OctoPrint as root!")
def _initLogging(self, debug, logConf=None): def _setup_logging(self, debug, logConf=None):
defaultConfig = { defaultConfig = {
"version": 1, "version": 1,
"formatters": { "formatters": {
@ -871,6 +864,115 @@ class Server():
logging.getLogger("SERIAL").setLevel(logging.DEBUG) logging.getLogger("SERIAL").setLevel(logging.DEBUG)
logging.getLogger("SERIAL").debug("Enabling serial logging") 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__": if __name__ == "__main__":
server = Server() server = Server()
server.run() server.run()

View file

@ -224,7 +224,9 @@ default_settings = {
{ "name": "Suppress M105 requests/responses", "regex": "(Send: M105)|(Recv: ok (B|T\d*):)" }, { "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)" } { "name": "Suppress M27 requests/responses", "regex": "(Send: M27)|(Recv: SD printing byte)" }
], ],
"plugins": {}, "plugins": {
"_disabled": []
},
"scripts": { "scripts": {
"gcode": { "gcode": {
"afterPrintCancelled": "; disable motors\nM84\n\n;disable all heaters\n{% snippet 'disable_hotends' %}\nM140 S0\n\n;disable fan\nM106 S0", "afterPrintCancelled": "; disable motors\nM84\n\n;disable all heaters\n{% snippet 'disable_hotends' %}\nM140 S0\n\n;disable fan\nM106 S0",

View file

@ -113,16 +113,18 @@ class SlicingManager(object):
Initializes the slicing manager by loading and initializing all available Initializes the slicing manager by loading and initializing all available
:class:`~octoprint.plugin.SlicerPlugin` implementations. :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 Retrieves all registered :class:`~octoprint.plugin.SlicerPlugin` implementations and registers them as
available slicers. available slicers.
""" """
plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin) plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SlicerPlugin)
slicers = dict()
for plugin in plugins: for plugin in plugins:
self._slicers[plugin.get_slicer_properties()["type"]] = plugin slicers[plugin.get_slicer_properties()["type"]] = plugin
self._slicers = slicers
@property @property
def slicing_enabled(self): def slicing_enabled(self):