diff --git a/src/octoprint/events.py b/src/octoprint/events.py index 638713dc..4a3d2023 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -1,21 +1,28 @@ # coding=utf-8 +from __future__ import (print_function, absolute_import) -__author__ = "Lars Norpchen" -__author__ = "Gina Häußge " +__author__ = "Gina Häußge , Lars Norpchen" __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 datetime import logging import subprocess import Queue import threading +import collections from octoprint.settings import settings +import octoprint.plugin # singleton _instance = None +def all_events(): + return [name for name in Events.__dict__ if not name.startswith("__")] + + class Events(object): # application startup STARTUP = "Startup" @@ -87,7 +94,7 @@ class EventManager(object): """ def __init__(self): - self._registeredListeners = {} + self._registeredListeners = collections.defaultdict(list) self._logger = logging.getLogger(__name__) self._queue = Queue.PriorityQueue() @@ -99,9 +106,7 @@ class EventManager(object): while True: (event, payload) = self._queue.get(True) - eventListeners = self._registeredListeners.get(event, None) - if eventListeners is None: - return + eventListeners = self._registeredListeners[event] self._logger.debug("Firing event: %s (Payload: %r)" % (event, payload)) for listener in eventListeners: @@ -111,6 +116,10 @@ class EventManager(object): except: self._logger.exception("Got an exception while sending event %s (Payload: %r) to %s" % (event, payload, listener)) + octoprint.plugin.call_plugin(octoprint.plugin.types.EventHandlerPlugin, + "on_event", + args=[event, payload]) + def fire(self, event, payload=None): """ Fire an event to anyone subscribed to it @@ -122,8 +131,6 @@ class EventManager(object): payload being a payload object specific to the event. """ - if not event in self._registeredListeners.keys(): - return self._queue.put((event, payload), 0) def subscribe(self, event, callback): @@ -131,9 +138,6 @@ class EventManager(object): Subscribe a listener to an event -- pass in the event name (as a string) and the callback object """ - if not event in self._registeredListeners.keys(): - self._registeredListeners[event] = [] - if callback in self._registeredListeners[event]: # callback is already subscribed to the event return @@ -146,10 +150,6 @@ class EventManager(object): Unsubscribe a listener from an event -- pass in the event name (as string) and the callback object """ - if not event in self._registeredListeners: - # no callback registered for callback, just return - return - if not callback in self._registeredListeners[event]: # callback not subscribed to event, just return return diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index b295fbbc..6278b2a1 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -14,16 +14,25 @@ from octoprint.plugin.types import * # singleton _instance = None -def plugin_manager(init=False, plugin_folders=None, plugin_types=None): +def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None): global _instance if _instance is None: if init: if plugin_folders is None: plugin_folders = (settings().getBaseFolder("plugins"), os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins"))) if plugin_types is None: - plugin_types = [StartupPlugin, TemplatePlugin, SettingsPlugin, SimpleApiPlugin, AssetPlugin, BlueprintPlugin] + plugin_types = [StartupPlugin, + ShutdownPlugin, + TemplatePlugin, + SettingsPlugin, + SimpleApiPlugin, + AssetPlugin, + BlueprintPlugin, + EventHandlerPlugin] + if plugin_entry_points is None: + plugin_entry_points = "octoprint.plugin" - _instance = PluginManager(plugin_folders, plugin_types) + _instance = PluginManager(plugin_folders, plugin_types, plugin_entry_points) else: raise ValueError("Plugin Manager not initialized yet") return _instance diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index f82ce15e..4de642d1 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -26,11 +26,13 @@ class PluginInfo(object): attr_check = '__plugin_check__' - def __init__(self, key, location, instance): + def __init__(self, key, location, instance, version=None): self.key = key self.location = location self.instance = instance + self._version = version + def __str__(self): return "{name} ({version})".format(name=self.name, version=self.version if self.version else "unknown") @@ -60,7 +62,7 @@ class PluginInfo(object): @property def version(self): - return self._get_instance_attribute(self.__class__.attr_version, default=None) + return self._version if self._version is not None else self._get_instance_attribute(self.__class__.attr_version, default=None) @property def hooks(self): @@ -82,7 +84,7 @@ class PluginInfo(object): class PluginManager(object): - def __init__(self, plugin_folders, plugin_types, plugin_disabled_list=None): + def __init__(self, plugin_folders, plugin_types, plugin_entry_points, plugin_disabled_list=None): self.logger = logging.getLogger(__name__) if plugin_disabled_list is None: @@ -90,6 +92,7 @@ class PluginManager(object): 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.plugins = dict() @@ -100,8 +103,14 @@ class PluginManager(object): def _find_plugins(self): plugins = dict() + if self.plugin_folders: + self._add_plugins_from_folders(self.plugin_folders, plugins) + if self.plugin_entry_points: + self._add_plugins_from_entry_points(self.plugin_entry_points, plugins) + return plugins - for folder in self.plugin_folders: + def _add_plugins_from_folders(self, folders, plugins): + for folder in folders: if not os.path.exists(folder): self.logger.warn("Plugin folder {folder} could not be found, skipping it".format(folder=folder)) continue @@ -110,38 +119,77 @@ class PluginManager(object): 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")): - id = entry + key = entry elif os.path.isfile(path) and entry.endswith(".py"): - id = entry[:-3] # strip off the .py extension + key = entry[:-3] # strip off the .py extension else: continue - if self._is_plugin_disabled(id): + if self._is_plugin_disabled(key): # plugin is disabled, ignore it continue - if id in plugins: + if key in plugins: # plugin is already defined, ignore it continue - module = imp.find_module(id, [folder]) - plugin = self._load_plugin(id, *module) - if plugin.check(): - plugins[id] = plugin - else: - self.logger.warn("Plugin \"{plugin}\" did not pass check, disabling it".format(plugin=str(plugin))) + plugin = self._load_plugin_from_module(key, folder=folder) + if plugin: + plugins[key] = plugin return plugins - def _load_plugin(self, id, f, filename, description): + def _add_plugins_from_entry_points(self, groups, plugins): + import pkg_resources + + 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 self._is_plugin_disabled(key): + # plugin is disabled, ignore it + continue + + if key in plugins: + # plugin is already defined, ignore it + continue + + plugin = self._load_plugin_from_module(key, module_name=module_name, version=version) + if plugin: + plugins[id] = plugin + + return plugins + + def _load_plugin_from_module(self, id, folder=None, module_name=None, version=None): + # TODO error handling + if folder: + module = imp.find_module(id, [folder]) + elif module_name: + module = imp.find_module(module_name) + else: + return None + + plugin = self._load_plugin(id, *module, version=version) + if plugin.check(): + return plugin + else: + self.logger.warn("Plugin \"{plugin}\" did not pass check, disabling it".format(plugin=str(plugin))) + return None + + def _load_plugin(self, id, f, filename, description, version=None): instance = imp.load_module(id, f, filename, description) - return PluginInfo(id, filename, instance) + return PluginInfo(id, filename, instance, version=version) def _is_plugin_disabled(self, id): return id in self.plugin_disabled_list or id.endswith('disabled') def reload_plugins(self): - self.logger.info("Loading plugins from {folders}...".format(folders=", ".join(self.plugin_folders))) + self.logger.info("Loading plugins from {folders} and installed plugin packages...".format(folders=", ".join(self.plugin_folders))) self.plugins = self._find_plugins() for name, plugin in self.plugins.items(): diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 92d34418..f1403916 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -13,6 +13,9 @@ class StartupPlugin(Plugin): def on_startup(self, host, port): pass + def on_after_startup(self): + pass + class ShutdownPlugin(Plugin): def on_shutdown(self): @@ -59,4 +62,8 @@ class SettingsPlugin(Plugin): pass +class EventHandlerPlugin(Plugin): + def on_event(self, event, payload): + pass + diff --git a/src/octoprint/plugins/discovery/__init__.py b/src/octoprint/plugins/discovery/__init__.py index 359738c6..64409169 100644 --- a/src/octoprint/plugins/discovery/__init__.py +++ b/src/octoprint/plugins/discovery/__init__.py @@ -11,6 +11,7 @@ import os import flask import octoprint.plugin +import octoprint.util default_settings = { "publicHost": None, @@ -18,7 +19,7 @@ default_settings = { "pathPrefix": None, "httpUsername": None, "httpPassword": None, - "upnpUuid": None + "upnpUuid": None } s = octoprint.plugin.plugin_settings("discovery", defaults=default_settings) @@ -60,34 +61,10 @@ def discovery(): response.headers['Content-Type'] = 'application/xml' return response -def interface_addresses(family=None): - import netifaces - if not family: - family = netifaces.AF_INET - - for interface in netifaces.interfaces(): - ifaddresses = netifaces.ifaddresses(interface) - if family in ifaddresses: - for ifaddress in ifaddresses[family]: - yield ifaddress["addr"] - -def address_for_client(client): - import socket - - for address in interface_addresses(): - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.bind((address, 0)) - sock.connect(client) - return address - except Exception as e: - pass - - class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.types.ShutdownPlugin, octoprint.plugin.types.BlueprintPlugin, - octoprint.plugin.SettingsPlugin): + octoprint.plugin.types.SettingsPlugin): def __init__(self): self.logger = logging.getLogger("octoprint.plugins." + __name__) @@ -257,7 +234,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, if alive and not self._ssdp_monitor_active: return - for addr in interface_addresses(): + for addr in octoprint.util.interface_addresses(): try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -320,7 +297,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, data, address = sock.recvfrom(4096) request = Request(data) if not request.error_code and request.command == "M-SEARCH" and request.path == "*" and (request.headers["ST"] == "upnp:rootdevice" or request.headers["ST"] == "ssdp:all") and request.headers["MAN"] == '"ssdp:discover"': - interface_address = address_for_client(address) + interface_address = octoprint.util.address_for_client(*address) if not interface_address: self.logger.warn("Can't determine address to user for client {}, not sending a M-SEARCH reply".format(address)) continue diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 473c8270..1f32ba08 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -301,11 +301,29 @@ class Server(): observer.schedule(util.watchdog.UploadCleanupWatchdogHandler(gcodeManager), settings().getBaseFolder("uploads")) observer.start() - # now it's the turn of the startup plugins + ioloop = IOLoop.instance() + + # run our startup plugins octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin, "on_startup", args=(self._host, self._port)) + # prepare our after startup function + def on_after_startup(): + 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 + # or service xmls or the like). While they are working though the ioloop would block. Therefore we'll + # create a single use thread in which to perform our after-startup-tasks, start that and hand back + # control to the ioloop + def work(): + octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin, + "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!") @@ -315,9 +333,8 @@ class Server(): "on_shutdown") atexit.register(on_shutdown) - logger.info("Listening on http://%s:%d" % (self._host, self._port)) try: - IOLoop.instance().start() + ioloop.start() except KeyboardInterrupt: pass except: diff --git a/src/octoprint/server/util/sockjs.py b/src/octoprint/server/util/sockjs.py index 8f4a49a9..a39b565f 100644 --- a/src/octoprint/server/util/sockjs.py +++ b/src/octoprint/server/util/sockjs.py @@ -15,10 +15,6 @@ from octoprint.events import Events class PrinterStateConnection(sockjs.tornado.SockJSConnection): - EVENTS = [Events.UPDATED_FILES, Events.METADATA_ANALYSIS_FINISHED, Events.MOVIE_RENDERING, Events.MOVIE_DONE, - Events.MOVIE_FAILED, Events.SLICING_STARTED, Events.SLICING_DONE, Events.SLICING_FAILED, - Events.TRANSFER_STARTED, Events.TRANSFER_DONE] - def __init__(self, printer, gcodeManager, userManager, eventManager, session): sockjs.tornado.SockJSConnection.__init__(self, session) @@ -54,7 +50,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): octoprint.timelapse.registerCallback(self) self._eventManager.fire(Events.CLIENT_OPENED, {"remoteAddress": remoteAddress}) - for event in PrinterStateConnection.EVENTS: + for event in octoprint.events.all_events(): self._eventManager.subscribe(event, self._onEvent) octoprint.timelapse.notifyCallbacks(octoprint.timelapse.current) @@ -66,7 +62,7 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection): octoprint.timelapse.unregisterCallback(self) self._eventManager.fire(Events.CLIENT_CLOSED) - for event in PrinterStateConnection.EVENTS: + for event in octoprint.events.all_events(): self._eventManager.unsubscribe(event, self._onEvent) def on_message(self, message): diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index d889ddce..02a79938 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -252,3 +252,28 @@ def dict_merge(a, b): class Object(object): pass + +def interface_addresses(family=None): + import netifaces + if not family: + family = netifaces.AF_INET + + for interface in netifaces.interfaces(): + ifaddresses = netifaces.ifaddresses(interface) + if family in ifaddresses: + for ifaddress in ifaddresses[family]: + yield ifaddress["addr"] + +def address_for_client(host, port): + import socket + + for address in interface_addresses(): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind((address, 0)) + sock.connect((host, port)) + return address + except Exception as e: + pass + + diff --git a/tests/test_plugin_core.py b/tests/test_plugin_core.py index 0dfcf8b6..1d436940 100644 --- a/tests/test_plugin_core.py +++ b/tests/test_plugin_core.py @@ -10,10 +10,13 @@ class PluginTestCase(unittest.TestCase): import logging logging.basicConfig(level=logging.DEBUG) + # TODO mock pkg_resources to return some defined entry_points + import os plugin_folders = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_plugins")] plugin_types = [octoprint.plugin.SettingsPlugin, octoprint.plugin.StartupPlugin] - self.plugin_manager = octoprint.plugin.core.PluginManager(plugin_folders, plugin_types) + plugin_entry_points = None + self.plugin_manager = octoprint.plugin.core.PluginManager(plugin_folders, plugin_types, plugin_entry_points) def test_plugin_loading(self): self.assertEquals(4, len(self.plugin_manager.plugins))