Added shutdown trigger and UUID persistence

This commit is contained in:
Gina Häußge 2014-09-07 17:25:20 +02:00
parent ac186f89f1
commit 3f2fdb0b14
3 changed files with 143 additions and 81 deletions

View file

@ -14,6 +14,11 @@ class StartupPlugin(Plugin):
pass
class ShutdownPlugin(Plugin):
def on_shutdown(self):
pass
class AssetPlugin(Plugin):
def get_asset_folder(self):
return None
@ -46,7 +51,7 @@ class BlueprintPlugin(Plugin):
return None
class SettingsPlugin(TemplatePlugin):
class SettingsPlugin(Plugin):
def on_settings_load(self):
return None

View file

@ -13,25 +13,35 @@ import flask
import octoprint.plugin
default_settings = {
"publicHost": None,
"publicPort": None,
"pathPrefix": None,
"httpUsername": None,
"httpPassword": None
"httpPassword": None,
"upnpUuid": None
}
s = octoprint.plugin.plugin_settings("discovery", defaults=default_settings)
def get_uuid():
import uuid
return str(uuid.uuid4())
upnpUuid = s.get(["upnpUuid"])
if upnpUuid is None:
import uuid
upnpUuid = str(uuid.uuid4())
s.set(["upnpUuid"], upnpUuid)
s.save()
return upnpUuid
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
name = s.globalGet(["appearance", "name"])
if name:
return "OctoPrint instance \"{}\"".format(name)
else:
import socket
return "OctoPrint instance on {}".format(socket.gethostname())
blueprint = flask.Blueprint("plugin.discovery", __name__, template_folder=os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates"))
@ -40,23 +50,59 @@ blueprint = flask.Blueprint("plugin.discovery", __name__, template_folder=os.pat
def discovery():
logging.getLogger("octoprint.plugins." + __name__).info("Rendering discovery.xml")
response = flask.make_response(flask.render_template("discovery.jinja2",
friendlyName=INSTANCENAME,
manufacturer="OctoPrint",
friendlyName=get_instance_name(),
manufacturer="The OctoPrint project",
manufacturerUrl="http://www.octoprint.org",
modelDescription="Some funny description",
modelName="Some funny name",
modelDescription="Some funny description", #TODO
modelName="Some funny name", #TODO
uuid=UUID,
presentationUrl=flask.url_for("index", _external=True)))
response.headers['Content-Type'] = 'application/xml'
return response
def interface_addresses(family=None):
import netifaces
if not family:
family = netifaces.AF_INET
class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.types.BlueprintPlugin, octoprint.plugin.SettingsPlugin):
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):
def __init__(self):
self.logger = logging.getLogger("octoprint.plugins." + __name__)
self.octoprint_sd_ref = None
self.http_sd_ref = None
self.host = None
self.port = None
# zeroconf
self._octoprint_sd_ref = None
self._http_sd_ref = None
# upnp/ssdp
self._ssdp_monitor_active = False
self._ssdp_monitor_thread = None
self._ssdp_notify_timeout = 10
self._ssdp_last_notify = 0
##~~ BlueprintPlugin API
@ -76,13 +122,29 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
#~~ StartupPlugin API
def on_startup(self, host, port):
self._bonjour_register(host, port)
self._ssdp_register(port)
public_host = s.get(["publicHost"])
if public_host:
host = public_host
public_port = s.get(["publicPort"])
if public_port:
port = public_port
self.host = host
self.port = port
self._zeroconf_register(host, port)
self._ssdp_register(host, port)
#~~ ShutdownPlugin API
def on_shutdown(self):
self._ssdp_unregister()
#~~ SettingsPlugin API
def on_settings_load(self):
return {
"publicHost": s.get(["publicHost"]),
"publicPort": s.getInt(["publicPort"]),
"pathPrefix": s.get(["pathPrefix"]),
"httpUsername": s.get(["httpUsername"]),
@ -90,6 +152,8 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
}
def on_settings_save(self, data):
if "publicHost" in data and data["publicHost"]:
s.set(["publicHost"], data["publicHost"])
if "publicPort" in data and data["publicPort"]:
s.setInt(["publicPort"], data["publicPort"])
if "pathPrefix" in data and data["pathPrefix"]:
@ -101,33 +165,34 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
#~~ internals
def _bonjour_register(self, host, port):
# ZeroConf
def _zeroconf_register(self, host, port):
import pybonjour
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()))
if s.getInt(["publicPort"]):
port = s.getInt(["publicPort"])
instance_name = get_instance_name()
self.octoprint_sd_ref = pybonjour.DNSServiceRegister(
name=INSTANCENAME,
self._octoprint_sd_ref = pybonjour.DNSServiceRegister(
name=instance_name,
regtype='_octoprint._tcp',
port=port,
txtRecord=pybonjour.TXTRecord(self._create_octoprint_txt_record_dict()),
callBack=register_callback
)
pybonjour.DNSServiceProcessResult(self.octoprint_sd_ref)
pybonjour.DNSServiceProcessResult(self._octoprint_sd_ref)
self.http_sd_ref = pybonjour.DNSServiceRegister(
name=INSTANCENAME,
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)
pybonjour.DNSServiceProcessResult(self._http_sd_ref)
def _create_octoprint_txt_record_dict(self):
entries = self._create_base_txt_record_dict()
@ -165,43 +230,34 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
return entries
# SSDP/UPNP
def _ssdp_register(self, port):
def _ssdp_register(self, host, port):
import threading
self._ssdp_monitor_thread = threading.Thread(target=self._ssdp_monitor, args=[port])
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.daemon = True
self._ssdp_monitor_thread.start()
@classmethod
def interface_addresses(cls, family=None):
import netifaces
if not family:
family = netifaces.AF_INET
def _ssdp_unregister(self):
self._ssdp_monitor_active = False
if self.host and self.port:
for _ in xrange(2):
self._ssdp_notify(self.host, self.port, alive=False)
for interface in netifaces.interfaces():
ifaddresses = netifaces.ifaddresses(interface)
if family in ifaddresses:
for ifaddress in ifaddresses[family]:
yield ifaddress["addr"]
@classmethod
def address_for_client(cls, client):
def _ssdp_notify(self, host, port, alive=True):
import socket
import time
for address in cls.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
if self._ssdp_last_notify + self._ssdp_notify_timeout > time.time():
return
def _ssdp_notify(self, port, alive=True):
import socket
import netifaces
if alive and not self._ssdp_monitor_active:
return
for addr in self.__class__.interface_addresses():
for addr in interface_addresses():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@ -223,15 +279,15 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
except Exception as e:
pass
def _ssdp_monitor(self, port):
self._ssdp_last_notify = time.time()
def _ssdp_monitor(self, host, port, timeout=5):
from BaseHTTPServer import BaseHTTPRequestHandler
from httplib import HTTPResponse
from StringIO import StringIO
import socket
import struct
socket.setdefaulttimeout(5)
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"])
@ -247,15 +303,6 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
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)
@ -263,24 +310,27 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
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.logger.info("Registered {} for SSDP".format(get_instance_name()))
self._ssdp_notify(port, alive=True)
self._ssdp_notify(host, port, alive=True)
try:
while (True):
while (self._ssdp_monitor_active):
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"':
address_for_client = self.__class__.address_for_client(address)
if not address_for_client:
address_for_client = "192.168.1.3"
message = location_message.format(uuid=UUID, location="http://{host}:{port}/plugin/discovery/discovery.xml".format(host=address_for_client, port=port))
interface_address = 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
message = location_message.format(uuid=UUID, location="http://{host}:{port}/plugin/discovery/discovery.xml".format(host=interface_address, port=port))
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))
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:
self._ssdp_notify(port, alive=True)
pass
finally:
self._ssdp_notify(host, port, alive=True)
finally:
try:
sock.close()
@ -290,7 +340,7 @@ class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.typ
__plugin_name__ = "Discovery"
__plugin_version__ = "0.1"
__plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf"
__plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf and uPnP"
__plugin_implementations__ = []
def __plugin_check__():

