Plugins can now also be retrieved via entry_points, also added EventPlugin and afterStartup handler in StartupPlugin

This commit is contained in:
Gina Häußge 2014-09-10 15:44:53 +02:00
parent b5b0ea5980
commit 08e4c05129
9 changed files with 155 additions and 73 deletions

View file

@ -1,21 +1,28 @@
# coding=utf-8
from __future__ import (print_function, absolute_import)
__author__ = "Lars Norpchen"
__author__ = "Gina Häußge <osd@foosel.net>"
__author__ = "Gina Häußge <osd@foosel.net>, 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

View file

@ -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

View file

@ -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():

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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):

View file

@ -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

View file

@ -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))