# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php
# Copyright 2006,2007,2008 Frank Scholz <coherence@beebits.net>
import copy
import logging
import os
import socket
import traceback
from twisted.internet import defer
from twisted.internet import reactor
from twisted.internet.tcp import CannotListenError
from twisted.web import resource, static
from coherence import __version__
from coherence import log
from coherence.extern import louie
from coherence.upnp.core.device import Device, RootDevice
from coherence.upnp.core.msearch import MSearch
from coherence.upnp.core.ssdp import SSDPServer
from coherence.upnp.core.utils import Site
from coherence.upnp.core.utils import get_ip_address, get_host_address
from coherence.upnp.devices.control_point import ControlPoint
from coherence.upnp.devices.media_renderer import MediaRenderer
from coherence.upnp.devices.media_server import MediaServer
__import_devices__ = ControlPoint, MediaServer, MediaRenderer
try:
import pkg_resources
except ImportError:
pkg_resources = None
[docs]class SimpleRoot(resource.Resource, log.LogAble):
addSlash = True
logCategory = 'coherence'
def __init__(self, coherence):
resource.Resource.__init__(self)
log.LogAble.__init__(self)
self.coherence = coherence
[docs] def getChild(self, name, request):
self.debug('SimpleRoot getChild %s, %s', name, request)
if isinstance(name, bytes):
name = name.decode('utf-8')
if name == 'oob':
""" we have an out-of-band request """
return static.File(
self.coherence.dbus.pinboard[request.args['key'][0]])
if name in ['', None, '\'']:
return self
if name.endswith('\''):
self.warning('\t modified wrong name from {} to {}'.format(
name, name[:-1]))
name = name[:-1]
# at this stage, name should be a device UUID
try:
return self.coherence.children[name]
except KeyError:
self.warning("Cannot find device for requested name: %r", name)
request.setResponseCode(404)
return \
static.Data(
b'<html><p>No device for requested UUID: %s</p></html>' %
name.encode('ascii'), 'text/html')
[docs] def listchilds(self, uri):
if isinstance(uri, bytes):
uri = uri.decode('utf-8')
self.info('listchilds %s', uri)
if uri[-1] != '/':
uri += '/'
cl = []
for child in self.coherence.children:
device = self.coherence.get_device_with_id(child)
if device is not None:
cl.append('<li><a href=%s%s>%s:%s %s</a></li>' % (
uri, child, device.get_friendly_device_type(),
device.get_device_type_version(),
device.get_friendly_name()))
for child in self.children:
cl.append('<li><a href=%s%s>%s</a></li>' % (uri, child, child))
return "".join(cl)
[docs] def render(self, request):
result = """<html>
<head><title>Coherence</title></head>
<body><a href="http://coherence.beebits.net">Coherence</a>
- a Python DLNA/UPnP framework for the Digital Living
<p>Hosting:<ul>%r</ul></p>
</body>
</html>""" % self.listchilds(request.uri.encode('utf-8'))
return result
[docs]class WebServer(log.LogAble):
logCategory = 'webserver'
def __init__(self, ui, port, coherence):
log.LogAble.__init__(self)
self.site = Site(SimpleRoot(coherence))
self.port = reactor.listenTCP(port, self.site)
coherence.web_server_port = self.port.getHost().port
self.warning("WebServer on port %r ready", coherence.web_server_port)
[docs]class Plugins(log.LogAble):
logCategory = 'plugins'
__instance = None # Singleton
__initialized = False
_valids = ("coherence.plugins.backend.media_server",
"coherence.plugins.backend.media_renderer",
"coherence.plugins.backend.binary_light",
"coherence.plugins.backend.dimmable_light")
_plugins = {}
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super(Plugins, cls).__new__(cls)
cls.__instance.__initialized = False
cls.__instance.__cls = cls
return cls.__instance
def __init__(self, ids=_valids):
# initialize only once
if self.__initialized:
return
self.__initialized = True
log.LogAble.__init__(self)
if not isinstance(ids, (list, tuple)):
ids = (ids,)
if pkg_resources:
for group in ids:
for entrypoint in pkg_resources.iter_entry_points(group):
# set a placeholder for lazy loading
self._plugins[entrypoint.name] = entrypoint
else:
self.info("no pkg_resources, fallback to simple plugin handling")
if len(self._plugins) == 0:
self._collect_from_module()
def __repr__(self):
return str(self._plugins)
def __getitem__(self, key):
plugin = self._plugins.__getitem__(key)
if pkg_resources and isinstance(plugin, pkg_resources.EntryPoint):
try:
plugin = plugin.load(require=False)
except (ImportError, AttributeError,
pkg_resources.ResolutionError) as msg:
self.warning(
"Can't load plugin %s (%s), maybe missing dependencies...",
plugin.name, msg)
self.info(traceback.format_exc())
del self._plugins[key]
raise KeyError
else:
self._plugins[key] = plugin
return plugin
[docs] def get(self, key, default=None):
try:
return self.__getitem__(key)
except KeyError:
return default
def __setitem__(self, key, value):
self._plugins.__setitem__(key, value)
[docs] def set(self, key, value):
return self.__setitem__(key, value)
[docs] def keys(self):
return list(self._plugins.keys())
[docs] def _collect_from_module(self):
from coherence.extern.simple_plugin import Reception
reception = Reception(
os.path.join(os.path.dirname(__file__), 'backends'),
log=self.warning)
self.info(reception.guestlist())
for cls in reception.guestlist():
self._plugins[cls.__name__.split('.')[-1]] = cls
[docs]class Coherence(log.LogAble):
__instance = None # Singleton
__initialized = False
__incarnations = 0
__cls = None
logCategory = 'coherence'
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super(Coherence, cls).__new__(cls)
cls.__instance.__initialized = False
cls.__instance.__incarnations = 0
cls.__instance.__cls = cls
cls.__instance.config = kwargs.get('config', {})
cls.__instance.__incarnations += 1
return cls.__instance
def __init__(self, config=None):
# initialize only once
if self.__initialized:
return
self.__initialized = True
# supers
log.LogAble.__init__(self)
self.config = config or {}
self.devices = []
self.children = {}
self._callbacks = {}
self.active_backends = {}
self.available_plugins = None
self.external_address = None
self.urlbase = None
self.web_server_port = int(config.get('serverport', 0))
""" Services """
self.ctrl = None
self.dbus = None
self.json = None
self.msearch = None
self.ssdp_server = None
self.transcoder_manager = None
self.web_server = None
""" initializes logsystem
a COHEN_DEBUG environment variable overwrites
all level settings here
"""
try:
logmode = config.get('logging').get('level', 'warning')
except (KeyError, AttributeError):
logmode = config.get('logmode', 'warning')
try:
subsystems = config.get('logging')['subsystem']
if isinstance(subsystems, dict):
subsystems = [subsystems]
for subsystem in subsystems:
try:
if subsystem['active'] == 'no':
continue
except (KeyError, TypeError):
pass
self.info("setting log-level for subsystem %s to %s",
subsystem['name'], subsystem['level'])
logging.getLogger(subsystem['name'].lower()).setLevel(
subsystem['level'].upper())
except (KeyError, TypeError):
subsystem_log = config.get('subsystem_log', {})
for subsystem, level in list(subsystem_log.items()):
logging.getLogger(subsystem.lower()).setLevel(level.upper())
try:
logfile = config.get('logging').get('logfile', None)
if logfile is not None:
logfile = str(logfile)
except (KeyError, AttributeError, TypeError):
logfile = config.get('logfile', None)
log.init(logfile, logmode.upper())
self.warning("Coherence UPnP framework version %s starting...",
__version__)
network_if = config.get('interface')
if network_if:
self.hostname = get_ip_address('%s' % network_if)
else:
try:
self.hostname = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
self.warning("hostname can't be resolved, "
"maybe a system misconfiguration?")
self.hostname = '127.0.0.1'
if self.hostname.startswith('127.'):
""" use interface detection via routing table as last resort """
def catch_result(hostname):
self.hostname = hostname
self.setup_part2()
d = defer.maybeDeferred(get_host_address)
d.addCallback(catch_result)
else:
self.setup_part2()
[docs] def clear(self):
""" we do need this to survive multiple calls
to Coherence during trial tests
"""
self.__cls.__instance = None
[docs] def setup_part2(self):
self.info('running on host: %s', self.hostname)
if self.hostname.startswith('127.'):
self.warning('detection of own ip failed, using %s as own address,'
' functionality will be limited', self.hostname)
unittest = self.config.get('unittest', 'no')
unittest = False if unittest == 'no' else True
""" SSDP Server Initialization
"""
try:
# TODO: add ip/interface bind
self.ssdp_server = SSDPServer(test=unittest)
except CannotListenError as err:
self.error("Error starting the SSDP-server: %s", err)
self.debug("Error starting the SSDP-server", exc_info=True)
reactor.stop()
return
louie.connect(self.create_device, 'Coherence.UPnP.SSDP.new_device',
louie.Any)
louie.connect(self.remove_device, 'Coherence.UPnP.SSDP.removed_device',
louie.Any)
louie.connect(self.add_device,
'Coherence.UPnP.RootDevice.detection_completed',
louie.Any)
# louie.connect(self.receiver,
# 'Coherence.UPnP.Service.detection_completed',
# louie.Any)
self.ssdp_server.subscribe("new_device", self.add_device)
self.ssdp_server.subscribe("removed_device", self.remove_device)
self.msearch = MSearch(self.ssdp_server, test=unittest)
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown,
force=True)
""" Web Server Initialization
"""
try:
# TODO: add ip/interface bind
self.web_server = WebServer(self.config.get('web-ui', None),
self.web_server_port, self)
except CannotListenError:
self.warning('port %r already in use, aborting!',
self.web_server_port)
reactor.stop()
return
self.urlbase = 'http://%s:%d/' % (self.hostname, self.web_server_port)
# self.renew_service_subscription_loop = \
# task.LoopingCall(self.check_devices)
# self.renew_service_subscription_loop.start(20.0, now=False)
try:
plugins = self.config['plugin']
if isinstance(plugins, dict):
plugins = [plugins]
except Exception:
plugins = None
if plugins is None:
plugins = self.config.get('plugins', None)
if plugins is None:
self.info("No plugin defined!")
else:
if isinstance(plugins, dict):
for plugin, arguments in list(plugins.items()):
try:
if not isinstance(arguments, dict):
arguments = {}
self.add_plugin(plugin, **arguments)
except Exception as msg:
self.warning("Can't enable plugin, %s: %s!", plugin,
msg)
self.info(traceback.format_exc())
else:
for plugin in plugins:
try:
if plugin['active'] == 'no':
continue
except (KeyError, TypeError):
pass
try:
backend = plugin['backend']
arguments = copy.copy(plugin)
del arguments['backend']
backend = self.add_plugin(backend, **arguments)
if self.writeable_config():
if 'uuid' not in plugin:
plugin['uuid'] = str(backend.uuid)[5:]
self.config.save()
except Exception as msg:
self.warning("Can't enable plugin, %s: %s!", plugin,
msg)
self.info(traceback.format_exc())
self.external_address = ':'.join(
(self.hostname, str(self.web_server_port)))
""" Control Point Initialization
"""
if self.config.get('controlpoint', 'no') == 'yes' or self.config.get(
'json', 'no') == 'yes':
self.ctrl = ControlPoint(self)
""" Json Interface Initialization
"""
if self.config.get('json', 'no') == 'yes':
from coherence.json_service import JsonInterface
self.json = JsonInterface(self.ctrl)
""" Transcoder Initialization
"""
if self.config.get('transcoding', 'no') == 'yes':
from coherence.transcoder import TranscoderManager
self.transcoder_manager = TranscoderManager(self)
""" DBus Initialization
"""
if self.config.get('use_dbus', 'no') == 'yes':
try:
from coherence import dbus_service
if self.ctrl is None:
self.ctrl = ControlPoint(self)
self.ctrl.auto_client_append('InternetGatewayDevice')
self.dbus = dbus_service.DBusPontoon(self.ctrl)
except Exception as msg:
self.warning("Unable to activate dbus sub-system: %r", msg)
self.debug(traceback.format_exc())
[docs] def add_plugin(self, plugin, **kwargs):
self.info("adding plugin %r", plugin)
self.available_plugins = Plugins()
# TODO clean up this exception concept
try:
plugin_class = self.available_plugins.get(plugin, None)
if plugin_class is None:
raise KeyError
for device in plugin_class.implements:
try:
device_class = globals().get(device, None)
if device_class is None:
raise KeyError
self.info("Activating %s plugin as %s...", plugin, device)
new_backend = device_class(self, plugin_class, **kwargs)
self.active_backends[str(new_backend.uuid)] = new_backend
return new_backend
except KeyError:
self.warning(
"Can't enable %s plugin, sub-system %s not found!",
plugin, device)
except Exception as e1:
self.exception(
"Can't enable %s plugin for sub-system %s "
"[exception: %r]", plugin, device, e1)
self.debug(traceback.format_exc())
except KeyError:
self.warning("Can't enable %s plugin, not found!", plugin)
except Exception as e2:
self.warning("Can't enable %s plugin, %s!", plugin, e2)
self.debug(traceback.format_exc())
[docs] def remove_plugin(self, plugin):
"""
Removes a backend from Coherence
@:param plugin: is the object return by add_plugin or an UUID string
"""
if isinstance(plugin, str):
try:
plugin = self.active_backends[plugin]
except KeyError:
self.warning("no backend with the uuid %r found", plugin)
return ""
try:
del self.active_backends[str(plugin.uuid)]
self.info("removing plugin %r", plugin)
plugin.unregister()
return plugin.uuid
except KeyError:
self.warning("no backend with the uuid %r found", plugin.uuid)
return ""
[docs] @staticmethod
def writeable_config():
""" do we have a new-style config file """
return False
[docs] def store_plugin_config(self, uuid, items):
""" find the backend with uuid
and store in its the config
the key and value pair(s)
"""
plugins = self.config.get('plugin')
if plugins is None:
self.warning("storing a plugin config option is only possible"
" with the new config file format")
return
if isinstance(plugins, dict):
plugins = [plugins]
uuid = str(uuid)
if uuid.startswith('uuid:'):
uuid = uuid[5:]
for plugin in plugins:
try:
if plugin['uuid'] == uuid:
for k, v in list(items.items()):
plugin[k] = v
self.config.save()
except Exception as e:
self.warning('Coherence.store_plugin_config: %r' % e)
else:
self.info(
"storing plugin config option for %s failed, plugin not found",
uuid)
[docs] def receiver(self, signal, *args, **kwargs):
pass
[docs] def shutdown(self, force=False):
if self.__incarnations > 1 and not force:
self.__incarnations -= 1
return
if self.dbus:
self.dbus.shutdown()
self.dbus = None
for backend in self.active_backends.values():
backend.unregister()
self.active_backends = {}
""" send service unsubscribe messages """
try:
if self.web_server.port is not None:
self.web_server.port.stopListening()
self.web_server.port = None
if hasattr(self.msearch, 'double_discover_loop'):
self.msearch.double_discover_loop.stop()
if hasattr(self.msearch, 'port'):
self.msearch.port.stopListening()
if hasattr(self.ssdp_server, 'resend_notify_loop'):
self.ssdp_server.resend_notify_loop.stop()
if hasattr(self.ssdp_server, 'port'):
self.ssdp_server.port.stopListening()
# self.renew_service_subscription_loop.stop()
except Exception:
pass
dev_l = []
for root_device in self.get_devices():
for device in root_device.get_devices():
dd = device.unsubscribe_service_subscriptions()
dd.addCallback(device.remove)
dev_l.append(dd)
rd = root_device.unsubscribe_service_subscriptions()
rd.addCallback(root_device.remove)
dev_l.append(rd)
def homecleanup(result):
"""anything left over"""
louie.disconnect(self.create_device,
'Coherence.UPnP.SSDP.new_device', louie.Any)
louie.disconnect(self.remove_device,
'Coherence.UPnP.SSDP.removed_device', louie.Any)
louie.disconnect(self.add_device,
'Coherence.UPnP.RootDevice.detection_completed',
louie.Any)
self.ssdp_server.shutdown()
if self.ctrl:
self.ctrl.shutdown()
self.warning('Coherence UPnP framework shutdown')
return result
dl = defer.DeferredList(dev_l)
dl.addCallback(homecleanup)
return dl
[docs] def check_devices(self):
"""
iterate over devices and their embedded ones and renew subscriptions
"""
for root_device in self.get_devices():
root_device.renew_service_subscriptions()
for device in root_device.get_devices():
device.renew_service_subscriptions()
[docs] def subscribe(self, name, callback):
self._callbacks.setdefault(name, []).append(callback)
[docs] def unsubscribe(self, name, callback):
callbacks = self._callbacks.get(name, [])
if callback in callbacks:
callbacks.remove(callback)
self._callbacks[name] = callbacks
[docs] def callback(self, name, *args):
for callback in self._callbacks.get(name, []):
callback(*args)
[docs] def get_device_by_host(self, host):
found = []
for device in self.devices:
if device.get_host() == host:
found.append(device)
return found
[docs] def get_device_with_usn(self, usn):
found = None
for device in self.devices:
if device.get_usn() == usn:
found = device
break
return found
[docs] def get_device_with_id(self, device_id):
# print('get_device_with_id [{}]: {}'.format(
# type(device_id), device_id))
found = None
for device in self.devices:
id = device.get_id()
if device_id[:5] != 'uuid:':
id = id[5:]
if id == device_id:
found = device
break
return found
[docs] def get_devices(self):
# print('get_devices: {}'.format(self.devices))
return self.devices
[docs] def get_local_devices(self):
# print('get_local_devices: {}'.format(
# [d for d in self.devices if d.manifestation == 'local']))
return [d for d in self.devices if d.manifestation == 'local']
[docs] def get_nonlocal_devices(self):
# print('get_nonlocal_devices: {}'.format(
# [d for d in self.devices if d.manifestation == 'remote']))
return [d for d in self.devices if d.manifestation == 'remote']
[docs] def create_device(self, device_type, infos):
self.info("creating %r %r", infos['ST'], infos['USN'])
if infos['ST'] == 'upnp:rootdevice':
self.info('creating upnp:rootdevice {}'.format(infos['USN']))
root = RootDevice(infos)
else:
self.info('creating device/service {}'.format(infos['USN']))
root_id = infos['USN'][:-len(infos['ST']) - 2]
root = self.get_device_with_id(root_id)
# TODO: must check that this is working as expected
device = Device(root, udn=infos['UDN'])
[docs] def add_device(self, device):
self.info('adding device {} {} {}'.format(
device.get_id(), device.get_usn(), device.friendly_device_type))
self.devices.append(device)
[docs] def remove_device(self, device_type, infos):
self.info('removed device {} %s{}'.format(infos['ST'], infos['USN']))
device = self.get_device_with_usn(infos['USN'])
if device:
louie.send('Coherence.UPnP.Device.removed', None, usn=infos['USN'])
self.devices.remove(device)
device.remove()
if infos['ST'] == 'upnp:rootdevice':
louie.send('Coherence.UPnP.RootDevice.removed', None,
usn=infos['USN'])
self.callback("removed_device", infos['ST'], infos['USN'])
[docs] def add_web_resource(self, name, sub):
self.children[name] = sub
[docs] def remove_web_resource(self, name):
try:
del self.children[name]
except KeyError:
""" probably the backend init failed """
pass
[docs] @staticmethod
def connect(receiver, signal=louie.signal.All, sender=louie.sender.Any,
weak=True):
""" wrapper method around louie.connect
"""
louie.connect(receiver, signal=signal, sender=sender, weak=weak)
[docs] @staticmethod
def disconnect(receiver, signal=louie.signal.All, sender=louie.sender.Any,
weak=True):
""" wrapper method around louie.disconnect
"""
louie.disconnect(receiver, signal=signal, sender=sender, weak=weak)