Merge pull request #573 from adamcik/feature/avahi

Cleaned up version of #558 - Add avahi publishing of MPD and HTTP server endpoints.
This commit is contained in:
Stein Magnus Jodal 2013-11-11 13:35:43 -08:00
commit d723db8f41
7 changed files with 131 additions and 6 deletions

View File

@ -21,6 +21,7 @@ class Extension(ext.Extension):
schema['hostname'] = config.Hostname() schema['hostname'] = config.Hostname()
schema['port'] = config.Port() schema['port'] = config.Port()
schema['static_dir'] = config.Path(optional=True) schema['static_dir'] = config.Path(optional=True)
schema['zeroconf'] = config.String(optional=True)
return schema return schema
def validate_environment(self): def validate_environment(self):

View File

@ -11,6 +11,7 @@ from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
from mopidy import models from mopidy import models
from mopidy.core import CoreListener from mopidy.core import CoreListener
from mopidy.utils import zeroconf
from . import ws from . import ws
@ -22,6 +23,12 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
super(HttpFrontend, self).__init__() super(HttpFrontend, self).__init__()
self.config = config self.config = config
self.core = core self.core = core
self.hostname = config['http']['hostname']
self.port = config['http']['port']
self.zeroconf_name = config['http']['zeroconf']
self.zeroconf_service = None
self._setup_server() self._setup_server()
self._setup_websocket_plugin() self._setup_websocket_plugin()
app = self._create_app() app = self._create_app()
@ -30,8 +37,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
def _setup_server(self): def _setup_server(self):
cherrypy.config.update({ cherrypy.config.update({
'engine.autoreload_on': False, 'engine.autoreload_on': False,
'server.socket_host': self.config['http']['hostname'], 'server.socket_host': self.hostname,
'server.socket_port': self.config['http']['port'], 'server.socket_port': self.port,
}) })
def _setup_websocket_plugin(self): def _setup_websocket_plugin(self):
@ -88,7 +95,21 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
cherrypy.engine.start() cherrypy.engine.start()
logger.info('HTTP server running at %s', cherrypy.server.base()) logger.info('HTTP server running at %s', cherrypy.server.base())
if self.zeroconf_name:
self.zeroconf_service = zeroconf.Zeroconf(
stype='_http._tcp', name=self.zeroconf_name,
host=self.hostname, port=self.port)
if self.zeroconf_service.publish():
logger.info('Registered HTTP with Zeroconf as "%s"',
self.zeroconf_service.name)
else:
logger.warning('Registering HTTP with Zeroconf failed.')
def on_stop(self): def on_stop(self):
if self.zeroconf_service:
self.zeroconf_service.unpublish()
logger.debug('Stopping HTTP server') logger.debug('Stopping HTTP server')
cherrypy.engine.exit() cherrypy.engine.exit()
logger.info('Stopped HTTP server') logger.info('Stopped HTTP server')

View File

@ -3,6 +3,7 @@ enabled = true
hostname = 127.0.0.1 hostname = 127.0.0.1
port = 6680 port = 6680
static_dir = static_dir =
zeroconf = Mopidy HTTP server on $hostname
[loglevels] [loglevels]
cherrypy = warning cherrypy = warning

View File

@ -23,6 +23,7 @@ class Extension(ext.Extension):
schema['password'] = config.Secret(optional=True) schema['password'] = config.Secret(optional=True)
schema['max_connections'] = config.Integer(minimum=1) schema['max_connections'] = config.Integer(minimum=1)
schema['connection_timeout'] = config.Integer(minimum=1) schema['connection_timeout'] = config.Integer(minimum=1)
schema['zeroconf'] = config.String(optional=True)
return schema return schema
def validate_environment(self): def validate_environment(self):

View File

