# coding=utf-8 from __future__ import absolute_import __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" import os import imp from collections import defaultdict import logging class PluginInfo(object): attr_name = '__plugin_name__' attr_description = '__plugin_description__' attr_version = '__plugin_version__' attr_author = '__plugin_author__' attr_url = '__plugin_url__' attr_license = '__plugin_license__' attr_hooks = '__plugin_hooks__' attr_implementations = '__plugin_implementations__' attr_helpers = '__plugin_helpers__' attr_check = '__plugin_check__' attr_init = '__plugin_init__' def __init__(self, key, location, instance, name=None, version=None, description=None, author=None, url=None, license=None): self.key = key self.location = location self.instance = instance self.origin = None self.enabled = True self.bundled = False self._name = name self._version = version self._description = description self._author = author self._url = url self._license = license def __str__(self): return "{name} ({version})".format(name=self.name, version=self.version if self.version else "unknown") def get_hook(self, hook): if not hook in self.hooks: return None return self.hooks[hook] def get_implementations(self, *types): result = set() for implementation in self.implementations: matches_all = True for type in types: if not isinstance(implementation, type): matches_all = False if matches_all: result.add(implementation) return result @property def name(self): return self._get_instance_attribute(self.__class__.attr_name, defaults=(self._name, self.key)) @property def description(self): return self._get_instance_attribute(self.__class__.attr_description, default=self._description) @property def version(self): return self._version if self._version is not None else self._get_instance_attribute(self.__class__.attr_version, default=self._version) @property def author(self): return self._get_instance_attribute(self.__class__.attr_author, default=self._author) @property def url(self): return self._get_instance_attribute(self.__class__.attr_url, default=self._url) @property def license(self): return self._get_instance_attribute(self.__class__.attr_license, default=self._license) @property def hooks(self): return self._get_instance_attribute(self.__class__.attr_hooks, default={}) @property def implementations(self): return self._get_instance_attribute(self.__class__.attr_implementations, default=[]) @property def helpers(self): return self._get_instance_attribute(self.__class__.attr_helpers, default={}) @property def check(self): return self._get_instance_attribute(self.__class__.attr_check, default=lambda: True) @property def init(self): return self._get_instance_attribute(self.__class__.attr_init, default=lambda: True) def _get_instance_attribute(self, attr, default=None, defaults=None): if not hasattr(self.instance, attr): if defaults is not None: for value in defaults: if value is not None: return value return default return getattr(self.instance, attr) class PluginManager(object): def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None): self.logger = logging.getLogger(__name__) if logging_prefix is None: logging_prefix = "" if plugin_disabled_list is None: plugin_disabled_list = [] self.plugin_folders = plugin_folders self.plugin_types = plugin_types self.plugin_entry_points = plugin_entry_points self.plugin_disabled_list = plugin_disabled_list self.logging_prefix = logging_prefix self.plugins = dict() self.plugin_hooks = defaultdict(list) self.plugin_implementations = defaultdict(set) self.plugin_implementations_by_type = defaultdict(list) self.disabled_plugins = dict() self.registered_clients = [] self.reload_plugins() def _find_plugins(self): plugins = dict() disabled_plugins = dict() if self.plugin_folders: self._add_plugins_from_folders(self.plugin_folders, plugins, disabled_plugins) if self.plugin_entry_points: self._add_plugins_from_entry_points(self.plugin_entry_points, plugins, disabled_plugins) return plugins, disabled_plugins def _add_plugins_from_folders(self, folders, plugins, disabled_plugins): for folder in folders: readonly = False if isinstance(folder, (list, tuple)): if len(folder) == 2: folder, readonly = folder else: continue if not os.path.exists(folder): self.logger.warn("Plugin folder {folder} could not be found, skipping it".format(folder=folder)) continue entries = os.listdir(folder) for entry in entries: path = os.path.join(folder, entry) if os.path.isdir(path) and os.path.isfile(os.path.join(path, "__init__.py")): key = entry elif os.path.isfile(path) and entry.endswith(".py"): key = entry[:-3] # strip off the .py extension else: continue if key in plugins: # plugin is already defined, ignore it continue plugin = self._load_plugin_from_module(key, folder=folder) if plugin: plugin.origin = ("folder", folder) if readonly: plugin.bundled = True if self._is_plugin_disabled(key): plugin.enabled = False disabled_plugins[key] = plugin else: plugins[key] = plugin return plugins, disabled_plugins def _add_plugins_from_entry_points(self, groups, plugins, disabled_plugins): import pkg_resources import pkginfo 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): key = entry_point.name module_name = entry_point.module_name version = entry_point.dist.version if key in plugins: # plugin is already defined, ignore it continue kwargs = dict(module_name=module_name, version=version) try: module_pkginfo = pkginfo.Installed(module_name) except: self.logger.exception("Something went wrong while retrieving package info data for module %s" % module_name) else: kwargs.update(dict( name=module_pkginfo.name, summary=module_pkginfo.summary, author=module_pkginfo.author, url=module_pkginfo.home_page, license=module_pkginfo.license )) plugin = self._load_plugin_from_module(key, **kwargs) if plugin: plugin.origin = ("entry_point", group, module_name) if self._is_plugin_disabled(key): plugin.enabled = False disabled_plugins[key] = plugin else: plugins[key] = plugin 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): # TODO error handling try: if folder: module = imp.find_module(key, [folder]) elif module_name: module = imp.find_module(module_name) else: return None except: 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) if plugin: if plugin.check(): return plugin else: self.logger.warn("Plugin \"{plugin}\" did not pass check".format(plugin=str(plugin))) return None def _load_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) except: self.logger.exception("Error loading plugin {key}, disabling it".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): self.logger.info("Loading plugins from {folders} and installed plugin packages...".format(folders=", ".join(self.plugin_folders))) self.plugins, self.disabled_plugins = self._find_plugins() for name, plugin in self.plugins.items(): # initialize the plugin plugin.init() # 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 ) self.plugin_implementations[name].update(plugin.get_implementations()) self.log_registered_plugins() def initialize_implementations(self, 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: 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() except: self.logger.exception("Exception while initializing plugin") # TODO disable plugin! self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations))) def log_registered_plugins(self): if len(self.plugins) <= 0: self.logger.info("No plugins found") else: self.logger.info("Found {count} plugin(s): {plugins}".format(count=len(self.plugins), plugins=", ".join(map(lambda x: str(x), self.plugins.values())))) if len(self.disabled_plugins) > 0: self.logger.info("{count} plugin(s) currently disabled: {plugins}".format(count=len(self.disabled_plugins), plugins=", ".join(map(lambda x: str(x), self.disabled_plugins.values())))) def get_plugin(self, name): if not name in self.plugins: return None return self.plugins[name].instance def get_hooks(self, hook): if not hook in self.plugin_hooks: return dict() return {hook[0]: hook[1] for hook in self.plugin_hooks[hook]} def get_implementations(self, *types): result = None for t in types: implementations = self.plugin_implementations_by_type[t] if result is None: result = set(implementations) else: result = result.intersection(implementations) if result is None: return dict() return {impl[0]: impl[1] for impl in result} def get_helpers(self, name, *helpers): if not name in self.plugins: return None plugin = self.plugins[name] all_helpers = plugin.helpers if len(helpers): return dict((k, v) for (k, v) in all_helpers.items() if k in helpers) else: return all_helpers def register_client(self, client): if client is None: return self.registered_clients.append(client) def unregister_client(self, client): self.registered_clients.remove(client) def send_plugin_message(self, plugin, data): for client in self.registered_clients: try: client.sendPluginMessage(plugin, data) except: self.logger.exception("Exception while sending plugin data to client") class Plugin(object): def initialize(self): pass