View file

@ -17,6 +17,7 @@ from watchdog.observers import Observer
import os
import logging
import logging.config
import atexit
SUCCESS = {}
NO_CONTENT = ("", 204)
@ -90,7 +91,7 @@ def get_locale():
@app.route("/")
def index():
settings_plugins = pluginManager.get_implementations(octoprint.plugin.SettingsPlugin)
settings_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin)
settings_plugin_template_vars = dict()
for name, implementation in settings_plugins.items():
settings_plugin_template_vars[name] = implementation.get_template_vars()
@ -305,17 +306,23 @@ class Server():
"on_startup",
args=(self._host, self._port))
# prepare our shutdown function
def on_shutdown():
logger.info("Goodbye!")
observer.stop()
observer.join()
octoprint.plugin.call_plugin(octoprint.plugin.ShutdownPlugin,
"on_shutdown")
atexit.register(on_shutdown)
logger.info("Listening on http://%s:%d" % (self._host, self._port))
try:
IOLoop.instance().start()
except KeyboardInterrupt:
logger.info("Goodbye!")
pass
except:
logger.fatal("Now that is embarrassing... Something really really went wrong here. Please report this including the stacktrace below in OctoPrint's bugtracker. Thanks!")
logger.exception("Stacktrace follows:")
finally:
observer.stop()
observer.join()
def _createSocketConnection(self, session):
global printer, gcodeManager, userManager, eventManager