Plugins can now also be retrieved via entry_points, also added EventPlugin and afterStartup handler in StartupPlugin
This commit is contained in:
parent
b5b0ea5980
commit
08e4c05129
9 changed files with 155 additions and 73 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue