Added plugin helpers, added ssdp/zeroconf browsing in discovery plugin as first helpers
This commit is contained in:
parent
08e4c05129
commit
56b1705df4
4 changed files with 243 additions and 60 deletions
|
|
@ -24,6 +24,8 @@ class PluginInfo(object):
|
|||
|
||||
attr_implementations = '__plugin_implementations__'
|
||||
|
||||
attr_helpers = '__plugin_helpers__'
|
||||
|
||||
attr_check = '__plugin_check__'
|
||||
|
||||
def __init__(self, key, location, instance, version=None):
|
||||
|
|
@ -72,6 +74,10 @@ class PluginInfo(object):
|
|||
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)
|
||||
|
|
@ -161,32 +167,32 @@ class PluginManager(object):
|
|||
|
||||
plugin = self._load_plugin_from_module(key, module_name=module_name, version=version)
|
||||
if plugin:
|
||||
plugins[id] = plugin
|
||||
plugins[key] = plugin
|
||||
|
||||
return plugins
|
||||
|
||||
def _load_plugin_from_module(self, id, folder=None, module_name=None, version=None):
|
||||
def _load_plugin_from_module(self, key, folder=None, module_name=None, version=None):
|
||||
# TODO error handling
|
||||
if folder:
|
||||
module = imp.find_module(id, [folder])
|
||||
module = imp.find_module(key, [folder])
|
||||
elif module_name:
|
||||
module = imp.find_module(module_name)
|
||||
else:
|
||||
return None
|
||||
|
||||
plugin = self._load_plugin(id, *module, version=version)
|
||||
plugin = self._load_plugin(key, *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, version=version)
|
||||
def _load_plugin(self, key, f, filename, description, version=None):
|
||||
instance = imp.load_module(key, f, filename, description)
|
||||
return PluginInfo(key, filename, instance, version=version)
|
||||
|
||||
def _is_plugin_disabled(self, id):
|
||||
return id in self.plugin_disabled_list or id.endswith('disabled')
|
||||
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)))
|
||||
|
|
@ -228,6 +234,17 @@ class PluginManager(object):
|
|||
return set()
|
||||
return {impl[0]: impl[1] for impl in result}
|
||||
|
||||
def get_helpers(self, name, helpers=None):
|
||||
if not name in self.plugins:
|
||||
return None
|
||||
plugin = self.plugins[name]
|
||||
|
||||
all_helpers = plugin.helpers
|
||||
if helpers:
|
||||
return dict((k, v) for (k, v) in all_helpers.items() if k in helpers)
|
||||
else:
|
||||
return all_helpers
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
|
|||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
import flask
|
||||
|
|
@ -13,13 +14,19 @@ import flask
|
|||
import octoprint.plugin
|
||||
import octoprint.util
|
||||
|
||||
try:
|
||||
import pybonjour
|
||||
except:
|
||||
pybonjour = False
|
||||
|
||||
default_settings = {
|
||||
"publicHost": None,
|
||||
"publicPort": None,
|
||||
"pathPrefix": None,
|
||||
"httpUsername": None,
|
||||
"httpPassword": None,
|
||||
"upnpUuid": None
|
||||
"upnpUuid": None,
|
||||
"zeroConf": []
|
||||
}
|
||||
s = octoprint.plugin.plugin_settings("discovery", defaults=default_settings)
|
||||
|
||||
|
|
@ -54,8 +61,7 @@ def discovery():
|
|||
friendlyName=get_instance_name(),
|
||||
manufacturer="The OctoPrint project",
|
||||
manufacturerUrl="http://www.octoprint.org",
|
||||
modelDescription="Some funny description", #TODO
|
||||
modelName="Some funny name", #TODO
|
||||
modelName="OctoPrint",
|
||||
uuid=UUID,
|
||||
presentationUrl=flask.url_for("index", _external=True)))
|
||||
response.headers['Content-Type'] = 'application/xml'
|
||||
|
|
@ -72,8 +78,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
self.port = None
|
||||
|
||||
# zeroconf
|
||||
self._octoprint_sd_ref = None
|
||||
self._http_sd_ref = None
|
||||
self._sd_refs = {}
|
||||
|
||||
# upnp/ssdp
|
||||
self._ssdp_monitor_active = False
|
||||
|
|
@ -109,8 +114,20 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self._zeroconf_register(host, port)
|
||||
self._ssdp_register(host, port)
|
||||
# Zeroconf
|
||||
self.zeroconf_register("_http._tcp", get_instance_name(), txt_record=self._create_base_txt_record_dict())
|
||||
self.zeroconf_register("_octoprint._tcp", get_instance_name(), txt_record=self._create_octoprint_txt_record_dict())
|
||||
for zeroconf in s.get(["zeroConf"]):
|
||||
if "service" in zeroconf:
|
||||
self.zeroconf_register(
|
||||
zeroconf["service"],
|
||||
zeroconf["name"] if "name" in zeroconf else get_instance_name(),
|
||||
port=zeroconf["port"] if "port" in zeroconf else None,
|
||||
txt_record=zeroconf["txtRecord"] if "txtRecord" in zeroconf else None
|
||||
)
|
||||
|
||||
# SSDP
|
||||
self._ssdp_register()
|
||||
|
||||
#~~ ShutdownPlugin API
|
||||
|
||||
|
|
@ -144,32 +161,96 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
|
||||
# ZeroConf
|
||||
|
||||
def _zeroconf_register(self, host, port):
|
||||
import pybonjour
|
||||
def zeroconf_register(self, service_type, name, port=None, txt_record=None):
|
||||
if not pybonjour:
|
||||
return
|
||||
|
||||
def register_callback(sd_ref, flags, error_code, name, reg_type, domain):
|
||||
if error_code == pybonjour.kDNSServiceErr_NoError:
|
||||
self.logger.info("Registered {name} for {reg_type} with domain {domain}".format(**locals()))
|
||||
|
||||
instance_name = get_instance_name()
|
||||
|
||||
self._octoprint_sd_ref = pybonjour.DNSServiceRegister(
|
||||
name=instance_name,
|
||||
regtype='_octoprint._tcp',
|
||||
port=port,
|
||||
txtRecord=pybonjour.TXTRecord(self._create_octoprint_txt_record_dict()),
|
||||
params = dict(
|
||||
name=name,
|
||||
regtype=service_type,
|
||||
port=port if port else self.port,
|
||||
callBack=register_callback
|
||||
)
|
||||
pybonjour.DNSServiceProcessResult(self._octoprint_sd_ref)
|
||||
if txt_record:
|
||||
params["txtRecord"] = pybonjour.TXTRecord(txt_record)
|
||||
|
||||
self._http_sd_ref = pybonjour.DNSServiceRegister(
|
||||
name=instance_name,
|
||||
regtype='_http._tcp',
|
||||
port=port,
|
||||
txtRecord=pybonjour.TXTRecord(self._create_base_txt_record_dict()),
|
||||
callBack=register_callback
|
||||
)
|
||||
pybonjour.DNSServiceProcessResult(self._http_sd_ref)
|
||||
self._sd_refs[service_type] = pybonjour.DNSServiceRegister(**params)
|
||||
pybonjour.DNSServiceProcessResult(self._sd_refs[service_type])
|
||||
|
||||
def zeroconf_browse(self, service_type, block=False, callback=None, timeout=5):
|
||||
if not pybonjour:
|
||||
return None
|
||||
|
||||
import threading
|
||||
import time
|
||||
import select
|
||||
|
||||
result = []
|
||||
result_available = threading.Event()
|
||||
result_available.clear()
|
||||
|
||||
def resolve_callback(sd_ref, flags, interface_index, error_code, fullname, hosttarget, port, txt_record):
|
||||
if error_code == pybonjour.kDNSServiceErr_NoError:
|
||||
txt_record_dict = None
|
||||
if txt_record:
|
||||
record = pybonjour.TXTRecord.parse(txt_record)
|
||||
txt_record_dict = dict()
|
||||
for key, value in record:
|
||||
txt_record_dict[key] = value
|
||||
|
||||
name = fullname[:fullname.find(service_type) - 1].replace("\\032", " ")
|
||||
host = hosttarget[:-1]
|
||||
|
||||
self.logger.debug("Resolved a result for Zeroconf resolution of {service_type}: {name} @ {host}".format(service_type=service_type, name=name, host=host))
|
||||
result.append(dict(
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
txt_record=txt_record_dict
|
||||
))
|
||||
|
||||
def browse_callback(sd_ref, flags, interface_index, error_code, service_name, regtype, reply_domain):
|
||||
if error_code != pybonjour.kDNSServiceErr_NoError:
|
||||
return
|
||||
|
||||
if not (flags & pybonjour.kDNSServiceFlagsAdd):
|
||||
return
|
||||
|
||||
self.logger.debug("Got a browsing result for Zeroconf resolution of {service_type}, resolving...".format(service_type=service_type))
|
||||
resolve_ref = pybonjour.DNSServiceResolve(0, interface_index, service_name, regtype, reply_domain, resolve_callback)
|
||||
try:
|
||||
pybonjour.DNSServiceProcessResult(resolve_ref)
|
||||
finally:
|
||||
resolve_ref.close()
|
||||
|
||||
self.logger.debug("Browsing Zeroconf for {service_type}".format(service_type=service_type))
|
||||
|
||||
def browse():
|
||||
sd_ref = pybonjour.DNSServiceBrowse(regtype=service_type, callBack=browse_callback)
|
||||
start = time.time()
|
||||
try:
|
||||
while start + timeout > time.time():
|
||||
ready = select.select([sd_ref], [], [], timeout)
|
||||
if sd_ref in ready[0]:
|
||||
pybonjour.DNSServiceProcessResult(sd_ref)
|
||||
finally:
|
||||
sd_ref.close()
|
||||
|
||||
if callback:
|
||||
callback(result)
|
||||
result_available.set()
|
||||
|
||||
browse_thread = threading.Thread(target=browse)
|
||||
browse_thread.daemon = True
|
||||
browse_thread.start()
|
||||
|
||||
if block:
|
||||
result_available.wait()
|
||||
return result
|
||||
|
||||
def _create_octoprint_txt_record_dict(self):
|
||||
entries = self._create_base_txt_record_dict()
|
||||
|
|
@ -209,12 +290,80 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
|
||||
# SSDP/UPNP
|
||||
|
||||
def _ssdp_register(self, host, port):
|
||||
def ssdp_browse(self, query, block=False, callback=None, timeout=1, retries=5):
|
||||
import threading
|
||||
|
||||
import httplib
|
||||
import io
|
||||
class Response(httplib.HTTPResponse):
|
||||
def __init__(self, response_text):
|
||||
self.fp = io.BytesIO(response_text)
|
||||
self.debuglevel = 0
|
||||
self.strict = 0
|
||||
self.msg = None
|
||||
self._method = None
|
||||
self.begin()
|
||||
|
||||
result = []
|
||||
result_available = threading.Event()
|
||||
result_available.clear()
|
||||
|
||||
def browse():
|
||||
import socket
|
||||
|
||||
socket.setdefaulttimeout(timeout)
|
||||
|
||||
search_message = "".join([
|
||||
"M-SEARCH * HTTP/1.1\r\n",
|
||||
"ST: {query}\r\n",
|
||||
"MX: 3\r\n",
|
||||
"MAN: \"ssdp:discovery\"\r\n",
|
||||
"HOST: 239.255.255.250:1900\r\n\r\n"
|
||||
])
|
||||
|
||||
for _ in xrange(retries):
|
||||
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)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
||||
sock.bind((addr, 0))
|
||||
|
||||
message = search_message.format(query=query)
|
||||
for _ in xrange(2):
|
||||
sock.sendto(message, ("239.255.255.250", 1900))
|
||||
|
||||
try:
|
||||
data = sock.recv(1024)
|
||||
except socket.timeout:
|
||||
pass
|
||||
else:
|
||||
response = Response(data)
|
||||
|
||||
result.append(response.getheader("Location"))
|
||||
except Exception as e:
|
||||
self.logger.exception("oops with {addr}".format(addr=addr))
|
||||
pass
|
||||
|
||||
if callback:
|
||||
callback(result)
|
||||
result_available.set()
|
||||
|
||||
browse_thread = threading.Thread(target=browse)
|
||||
browse_thread.daemon = True
|
||||
browse_thread.start()
|
||||
|
||||
if block:
|
||||
result_available.wait()
|
||||
|
||||
return result
|
||||
|
||||
def _ssdp_register(self):
|
||||
import threading
|
||||
|
||||
self._ssdp_monitor_active = True
|
||||
|
||||
self._ssdp_monitor_thread = threading.Thread(target=self._ssdp_monitor, args=[host, port], kwargs=dict(timeout=self._ssdp_notify_timeout))
|
||||
self._ssdp_monitor_thread = threading.Thread(target=self._ssdp_monitor, kwargs=dict(timeout=self._ssdp_notify_timeout))
|
||||
self._ssdp_monitor_thread.daemon = True
|
||||
self._ssdp_monitor_thread.start()
|
||||
|
||||
|
|
@ -222,9 +371,9 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
self._ssdp_monitor_active = False
|
||||
if self.host and self.port:
|
||||
for _ in xrange(2):
|
||||
self._ssdp_notify(self.host, self.port, alive=False)
|
||||
self._ssdp_notify(alive=False)
|
||||
|
||||
def _ssdp_notify(self, host, port, alive=True):
|
||||
def _ssdp_notify(self, alive=True):
|
||||
import socket
|
||||
import time
|
||||
|
||||
|
|
@ -241,24 +390,28 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
||||
sock.bind((addr, 0))
|
||||
|
||||
location = "http://{addr}:{port}/plugin/discovery/discovery.xml".format(addr=addr, port=port)
|
||||
location = "http://{addr}:{port}/plugin/discovery/discovery.xml".format(addr=addr, port=self.port)
|
||||
|
||||
self.logger.debug("Sending NOTIFY {}".format("alive" if alive else "byebye"))
|
||||
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"])
|
||||
self.logger.debug("Sending NOTIFY {} via {}".format("alive" if alive else "byebye", addr))
|
||||
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
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
self._ssdp_last_notify = time.time()
|
||||
|
||||
def _ssdp_monitor(self, host, port, timeout=5):
|
||||
def _ssdp_monitor(self, timeout=5):
|
||||
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
from StringIO import StringIO
|
||||
|
|
@ -266,7 +419,13 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
|
||||
socket.setdefaulttimeout(timeout)
|
||||
|
||||
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"])
|
||||
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):
|
||||
|
||||
|
|
@ -289,7 +448,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
|
||||
self.logger.info("Registered {} for SSDP".format(get_instance_name()))
|
||||
|
||||
self._ssdp_notify(host, port, alive=True)
|
||||
self._ssdp_notify(alive=True)
|
||||
|
||||
try:
|
||||
while (self._ssdp_monitor_active):
|
||||
|
|
@ -301,13 +460,13 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
if not interface_address:
|
||||
self.logger.warn("Can't determine address to user for client {}, not sending a M-SEARCH reply".format(address))
|
||||
continue
|
||||
message = location_message.format(uuid=UUID, location="http://{host}:{port}/plugin/discovery/discovery.xml".format(host=interface_address, port=port))
|
||||
message = location_message.format(uuid=UUID, location="http://{host}:{port}/plugin/discovery/discovery.xml".format(host=interface_address, port=self.port))
|
||||
sock.sendto(message, address)
|
||||
self.logger.debug("Sent M-SEARCH reply for {path} and {st} to {address!r}".format(path=request.path, st=request.headers["ST"], address=address))
|
||||
except socket.timeout:
|
||||
pass
|
||||
finally:
|
||||
self._ssdp_notify(host, port, alive=True)
|
||||
self._ssdp_notify(alive=True)
|
||||
finally:
|
||||
try:
|
||||
sock.close()
|
||||
|
|
@ -318,17 +477,23 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin,
|
|||
__plugin_name__ = "Discovery"
|
||||
__plugin_version__ = "0.1"
|
||||
__plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf and uPnP"
|
||||
__plugin_implementations__ = []
|
||||
|
||||
def __plugin_check__():
|
||||
try:
|
||||
import pybonjour
|
||||
except:
|
||||
if not pybonjour:
|
||||
# no pybonjour available, we can't continue
|
||||
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Discovery Plugin won't be available. Please manually install pybonjour and restart OctoPrint")
|
||||
return False
|
||||
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available")
|
||||
|
||||
discovery_plugin = DiscoveryPlugin()
|
||||
|
||||
global __plugin_implementations__
|
||||
__plugin_implementations__ = [DiscoveryPlugin(),]
|
||||
__plugin_implementations__ = [discovery_plugin]
|
||||
|
||||
global __plugin_helpers__
|
||||
__plugin_helpers__ = dict(
|
||||
ssdp_browse=discovery_plugin.ssdp_browse
|
||||
)
|
||||
if pybonjour:
|
||||
__plugin_helpers__["zeroconf_browse"] = discovery_plugin.zeroconf_browse
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ def afterApiRequests(resp):
|
|||
def pluginData(name):
|
||||
api_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.SimpleApiPlugin)
|
||||
if not name in api_plugins:
|
||||
return make_response(404)
|
||||
make_response(404)
|
||||
|
||||
api_plugin = api_plugins[name]
|
||||
response = api_plugin.on_api_get(request)
|
||||
|
|
|
|||
|
|
@ -262,7 +262,8 @@ def interface_addresses(family=None):
|
|||
ifaddresses = netifaces.ifaddresses(interface)
|
||||
if family in ifaddresses:
|
||||
for ifaddress in ifaddresses[family]:
|
||||
yield ifaddress["addr"]
|
||||
if not ifaddress["addr"].startswith("169.254."):
|
||||
yield ifaddress["addr"]
|
||||
|
||||
def address_for_client(host, port):
|
||||
import socket
|
||||
|
|
|
|||
Loading…
Reference in a new issue