@ -7,7 +7,7 @@ import pykka
from mopidy.core import CoreListener from mopidy.core import CoreListener
from mopidy.frontends.mpd import session from mopidy.frontends.mpd import session
from mopidy.utils import encoding, network, process from mopidy.utils import encoding, network, process, zeroconf
logger = logging.getLogger('mopidy.frontends.mpd') logger = logging.getLogger('mopidy.frontends.mpd')
@ -15,12 +15,16 @@ logger = logging.getLogger('mopidy.frontends.mpd')
class MpdFrontend(pykka.ThreadingActor, CoreListener): class MpdFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core): def __init__(self, config, core):
super(MpdFrontend, self).__init__() super(MpdFrontend, self).__init__()
hostname = network.format_hostname(config['mpd']['hostname']) hostname = network.format_hostname(config['mpd']['hostname'])
port = config['mpd']['port'] self.hostname = hostname
self.port = config['mpd']['port']
self.zeroconf_name = config['mpd']['zeroconf']
self.zeroconf_service = None
try: try:
network.Server( network.Server(
hostname, port, self.hostname, self.port,
protocol=session.MpdSession, protocol=session.MpdSession,
protocol_kwargs={ protocol_kwargs={
'config': config, 'config': config,
@ -34,9 +38,24 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
encoding.locale_decode(error)) encoding.locale_decode(error))
sys.exit(1) sys.exit(1)
logger.info('MPD server running at [%s]:%s', hostname, port) logger.info('MPD server running at [%s]:%s', self.hostname, self.port)
def on_start(self):
if self.zeroconf_name:
self.zeroconf_service = zeroconf.Zeroconf(
stype='_mpd._tcp', name=self.zeroconf_name,
host=self.hostname, port=self.port)
if self.zeroconf_service.publish():
logger.info('Registered MPD with Zeroconf as "%s"',
self.zeroconf_service.name)
else:
logger.warning('Registering MPD with Zeroconf failed.')
def on_stop(self): def on_stop(self):
if self.zeroconf_service:
self.zeroconf_service.unpublish()
process.stop_actors_by_class(session.MpdSession) process.stop_actors_by_class(session.MpdSession)
def send_idle(self, subsystem): def send_idle(self, subsystem):

View File

@ -5,3 +5,4 @@ port = 6600
password = password =
max_connections = 20 max_connections = 20
connection_timeout = 60 connection_timeout = 60
zeroconf = Mopidy MPD server on $hostname

81
mopidy/utils/zeroconf.py Normal file
View File

@ -0,0 +1,81 @@
from __future__ import unicode_literals
import logging
import re
import socket
import string
logger = logging.getLogger('mopidy.utils.zerconf')
try:
import dbus
except ImportError:
dbus = None
_AVAHI_IF_UNSPEC = -1
_AVAHI_PROTO_UNSPEC = -1
_AVAHI_PUBLISHFLAGS_NONE = 0
def _filter_loopback_and_meta_addresses(host):
# TODO: see if we can find a cleaner way of handling this.
if re.search(r'(?<![.\d])(127|0)[.]', host):
return ''
return host
def _convert_text_to_dbus_bytes(text):
return [dbus.Byte(ord(c)) for c in text]
class Zeroconf(object):
"""Publish a network service with Zeroconf using Avahi."""
def __init__(self, name, port, stype=None, domain=None,
host=None, text=None):
self.group = None
self.stype = stype or '_http._tcp'
self.domain = domain or ''
self.port = port
self.text = text or []
self.host = _filter_loopback_and_meta_addresses(host or '')
template = string.Template(name)
self.name = template.safe_substitute(
hostname=self.host or socket.getfqdn(), port=self.port)
def publish(self):
if not dbus:
logger.debug('Zeroconf publish failed: dbus not installed.')
return False
try:
bus = dbus.SystemBus()
except dbus.exceptions.DBusException as e:
logger.debug('Zeroconf publish failed: %s', e)
return False
if not bus.name_has_owner('org.freedesktop.Avahi'):
logger.debug('Zeroconf publish failed: Avahi service not running.')
return False
server = dbus.Interface(bus.get_object('org.freedesktop.Avahi', '/'),
'org.freedesktop.Avahi.Server')
self.group = dbus.Interface(
bus.get_object('org.freedesktop.Avahi', server.EntryGroupNew()),
'org.freedesktop.Avahi.EntryGroup')
text = [_convert_text_to_dbus_bytes(t) for t in self.text]
self.group.AddService(_AVAHI_IF_UNSPEC, _AVAHI_PROTO_UNSPEC,
dbus.UInt32(_AVAHI_PUBLISHFLAGS_NONE),
self.name, self.stype, self.domain, self.host,
dbus.UInt16(self.port), text)
self.group.Commit()
return True
def unpublish(self):
if self.group:
self.group.Reset()
self.group = None