diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 63f30d06..b295fbbc 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -21,7 +21,7 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None): 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] + plugin_types = [StartupPlugin, TemplatePlugin, SettingsPlugin, SimpleApiPlugin, AssetPlugin, BlueprintPlugin] _instance = PluginManager(plugin_folders, plugin_types) else: @@ -33,6 +33,22 @@ def plugin_settings(plugin_key, defaults=None): return PluginSettings(settings(), plugin_key, defaults=defaults) +def call_plugin(types, method, args=None, kwargs=None, callback=None): + if not isinstance(types, (list, tuple)): + types = [types] + if args is None: + args = [] + if kwargs is None: + kwargs = dict() + + plugins = plugin_manager().get_implementations(*types) + for name, plugin in plugins.items(): + if hasattr(plugin, method): + result = getattr(plugin, method)(*args, **kwargs) + if callback: + callback(name, plugin, result) + + class PluginSettings(object): def __init__(self, settings, plugin_key, defaults=None): self.settings = settings diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index f23ca21b..a82c0613 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -41,6 +41,11 @@ class SimpleApiPlugin(Plugin): return None +class BlueprintPlugin(Plugin): + def get_blueprint(self): + return None + + class SettingsPlugin(TemplatePlugin): def on_settings_load(self): return None diff --git a/src/octoprint/plugins/discovery/__init__.py b/src/octoprint/plugins/discovery/__init__.py index fc3f53d2..71f35ec0 100644 --- a/src/octoprint/plugins/discovery/__init__.py +++ b/src/octoprint/plugins/discovery/__init__.py @@ -8,6 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import logging import os +import flask import octoprint.plugin @@ -20,13 +21,48 @@ default_settings = { s = octoprint.plugin.plugin_settings("discovery", defaults=default_settings) -class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin): +def get_uuid(): + import uuid + return str(uuid.uuid4()) +UUID = get_uuid() +del get_uuid + +def get_instance_name(): + import socket + return "OctoPrint instance on {}".format(socket.gethostname()) +INSTANCENAME = get_instance_name() +del get_instance_name + + +blueprint = flask.Blueprint("plugin.discovery", __name__, template_folder=os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")) + +@blueprint.route("/discovery.xml") +def discovery(): + logging.getLogger("octoprint.plugins." + __name__).info("Rendering discovery.xml") + response = flask.make_response(flask.render_template("discovery.jinja2", + friendlyName=INSTANCENAME, + manufacturer="OctoPrint", + manufacturerUrl="http://www.octoprint.org", + modelDescription="Some funny description", + modelName="Some funny name", + uuid=UUID, + presentationUrl=flask.url_for("index", _external=True))) + response.headers['Content-Type'] = 'application/xml' + return response + + +class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.types.BlueprintPlugin, octoprint.plugin.SettingsPlugin): def __init__(self): self.logger = logging.getLogger("octoprint.plugins." + __name__) self.octoprint_sd_ref = None self.http_sd_ref = None + ##~~ BlueprintPlugin API + + def get_blueprint(self): + return blueprint + ##~~ TemplatePlugin API (part of SettingsPlugin) def get_template_vars(self): @@ -41,6 +77,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin): def on_startup(self, host, port): self._bonjour_register(host, port) + self._ssdp_register(port) #~~ SettingsPlugin API @@ -66,7 +103,6 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin): def _bonjour_register(self, host, port): import pybonjour - import socket def register_callback(sd_ref, flags, error_code, name, reg_type, domain): if error_code == pybonjour.kDNSServiceErr_NoError: @@ -75,17 +111,8 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin): if s.getInt(["publicPort"]): port = s.getInt(["publicPort"]) - prefix = s.globalGet(["server", "reverseProxy", "prefixFallback"]) - path = "/" - if s.get(["pathPrefix"]): - path = s.get(["pathPrefix"]) - elif prefix: - path = prefix - - name = "OctoPrint instance on {}".format(socket.gethostname()) - self.octoprint_sd_ref = pybonjour.DNSServiceRegister( - name=name, + name=INSTANCENAME, regtype='_octoprint._tcp', port=port, txtRecord=pybonjour.TXTRecord(self._create_octoprint_txt_record_dict()), @@ -94,7 +121,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin): pybonjour.DNSServiceProcessResult(self.octoprint_sd_ref) self.http_sd_ref = pybonjour.DNSServiceRegister( - name=name, + name=INSTANCENAME, regtype='_http._tcp', port=port, txtRecord=pybonjour.TXTRecord(self._create_base_txt_record_dict()), @@ -139,6 +166,101 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin): return entries + def _ssdp_register(self, port): + import threading + self._ssdp_monitor_thread = threading.Thread(target=self._ssdp_monitor, args=[port]) + self._ssdp_monitor_thread.daemon = True + self._ssdp_monitor_thread.start() + + def _ssdp_notify(self, port, alive=True): + import socket + + def interface_addresses(family=socket.AF_INET): + for fam, _, _, _, sockaddr in socket.getaddrinfo('', None): + if family == fam: + yield sockaddr[0] + + for addr in interface_addresses(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + sock.bind((addr, 0)) + + location = "http://{addr}:{port}/plugins/discovery/discovery.xml".format(addr=addr, port=port) + + self.logger.info("Sending NOTIFY alive") + notify_message = "".join(["NOTIFY * HTTP/1.1\r\n", "Server: Python/2.7\r\n", "Cache-Control: max-age=900\r\n", "Location: {location}\r\n", "NTS: {nts}\r\n", "NT: upnp:rootdevice\r\n", "USN: uuid:{uuid}::upnp:rootdevice\r\n", "Host: 239.255.255.250:1900\r\n\r\n"]) + message = notify_message.format(uuid=UUID, location=location, nts="ssdp:alive" if alive else "ssdp:byebye") + for _ in xrange(2): + sock.sendto(message, ("239.255.255.250", 1900)) + + try: + sock.recv(1024) + except socket.timeout: + pass + + def _ssdp_monitor(self, port): + + from BaseHTTPServer import BaseHTTPRequestHandler + from httplib import HTTPResponse + from StringIO import StringIO + import socket + import struct + + socket.setdefaulttimeout(5) + + location_message = "".join(["HTTP/1.1 200 OK\r\n", "ST: upnp:rootdevice\r\n", "USN: uuid:{uuid}::upnp:rootdevice\r\n", "Location: {location}\r\n", "Cache-Control: max-age=60\r\n\r\n"]) + + class Request(BaseHTTPRequestHandler): + + def __init__(self, request_text): + self.rfile = StringIO(request_text) + self.raw_requestline = self.rfile.readline() + self.error_code = self.error_message = None + self.parse_request() + + def send_error(self, code, message=None): + self.error_code = code + self.error_message = message + + class Response(HTTPResponse): + def __init__(self, response_text): + self.fp = StringIO(response_text) + self.debuglevel = 0 + self.strict = 0 + self.msg = None + self._method = None + self.begin() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + sock.bind(('', 1900)) + + sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton('239.255.255.250') + socket.inet_aton('0.0.0.0')) + + self.logger.info("Registered {} for SSDP".format(INSTANCENAME)) + + self._ssdp_notify(port, alive=True) + + try: + while (True): + try: + 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"': + message = location_message.format(uuid=UUID, location="http://192.168.1.3:5000/plugin/discovery/discovery.xml") + sock.sendto(message, address) + self.logger.info("Sent M-SEARCH reply for {path} and {st} to {address!r}".format(path=request.path, st=request.headers["ST"], address=address)) + except socket.timeout: + self._ssdp_notify(port, alive=True) + finally: + try: + sock.close() + except: + pass + + __plugin_name__ = "Discovery" __plugin_version__ = "0.1" __plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf" diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index d7e7f378..ef6a5c59 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -263,8 +263,14 @@ class Server(): from octoprint.server.api import api + # register API blueprint app.register_blueprint(api, url_prefix="/api") + # also register any blueprints defined in BlueprintPlugins + octoprint.plugin.call_plugin(octoprint.plugin.types.BlueprintPlugin, + "get_blueprint", + callback=lambda name, _, blueprint: app.register_blueprint(blueprint, url_prefix="/plugin/{name}".format(name=name))) + self._router = SockJSRouter(self._createSocketConnection, "/sockjs") upload_suffixes = dict(name=settings().get(["server", "uploads", "nameSuffix"]), path=settings().get(["server", "uploads", "pathSuffix"])) @@ -295,9 +301,9 @@ class Server(): observer.start() # now it's the turn of the startup plugins - startup_plugins = pluginManager.get_implementations(octoprint.plugin.StartupPlugin) - for name, plugin in startup_plugins.items(): - plugin.on_startup(self._host, self._port) + octoprint.plugin.call_plugin(octoprint.plugin.StartupPlugin, + "on_startup", + args=(self._host, self._port)) logger.info("Listening on http://%s:%d" % (self._host, self._port)) try: diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py index 4e9c527e..93c9b567 100644 --- a/src/octoprint/server/api/settings.py +++ b/src/octoprint/server/api/settings.py @@ -106,15 +106,17 @@ def getSettings(): } } - settings_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin) - for name, plugin in settings_plugins.items(): - plugin_data = plugin.on_settings_load() - if plugin_data: + def process_plugin_result(name, plugin, result): + if result: if not "plugins" in data: data["plugins"] = dict() - if "__enabled" in plugin_data: - del plugin_data["__enabled"] - data["plugins"][name] = plugin_data + if "__enabled" in result: + del result["__enabled"] + data["plugins"][name] = result + + octoprint.plugin.call_plugin(octoprint.plugin.SettingsPlugin, + "on_settings_load", + callback=process_plugin_result) return jsonify(data) @@ -218,10 +220,10 @@ def setSettings(): enabled = cura.get("enabled") s.setBoolean(["cura", "enabled"], enabled) - settings_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SettingsPlugin) - for name, plugin in settings_plugins.items(): - if "plugins" in data and name in data["plugins"]: - plugin.on_settings_save(data["plugins"][name]) + octoprint.plugin.call_plugin(octoprint.plugin.SettingsPlugin, + "on_settings_save", + args=(data["plugins"][name])) + s.save()