class Coherence(log.Loggable): __instance = None # Singleton 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.__incarnations += 1 return cls.__instance def __init__(self, config=None): # initialize only once if self.__initialized: return self.__initialized = True # supers log.Loggable.__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() def clear(self): """ we do need this to survive multiple calls to Coherence during trial tests """ self.__cls.__instance = None 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: 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()) 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: self.exception("Can't enable %s plugin for sub-system %s", plugin, device) self.debug(traceback.format_exc()) except KeyError: self.warning("Can't enable %s plugin, not found!", plugin) except Exception as msg: self.warning("Can't enable %s plugin, %s!", plugin, msg) self.debug(traceback.format_exc()) 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 "" @staticmethod def writeable_config(): """ do we have a new-style config file """ return False 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: pass else: self.info("storing plugin config option for %s failed, plugin not found", uuid) def receiver(self, signal, *args, **kwargs): pass 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: pass l = [] for root_device in self.get_devices(): for device in root_device.get_devices(): dd = device.unsubscribe_service_subscriptions() dd.addCallback(device.remove) l.append(dd) rd = root_device.unsubscribe_service_subscriptions() rd.addCallback(root_device.remove) 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(l) dl.addCallback(homecleanup) return dl 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() def subscribe(self, name, callback): self._callbacks.setdefault(name, []).append(callback) def unsubscribe(self, name, callback): callbacks = self._callbacks.get(name, []) if callback in callbacks: callbacks.remove(callback) self._callbacks[name] = callbacks def callback(self, name, *args): for callback in self._callbacks.get(name, []): callback(*args) def get_device_by_host(self, host): found = [] for device in self.devices: if device.get_host() == host: found.append(device) return found def get_device_with_usn(self, usn): found = None for device in self.devices: if device.get_usn() == usn: found = device break return found def get_device_with_id(self, 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 def get_devices(self): return self.devices def get_local_devices(self): return [d for d in self.devices if d.manifestation == 'local'] def get_nonlocal_devices(self): return [d for d in self.devices if d.manifestation == 'remote'] def create_device(self, device_type, infos): self.info("creating %s %s", infos['ST'], infos['USN']) if infos['ST'] == 'upnp:rootdevice': self.info("creating upnp:rootdevice %s", infos['USN']) root = RootDevice(infos) else: self.info("creating device/service %s", infos['USN']) root_id = infos['USN'][:-len(infos['ST']) - 2] root = self.get_device_with_id(root_id) # FIXME doesn't look like doing right thing device = Device(infos, root) def add_device(self, device): self.info("adding device %s %s %s", device.get_id(), device.get_usn(), device.friendly_device_type) self.devices.append(device) def remove_device(self, device_type, infos): self.info("removed device %s %s", 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']) def add_web_resource(self, name, sub): self.children[name] = sub def remove_web_resource(self, name): try: del self.children[name] except KeyError: """ probably the backend init failed """ pass @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) @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)
class Coherence(EventDispatcher, log.LogAble): ''' The Main class of the Cohen3 project. The Coherence class controls all the servers initialization depending on the configuration passed. It is also capable of initialize the plugins defined in config variable or by configuration file. It supports the creation of multiple servers at once. **Example of a simple server via plugin AppleTrailersStore**:: from coherence.base import Coherence from coherence.upnp.core.uuid import UUID from twisted.internet import reactor new_uuid = UUID() coherence = Coherence( {'logmode': 'info', 'controlpoint': 'yes', 'plugin': [{'backend': 'AppleTrailersStore', 'name': 'Cohen3 Example FSStore', 'uuid': new_uuid, } ] } ) reactor.run() .. versionchanged:: 0.9.0 * Introduced inheritance from EventDispatcher * The emitted events changed: - Coherence.UPnP.Device.detection_completed => coherence_device_detection_completed - Coherence.UPnP.Device.removed => coherence_device_removed - Coherence.UPnP.RootDevice.removed => coherence_root_device_removed * Changed some variables to benefit from the EventDispatcher's properties: - :attr:`devices` - :attr:`children` - :attr:`_callbacks` - :attr:`active_backends` - :attr:`ctrl` - :attr:`dbus` - :attr:`json` - :attr:`msearch` - :attr:`ssdp_server` - :attr:`transcoder_manager` - :attr:`web_server` ''' __instance = None # Singleton __initialized = False __incarnations = 0 __cls = None logCategory = 'coherence' devices = ListProperty([]) '''A list of the added devices.''' children = DictProperty({}) '''A dict containing the web resources.''' _callbacks = DictProperty({}) '''A dict containing the callbacks, used by the methods :meth:`subscribe` and :meth:`unsubscribe`.''' active_backends = DictProperty({}) '''A dict containing the active backends.''' # Services/Devices ctrl = Property(None) '''A coherence's instance of class :class:`~coherence.upnp.devices.control_point.ControlPoint`. This will be enabled if we request it by config dict or configuration file via keyword *controlpoint = yes*.''' dbus = Property(None) '''A coherence's instance of class :class:`~coherence.dbus_service.DBusPontoon`. This will be enabled if we request it by config dict or configuration file via keyword *use_dbus = yes*.''' json = Property(None) '''A coherence's instance of class :class:`~coherence.json_service.JsonInterface`. This will be enabled if we request it by config dict or configuration file via keyword *json = yes*.''' msearch = Property(None) '''A coherence's instance of class :class:`~coherence.upnp.core.msearch.MSearch`. This is automatically enabled when :class:`Coherence` is initialized''' ssdp_server = Property(None) '''A coherence's instance of class :class:`~coherence.upnp.core.ssdp.SSDPServer`. This is automatically enabled when :class:`Coherence` is initialized''' transcoder_manager = Property(None) '''A coherence's instance of class :class:`~coherence.transcoder.TranscoderManager`. This will be enabled if we request itby config dict or configuration file via keyword *transcoding = yes*.''' web_server = Property(None) '''A coherence's instance of class :class:`WebServer` or :class:`WebServerUi`. We can request our preference by config dict or configuration file. If we use the keyword *web-ui = yes*, then the class :class:`WebServerUi` will be used, otherwise, the enabled web server will be of class :class:`WebServer`.''' 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) EventDispatcher.__init__(self) self.register_event( 'coherence_device_detection_completed', 'coherence_device_removed', 'coherence_root_device_removed', ) self.config = config or {} self.available_plugins = None self.external_address = None self.urlbase = None self.web_server_port = int(config.get('serverport', 8080)) self.setup_logger() self.setup_hostname() 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() def clear(self): '''We do need this to survive multiple calls to Coherence during trial tests''' self.unbind_all() self.__cls.__instance = None @property def is_unittest(self): ''' Reads config and returns if we are testing or not via `unittest` key. ''' unittest = self.config.get('unittest', 'no') return False if unittest in {'no', False, None} else True @property def log_level(self): '''Read config and return the log level.''' try: log_level = self.config.get('logging').get('level', 'warning') except (KeyError, AttributeError): log_level = self.config.get('logmode', 'warning') return log_level.upper() @property def log_file(self): '''Read config and return the logfile or `None`.''' try: logfile = self.config.get('logging').get('logfile', None) if logfile is not None: logfile = str(logfile) except (KeyError, AttributeError, TypeError): logfile = self.config.get('logfile', None) return logfile def setup_logger(self): '''Initializes log's system based on config. .. note:: the COHEN_DEBUG environment variable overwrites all level settings in here ''' try: subsystems = self.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(f'setting log-level for subsystem ' + f'{subsystem["name"]} to {subsystem["level"]}') logging.getLogger(subsystem['name'].lower()).setLevel( subsystem['level'].upper()) except (KeyError, TypeError): subsystem_log = self.config.get('subsystem_log', {}) for subsystem, level in list(subsystem_log.items()): logging.getLogger(subsystem.lower()).setLevel(level.upper()) log.init(self.log_file, self.log_level) self.warning(f'Coherence UPnP framework version {__version__} ' + f'starting [log level: {self.log_level}]...') def setup_hostname(self): ''' Configure the `hostname`. .. note:: If something goes wrong will default to `127.0.0.1` ''' network_if = self.config.get('interface') if network_if: self.hostname = get_ip_address(f'{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' self.info(f'running on host: {self.hostname}') if self.hostname.startswith('127.'): self.warning( f'detection of own ip failed, using {self.hostname} ' + f'as own address, functionality will be limited') def setup_ssdp_server(self): '''Initialize the :class:`~coherence.upnp.core.ssdp.SSDPServer`.''' try: # TODO: add ip/interface bind self.ssdp_server = SSDPServer(test=self.is_unittest) except CannotListenError as err: self.error(f'Error starting the SSDP-server: {err}') self.debug('Error starting the SSDP-server', exc_info=True) reactor.stop() return # maybe some devices are already notified, so we enforce # to create the device, if it is not already added...and # then we connect the signals for new detections. for st, usn in self.ssdp_server.root_devices: self.create_device(st, usn) self.ssdp_server.bind(new_device=self.create_device) self.ssdp_server.bind(removed_device=self.remove_device) self.ssdp_server.subscribe('new_device', self.add_device) self.ssdp_server.subscribe('removed_device', self.remove_device) def setup_web_server(self): '''Initialize the web server.''' try: # TODO: add ip/interface bind if self.config.get('web-ui', 'no') != 'yes': self.web_server = WebServer(None, self.web_server_port, self) else: self.web_server = WebServerUi(self.web_server_port, self, unittests=self.is_unittest) except CannotListenError: self.error( f'port {self.web_server_port} already in use, aborting!') reactor.stop() return self.urlbase = f'http://{self.hostname}:{self.web_server_port:d}/' self.external_address = ':'.join( (self.hostname, str(self.web_server_port))) # self.renew_service_subscription_loop = \ # task.LoopingCall(self.check_devices) # self.renew_service_subscription_loop.start(20.0, now=False) def setup_plugins(self): '''Initialize the plugins.''' 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(f'Can\'t enable plugin, {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(f'Can\'t enable plugin, {plugin}: {msg}!') self.error(traceback.format_exc()) def setup_part2(self): '''Initializes the basic and optional services/devices and the enabled plugins (backends).''' self.setup_ssdp_server() if not self.ssdp_server: raise Exception('Unable to initialize an ssdp server') self.msearch = MSearch(self.ssdp_server, test=self.is_unittest) reactor.addSystemEventTrigger( 'before', 'shutdown', self.shutdown, force=True, ) self.setup_web_server() if not self.urlbase: raise Exception('Unable to initialize an web server') self.setup_plugins() # 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(f'Unable to activate dbus sub-system: {msg}') self.debug(traceback.format_exc()) def add_plugin(self, plugin, **kwargs): self.info(f'adding plugin {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(f'Activating {plugin} plugin as {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(f'Can\'t enable {plugin} plugin, ' f'sub-system {device} not found!') except Exception as e1: self.exception(f'Can\'t enable {plugin} plugin for ' f'sub-system {device} [exception: {e1}]') self.debug(traceback.format_exc()) except KeyError: self.warning(f'Can\'t enable {plugin} plugin, not found!') except Exception as e2: self.warning(f'Can\'t enable {plugin} plugin, {e2}!') self.debug(traceback.format_exc()) def remove_plugin(self, plugin): '''Removes a backend from Coherence Args: plugin (object): 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(f'no backend with the uuid {plugin} found') return '' try: del self.active_backends[str(plugin.uuid)] self.info(f'removing plugin {plugin}') plugin.unregister() return plugin.uuid except KeyError: self.warning(f'no backend with the uuid {plugin.uuid} found') return '' @staticmethod def writeable_config(): '''Do we have a new-style config file''' return False 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(f'Coherence.store_plugin_config: {e}') else: self.info(f'storing plugin config option ' f'for {uuid} failed, plugin not found') def receiver(self, signal, *args, **kwargs): pass 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 if self.web_server is not None: if hasattr(self.web_server, 'endpoint_listen'): if self.web_server.endpoint_listen is not None: self.web_server.endpoint_listen.cancel() self.web_server.endpoint_listen = None if self.web_server.endpoint_port is not None: self.web_server.endpoint_port.stopListening() if hasattr(self.web_server, 'ws_endpoint_listen'): if self.web_server.ws_endpoint_listen is not None: self.web_server.ws_endpoint_listen.cancel() self.web_server.ws_endpoint_listen = None if self.web_server.ws_endpoint_port is not None: self.web_server.ws_endpoint_port.stopListening() try: 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(): if hasattr(root_device, 'root_device_detection_completed'): root_device.unbind( root_device_detection_completed=self.add_device) 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): # cleans up anything left over self.ssdp_server.unbind(new_device=self.create_device) self.ssdp_server.unbind(removed_device=self.remove_device) 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 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() def subscribe(self, name, callback): self._callbacks.setdefault(name, []).append(callback) def unsubscribe(self, name, callback): callbacks = self._callbacks.get(name, []) if callback in callbacks: callbacks.remove(callback) self._callbacks[name] = callbacks def callback(self, name, *args): for callback in self._callbacks.get(name, []): callback(*args) def get_device_by_host(self, host): found = [] for device in self.devices: if device.get_host() == host: found.append(device) return found def get_device_with_usn(self, usn): found = None for device in self.devices: if device.get_usn() == usn: found = device break return found def get_device_with_id(self, device_id): # print(f'get_device_with_id [{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 def get_devices(self): # print(f'get_devices: {self.devices}') return self.devices def get_local_devices(self): # print(f'get_local_devices: ' # f'{[d for d in self.devices if d.manifestation == "local"]}') return [d for d in self.devices if d.manifestation == 'local'] def get_nonlocal_devices(self): # print(f'get_nonlocal_devices: ' # f'{[d for d in self.devices if d.manifestation == "remote"]}') return [d for d in self.devices if d.manifestation == 'remote'] def is_device_added(self, infos): ''' Check if the device exists in our list of created :attr:`devices`. Args: infos (dict): Information about the device Returns: True if the device exists in our list of :attr:`devices`, otherwise, returns False. .. versionadded:: 0.9.0 ''' for d in self.devices: if d.st == infos['ST'] and d.usn == infos['USN']: return True return False def create_device(self, device_type, infos): if self.is_device_added(infos): self.warning( f'No need to create the device, we already added device: ' f'{infos["ST"]} with usn {infos["USN"]}...!!') return self.info(f'creating {infos["ST"]} {infos["USN"]}') if infos['ST'] == 'upnp:rootdevice': self.info(f'creating upnp:rootdevice {infos["USN"]}') root = RootDevice(infos) root.bind(root_detection_completed=self.add_device) else: self.info(f'creating device/service {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']) def add_device(self, device, *args): self.info(f'adding device {device.get_id()} {device.get_usn()} ' + f'{device.friendly_device_type}') self.devices.append(device) self.dispatch_event('coherence_device_detection_completed', device=device) def remove_device(self, device_type, infos): self.info(f'removed device {infos["ST"]} {infos["USN"]}') device = self.get_device_with_usn(infos['USN']) if device: self.dispatch_event('coherence_device_removed', infos['USN'], device.client) self.devices.remove(device) device.remove() if infos['ST'] == 'upnp:rootdevice': self.dispatch_event( 'coherence_root_device_removed', infos['USN'], device.client, ) self.callback('removed_device', infos['ST'], infos['USN']) def add_web_resource(self, name, sub): self.children[name] = sub def remove_web_resource(self, name): try: del self.children[name] except KeyError: ''' probably the backend init failed ''' pass @staticmethod def check_louie(receiver, signal, method='connect'): ''' Check if the connect or disconnect method's arguments are valid in order to automatically convert to EventDispatcher's bind The old valid signals are: - Coherence.UPnP.Device.detection_completed - Coherence.UPnP.RootDevice.detection_completed - Coherence.UPnP.Device.removed - Coherence.UPnP.RootDevice.removed .. versionadded:: 0.9.0 ''' if not callable(receiver): raise Exception('The receiver should be callable in order to use' + ' the method {method}') if not signal: raise Exception( f'We need a signal in order to use method {method}') if not (signal.startswith('Coherence.UPnP.Device.') or signal.startswith('Coherence.UPnP.RootDevice.')): raise Exception( 'We need a signal an old signal starting with: ' + '"Coherence.UPnP.Device." or "Coherence.UPnP.RootDevice."') def connect(self, receiver, signal=None, sender=None, weak=True): ''' Wrapper method around the deprecated method louie.connect. It will check if the passed signal is supported by executing the method :meth:`check_louie`. .. warning:: This will probably be removed at some point, if you use the connect method you should migrate to the new event system EventDispatcher. .. versionchanged:: 0.9.0 Added EventDispatcher's compatibility for some basic signals ''' self.check_louie(receiver, signal, 'connect') if signal.endswith('.detection_completed'): self.bind(coherence_device_detection_completed=receiver) elif signal.endswith('.Device.removed'): self.bind(coherence_device_removed=receiver) elif signal.endswith('.RootDevice.removed'): self.bind(coherence_root_device_removed=receiver) else: raise Exception( f'Unknown signal {signal}, we cannot bind that signal.') def disconnect(self, receiver, signal=None, sender=None, weak=True): ''' Wrapper method around the deprecated method louie.disconnect. It will check if the passed signal is supported by executing the method :meth:`check_louie`. .. warning:: This will probably be removed at some point, if you use the disconnect method you should migrate to the new event system EventDispatcher. .. versionchanged:: 0.9.0 Added EventDispatcher's compatibility for some basic signals ''' self.check_louie(receiver, signal, 'disconnect') if signal.endswith('.detected'): self.unbind(coherence_device_detection_completed=receiver) elif signal.endswith('.removed'): self.unbind(control_point_client_removed=receiver) else: raise Exception( f'Unknown signal {signal}, we cannot unbind that signal.')
class ManagedControlPoint(object): def __init__(self): self.coherence = Coherence({'logmode':'warning'}) self._controlPoint = ControlPoint(self.coherence, auto_client=['MediaServer','MediaRenderer']) self._controlPoint.connect(self._onMediaServerDetected, 'Coherence.UPnP.ControlPoint.MediaServer.detected') self._controlPoint.connect(self._onMediaServerRemoved, 'Coherence.UPnP.ControlPoint.MediaServer.removed') self._controlPoint.connect(self._onMediaRendererDetected, 'Coherence.UPnP.ControlPoint.MediaRenderer.detected') self._controlPoint.connect(self._onMediaRendererRemoved, 'Coherence.UPnP.ControlPoint.MediaRenderer.removed') self._controlPoint.connect(self._onMediaDeviceDectected, 'Coherence.UPnP.Device.detection_completed') self.__mediaServerClients = {} self.__mediaRendererClients = {} self.__mediaDevices = {} self.__browser = [] self.__devices = [] self.onMediaServerDetected = [] self.onMediaServerRemoved = [] self.onMediaRendererDetected = [] self.onMediaRendererRemoved = [] self.onMediaDeviceDectected = [] def _onMediaServerDetected(self, client, udn): print "[DLNA] MediaServer Detected: %s (%s)" % (client.device.get_friendly_name(), client.device.get_friendly_device_type()) self.__mediaServerClients[udn] = client for fnc in self.onMediaServerDetected: fnc(udn, client) def _onMediaServerRemoved(self, udn): if self.__mediaServerClients.get(udn, None) != None: del self.__mediaServerClients[udn] for fnc in self.onMediaServerRemoved: fnc(udn) def _onMediaRendererDetected(self, client, udn): print "[DLNA] MediaRenderer detected: %s (%s, %s)" % (client.device.get_friendly_name(), client.device.get_friendly_device_type(), udn) self.__mediaRendererClients[udn] = client for fnc in self.onMediaRendererDetected: fnc(udn, client) def _onMediaRendererRemoved(self, udn): print "[DLNA] MediaRenderer removed: %s" % (udn) if self.__mediaRendererClients.get(udn, None) != None: del self.__mediaRendererClients[udn] for fnc in self.onMediaRendererRemoved: fnc(udn) def _onMediaDeviceDectected(self, device): print "[DLNA] Device found: %s (%s)" % (device.get_friendly_name(), device.get_friendly_device_type()) self.__mediaDevices[device.udn] = device def registerRenderer(self, classDef, **kwargs): renderer = MediaRenderer(self.coherence, classDef, no_thread_needed=True, **kwargs) self.__devices.append(renderer) return renderer def registerServer(self, classDef, **kwargs): server = MediaServer(self.coherence, classDef, no_thread_needed=True, **kwargs) self.__devices.append(server) return server def getServerList(self): return self.__mediaServerClients.values() def getRenderingControlClientList(self): return self.__mediaRendererClients.values() def getDeviceName(self, client): return Item.ue(client.device.get_friendly_name()) def shutdown(self): for device in self.__devices: device.unregister() self._controlPoint.shutdown()