def disconnect(self, remove_from_list=True, send_bye_msg=False): """ Disconnect the client. @keyword remove_from_list: whether to remove the client from the client list or not (default is true) @keyword send_bye_msg: whether to send a bye message before disconnecting """ # send bye message if send_bye_msg and self.__sock is not None: log.info("send 'bye' to %s" % self) msg = build_message(message.CONN_BYE, None) sent = 0 retry = 0 while sent < len(msg) and retry < 10: try: sent += self.__sock.send(msg) except socket.error, e: log.warning("failed to send 'bye' to %s (%s)" % (self, e)) break time.sleep(0.02) retry += 1 if sent < len(msg): log.warning("failed to send 'bye' to %s" % self) else: # give client some time to close connection: time.sleep(0.1)
def run(self): """Activate the manager. This method starts the player adapter, runs a main loop (GLib) and blocks until SIGINT or SIGTERM arrives or until stop() gets called. If this happens the player adapter gets stopped and this method returns. If `player_dbus_name` or `poll_fn` has been passed to __init__(), then the player adapter does not get started until the player is running (according to checks based on the DBus name or poll function). Also the adapter gets stopped automatically if the player is not running anymore. However, the manager keeps running, i.e. the player adapter may get started and stopped multiple times while this method is running. """ if self.__observer is None: # start pa directly ready = _start_pa(self.__pa) else: # observer will start pa ready = True if ready and not self.__stopped: # not stopped since creation log.info("start main loop") try: self.__ml.run() except Exception, e: log.exception("** BUG ** %s", e) else: log.info("main loop stopped")
def run(self): """Activate the manager. This method starts the player adapter, runs a main loop (GLib) and blocks until SIGINT or SIGTERM arrives or until stop() gets called. If this happens the player adapter gets stopped and this method returns. @note: If the keyword 'player_dbus_name' has been set in __init__(), then the player adapter does not get started until an application owns the bus name given by 'player_dbus_name'. It automatically gets started whenever the DBus name has an owner (which means the adapter's player is running) and it gets stopped when it has no owner. Obvisously here the player adapter may get started and stopped repeatedly while this method is running. """ if self.__observer is None: # start pa directly ready = _start_pa(self.__pa) else: # observer will start pa ready = True if ready and not self.__stopped: # not stopped since creation log.info("start main loop") try: self.__ml.run() except Exception, e: log.exception("** BUG ** %s", e) log.info("main loop stopped")
def _stop_pa(pa): """Stop the given player adapter with error handling.""" log.info("stop player adapter") try: pa.stop() except Exception, e: log.exception("** BUG ** %s", e)
def _start_pa(pa): """Start the given player adapter with error handling.""" log.info("start player adapter") try: pa.start() except StandardError, e: log.error("failed to start player adapter (%s)" % e) return False
def stop(self): """Manually shut down the manager. Stops the manager's main loop and player adapter. As a result a previous call to run() will return now. """ log.info("stop manager manually") self.__stopped = True self.__ml.quit()
def stop(self): """Shut down the manager. Stops the manager's main loop and player adapter. As a result a previous call to run() will return now. This should be used by player adapters when there is a crucial error and restarting the adapter won't fix this. """ log.info("manager stopped internally") self.__stopped = True self.__ml.quit()
def disconnect(self, remove_from_list=True, send_bye_msg=False): """ Disconnect the client. @keyword remove_from_list: whether to remove the client from the client list or not (default is true) @keyword send_bye_msg: whether to send a bye message before disconnecting """ # send bye message if send_bye_msg and self.__sock is not None: log.info("send 'bye' to %s" % self) msg = build_message(message.CONN_BYE, None) sent = 0 retry = 0 while sent < len(msg) and retry < 10: try: sent += self.__sock.send(msg) except socket.error as e: log.warning("failed to send 'bye' to %s (%s)" % (self, e)) break time.sleep(0.02) retry += 1 if sent < len(msg): log.warning("failed to send 'bye' to %s" % self) else: # give client some time to close connection: time.sleep(0.1) # disconnect log.debug("disconnect %s" % self) if remove_from_list and self in self.__clients: self.__clients.remove(self) for sid in self.__sids: GObject.source_remove(sid) self.__sids = () if (self.__sid_out > 0): GObject.source_remove(self.__sid_out) self.__sid_out = 0 if self.__sock is not None: try: self.__sock.shutdown(socket.SHUT_RDWR) except socket.error as e: pass self.__sock.close() self.__sock = None
def get_system_shutdown_command(): path = os.path.join(xdg_config, "remuco", "shutdown-system") if not os.path.isfile(path): log.info("system shutdown command (%s) does not exist" % path) return None if not os.access(path, os.X_OK): log.info("system shutdown command (%s) is not executable" % path) return None return path
def _start_pa(pa): """Start the given player adapter with error handling.""" log.info("start player adapter") try: pa.start() except StandardError as e: log.error("failed to start player adapter (%s)" % e) return False except Exception as e: log.exception("** BUG ** %s", e) return False else: log.info("player adapter started") return True
def __cleanup(self): """Trash obsolete config and cache data from older versions.""" def obsolete(fn): """Check if a config or cache item may be trashed.""" obs = isdir(fn) obs |= basename(fn) in ("shutdown-system", "volume") obs &= not basename(fn).startswith("old-") return obs for dname, dtype in ((self.dir, "config"), (self.cache, "cache")): trash = join(dname, "old-%s.backup" % _TS) fnames = [f for f in glob(join(dname, "*")) if obsolete(f)] if fnames: log.info("moving old %s data to %s" % (dtype, trash)) if not exists(trash): os.makedirs(trash) for fn in fnames: shutil.move(fn, trash)
def __init__(self, root_dirs, mime_types, show_extensions, show_hidden): self.__mime_types = mime_types self.__show_extensions = show_extensions self.__show_hidden = show_hidden if not sys.getfilesystemencoding() in ("UTF8", "UTF-8", "UTF_8"): log.warning("file system encoding is not UTF-8, this may cause " + "problems with file browser features") root_dirs = root_dirs or [] # mimetype dependent root dirs if "auto" in root_dirs: root_dirs.remove("auto") if mime_types: for mtype in mime_types: if mtype in media_dirs: root_dirs += media_dirs[mtype] mtype = mtype.split("/")[0] # use main mimetype if mtype in media_dirs: root_dirs += media_dirs[mtype] root_dirs = self.__trim_root_dirs(root_dirs) or [user_home] # map root dirs to names self.__roots = {} for dir in root_dirs: name = os.path.basename(dir) if name == dir: # == "/" name = "Root" else: name = name.capitalize() counter = 2 name_x = name while name_x in self.__roots: name_x = "%s (%d)" % (name, counter) counter += 1 self.__roots[name_x] = dir log.info("file browser root dirs: %s " % self.__roots) if not mimetypes.inited: mimetypes.init()
def __init__(self, pa, dbus_name=None, poll_fn=None): """Create a new manager. @param pa: the PlayerAdapter to manage @keyword dbus_name: if the player adapter uses DBus to communicate with its player set this to the player's well known bus name (see run() for more information) @keyword poll_fn: if DBus is not used, this function may be set for periodic checks if the player is running, used to automatically start and stop the player adapter When neither `dbus_name` nor `poll_fn` is given, the adapter is started immediately, assuming the player is running and the adapter is ready to work. """ self.__pa = pa self.__pa.manager = self self.__stopped = False self.__observer = None global _ml if _ml is None: _ml = gobject.MainLoop() signal.signal(signal.SIGINT, _sighandler) signal.signal(signal.SIGTERM, _sighandler) self.__ml = _ml if dbus_name: log.info("start dbus observer") self.__observer = _DBusObserver(pa, dbus_name) elif poll_fn: log.info("start polling observer") self.__observer = _PollingObserver(pa, poll_fn) else: # nothing to do pass
def __init__(self, root_dirs, mime_types, show_extensions=False, show_hidden=False, use_user_dirs=True): self.__mime_types = mime_types or _AllMimeTypes() self.__show_extensions = show_extensions self.__show_hidden = show_hidden if not sys.getfilesystemencoding() in ("UTF8", "UTF-8", "UTF_8"): log.warning("file system encoding is not UTF-8, this may cause " + "problems with file browser features") root_dirs = root_dirs or [] if use_user_dirs: root_dirs += self.__get_mime_dirs(mime_types) root_dirs = self.__trim_dirs(root_dirs) if not root_dirs: root_dirs = (os.getenv("USERPROFILE", os.path.sep), ) # map root dirs to names self.__roots = {} for dir in root_dirs: name = os.path.basename(dir) if name == dir: # == "/" name = "Root" else: name.capitalize() counter = 2 name_x = name while name_x in self.__roots: name_x = "%s (%d)" % (name, counter) counter += 1 self.__roots[name_x] = dir log.info("file browser root dirs: %s " % self.__roots) if not mimetypes.inited: mimetypes.init()
def __init__(self, clients, pinfo, msg_handler_fn, config): """ Create a new server. @param clients: a list to add connected clients to @param pinfo: player info (type data.PlayerInfo) @param msg_handler_fn: callback function for passing received messages to @param config: adapter configuration """ self.__clients = clients self.__msg_handler_fn = msg_handler_fn self.__pinfo_msg = build_message(message.CONN_PINFO, pinfo) self.__sid = None self._pinfo = pinfo self._config = config self._sock = None # set up socket try: self._sock = self._create_socket() self._sock.settimeout(_Server.SOCKET_TIMEOUT) except (IOError, socket.error) as e: # TODO: socket.error may be removed when 2.5 support is dropped log.error("failed to set up %s server (%s)" % (self._get_type(), e)) return log.info("created %s server" % self._get_type()) # watch socket self.__sid = GObject.io_add_watch(self._sock, GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP, self.__handle_io)
def __check_version(self): """Check version of the configuration. Resets the config on a major change and rewrites the config on any change. The last ensures that added options (minor change) are present in the configuration file. """ try: # check version version = self.__cp.get(SEC, KEY_CONFIG_VERSION) except (ValueError, AttributeError, NoOptionError): version = "0.0" major = version.split(".")[0] if major != CONFIG_VERSION_MAJOR: # on major change, reset configuration log.info("config major version changed -> reset config") self.__cp = ConfigParser.SafeConfigParser(DEFAULTS) # remove old, now unused options: rewrite = False items = self.__cp.items(SEC) for key, val in items: if not (key in DEFAULTS or key.startswith("custom-") or key == KEY_CONFIG_VERSION): # obsolete option -> remove self.__cp.remove_option(SEC, key) rewrite = True # force a rewrite if something has changed if version != CONFIG_VERSION or rewrite: # on any change (major or minor), rewrite config log.debug("config structure changed -> force rewrite") self.__cp.set(SEC, KEY_CONFIG_VERSION, CONFIG_VERSION) self.__save()
class _Server(object): SOCKET_TIMEOUT = 2.5 def __init__(self, clients, pinfo, msg_handler_fn, config): """ Create a new server. @param clients: a list to add connected clients to @param pinfo: player info (type data.PlayerInfo) @param msg_handler_fn: callback function for passing received messages to @param config: adapter configuration """ self.__clients = clients self.__msg_handler_fn = msg_handler_fn self.__pinfo_msg = build_message(message.CONN_PINFO, pinfo) self.__sid = None self._pinfo = pinfo self._config = config self._sock = None # set up socket try: self._sock = self._create_socket() self._sock.settimeout(_Server.SOCKET_TIMEOUT) except (IOError, socket.error), e: # TODO: socket.error may be removed when 2.5 support is dropped log.error("failed to set up %s server (%s)" % (self._get_type(), e)) return log.info("created %s server" % self._get_type()) # watch socket self.__sid = gobject.io_add_watch( self._sock, gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.__handle_io)
def __init__(self, pa, player_dbus_name=None, run_check_fn=None): """Create a new Manager. @param pa: the PlayerAdapter to manage @keyword player_dbus_name: if the player adapter uses DBus to communicate with its player set this to the player's well known bus name (see run() for more information) @keyword run_check_fn: optional function to call to check if the player is running, used for automatically starting and stopping the player When neither `player_dbus_name` nor `run_check_fn` is given, the adapter is started immediately, assuming the player is running and the adapter is ready to work. """ self.__pa = pa self.__pa.manager = self self.__stopped = False self.__ml = _init_main_loop() self.__observer = None if player_dbus_name: log.info("start dbus observer") self.__observer = _DBusObserver(pa, player_dbus_name) log.info("dbus observer started") elif run_check_fn: log.info("start custom observer") self.__observer = _CustomObserver(pa, run_check_fn) log.info("custom observer started") else: # nothing to do pass
# ============================================================================= def _start_pa(pa): """Start the given player adapter with error handling.""" log.info("start player adapter") try: pa.start() except StandardError, e: log.error("failed to start player adapter (%s)" % e) return False except Exception, e: log.exception("** BUG ** %s", e) return False else: log.info("player adapter started") return True def _stop_pa(pa): """Stop the given player adapter with error handling.""" log.info("stop player adapter") try: pa.stop() except Exception, e: log.exception("** BUG ** %s", e) else: log.info("player adapter stopped") # ============================================================================= # Polling Observer
if exists(self.file): try: cp.read(self.file) except ConfigParser.Error, e: log.warning("failed to read config %s (%s)" % (self.file, e)) # reset on version change if cp.get(ConfigParser.DEFAULTSECT, "config-version") != _CONFIG_VERSION: sections = cp.sections() # keep already existing player sections cp = ConfigParser.RawConfigParser(_DEFAULTS, _odict) for sec in sections: cp.add_section(sec) if exists(self.file): bak = "%s.%s.backup" % (self.file, _TS) log.info("reset config (major changes, backup: %s)" % bak) shutil.copy(self.file, bak) # remove unknown options in all sections for sec in cp.sections() + [ConfigParser.DEFAULTSECT]: for key, value in cp.items(sec): if key not in _DEFAULTS and not key.startswith("x-"): cp.remove_option(sec, key) # add not yet existing options to default section for key, value in _DEFAULTS.items(): if not cp.has_option(ConfigParser.DEFAULTSECT, key): cp.set(ConfigParser.DEFAULTSECT, key, value) # update version cp.set(ConfigParser.DEFAULTSECT, "config-version", _CONFIG_VERSION)
cp.add_section(self.player) if exists(self.file): try: cp.read(self.file) except ConfigParser.Error, e: log.warning("failed to read config %s (%s)" % (self.file, e)) # reset on version change if cp.get(ConfigParser.DEFAULTSECT, "config-version") != _CONFIG_VERSION: sections = cp.sections() # keep already existing player sections cp = ConfigParser.RawConfigParser(_DEFAULTS, _odict) for sec in sections: cp.add_section(sec) if exists(self.file): bak = "%s.%s.backup" % (self.file, _TS) log.info("reset config (major changes, backup: %s)" % bak) shutil.copy(self.file, bak) # remove unknown options in all sections for sec in cp.sections() + [ConfigParser.DEFAULTSECT]: for key, value in cp.items(sec): if key not in _DEFAULTS and not key.startswith("x-"): cp.remove_option(sec, key) # add not yet existing options to default section for key, value in _DEFAULTS.items(): if not cp.has_option(ConfigParser.DEFAULTSECT, key): cp.set(ConfigParser.DEFAULTSECT, key, value) # update version cp.set(ConfigParser.DEFAULTSECT, "config-version", _CONFIG_VERSION)
def __init__(self, player_name): """Create a new instance for the given player (adapter).""" super(Config, self).__init__() # convert descriptive name to a plain canonical one self.player = re.sub(r'[^\w-]', '', player_name).lower() # paths self.dir = join(user_config_dir, "remuco") self.cache = join(user_cache_dir, "remuco") self.file = join(self.dir, "remuco.cfg") # remove old stuff self.__cleanup() # create directories for dname in (self.dir, self.cache): try: if not isdir(dname): os.makedirs(dname) except OSError as e: log.error("failed to make dir: %s", e) if not "REMUCO_LOG_STDOUT" in os.environ and isdir(self.cache): log.set_file(join(self.cache, "%s.log" % self.player)) # load cp = configparser.RawConfigParser(_DEFAULTS, _odict) if not cp.has_section(self.player): cp.add_section(self.player) if exists(self.file): try: cp.read(self.file) except configparser.Error as e: log.warning("failed to read config %s (%s)" % (self.file, e)) # reset on version change if cp.get(configparser.DEFAULTSECT, "config-version") != _CONFIG_VERSION: sections = cp.sections() # keep already existing player sections cp = configparser.RawConfigParser(_DEFAULTS, _odict) for sec in sections: cp.add_section(sec) if exists(self.file): bak = "%s.%s.backup" % (self.file, _TS) log.info("reset config (major changes, backup: %s)" % bak) shutil.copy(self.file, bak) # remove unknown options in all sections for sec in cp.sections() + [configparser.DEFAULTSECT]: for key, value in cp.items(sec): if key not in _DEFAULTS and not key.startswith("x-"): cp.remove_option(sec, key) # add not yet existing options to default section for key, value in _DEFAULTS.items(): if not cp.has_option(configparser.DEFAULTSECT, key): cp.set(configparser.DEFAULTSECT, key, value) # update version cp.set(configparser.DEFAULTSECT, "config-version", _CONFIG_VERSION) self.__cp = cp # save to always have a clean file self.__save() log.set_level(self.log_level) log.info("remuco version: %s" % defs.REMUCO_VERSION)
def __user_notification(summary, text): text = text.replace("<b>", "") text = text.replace("</b>", "") log.info("%s: %s" % (summary, text))
def _sighandler(signum, frame): log.info("received signal %i" % signum) if _ml is not None: _ml.quit()
if not "--remuco-log-stdout" in sys.argv: log.set_file(self.__file_log) ###### custom volume command ###### cmd = os.path.join(xdg_config, "remuco", "volume") if not os.path.isfile(cmd): cmd = os.path.join(self.__dir_config, "volume") if not os.path.isfile(cmd): log.debug("custom volume command does not exist (%s)" % cmd) self.__custom_volume_cmd = None elif not os.access(cmd, os.X_OK): log.warning("custom volume command (%s) is not executable" % cmd) self.__custom_volume_cmd = None else: log.info("using custom volume command (%s)" % cmd) self.__custom_volume_cmd = cmd ###### load configuration ###### self.__cp = ConfigParser.SafeConfigParser(DEFAULTS) if os.path.exists(self.__file_config): self.__load() self.__check_version() else: self.__cp.set(SEC, KEY_CONFIG_VERSION, CONFIG_VERSION) self.__save() log.set_level(self.log_level)
class Manager(object): """Life cycle manager for a stand-alone player adapter. A manager cares about calling a PlayerAdapter's start and stop methods. Additionally, because Remuco needs a GLib main loop to run, it sets up and manages such a loop. It is intended for player adapters running stand-alone, outside the players they adapt. A manager is not needed for player adapters realized as a plugin for a media player. In that case the player's plugin interface should care about the life cycle of a player adapter (see the Rhythmbox player adapter as an example). """ def __init__(self, pa, dbus_name=None, poll_fn=None): """Create a new manager. @param pa: the PlayerAdapter to manage @keyword dbus_name: if the player adapter uses DBus to communicate with its player set this to the player's well known bus name (see run() for more information) @keyword poll_fn: if DBus is not used, this function may be set for periodic checks if the player is running, used to automatically start and stop the player adapter When neither `dbus_name` nor `poll_fn` is given, the adapter is started immediately, assuming the player is running and the adapter is ready to work. """ self.__pa = pa self.__pa.manager = self self.__stopped = False self.__observer = None global _ml if _ml is None: _ml = gobject.MainLoop() signal.signal(signal.SIGINT, _sighandler) signal.signal(signal.SIGTERM, _sighandler) self.__ml = _ml if dbus_name: log.info("start dbus observer") self.__observer = _DBusObserver(pa, dbus_name) elif poll_fn: log.info("start polling observer") self.__observer = _PollingObserver(pa, poll_fn) else: # nothing to do pass def run(self): """Activate the manager. This method starts the player adapter, runs a main loop (GLib) and blocks until SIGINT or SIGTERM arrives or until stop() gets called. If this happens the player adapter gets stopped and this method returns. If `player_dbus_name` or `poll_fn` has been passed to __init__(), then the player adapter does not get started until the player is running (according to checks based on the DBus name or poll function). Also the adapter gets stopped automatically if the player is not running anymore. However, the manager keeps running, i.e. the player adapter may get started and stopped multiple times while this method is running. """ if self.__observer is None: # start pa directly ready = _start_pa(self.__pa) else: # observer will start pa ready = True if ready and not self.__stopped: # not stopped since creation log.info("start main loop") try: self.__ml.run() except Exception, e: log.exception("** BUG ** %s", e) else: log.info("main loop stopped") if self.__observer: # stop observer self.__observer.stop() log.info("observer stopped") # stop pa _stop_pa(self.__pa)
def __init__( self, name, playback_known=False, volume_known=False, repeat_known=False, shuffle_known=False, progress_known=False, max_rating=0, poll=2.5, file_actions=None, mime_types=None, search_mask=None, ): """Create a new player adapter and configure its capabilities. Just does some early initializations. Real job starts with start(). @param name: name of the media player @keyword playback_known: indicates if the player's playback state can be provided (see update_playback()) @keyword volume_known: indicates if the player's volume can be provided (see update_volume()) @keyword repeat_known: indicates if the player's repeat mode can be provided (see update_repeat()) @keyword shuffle_known: indicates if the player's shuffle mode can be provided (see update_shuffle()) @keyword progress_known: indicates if the player's playback progress can be provided (see update_progress()) @keyword max_rating: maximum possible rating value for items @keyword poll: interval in seconds to call poll() @keyword file_actions: list of ItemAction which can be applied to files from the local file system (actions like play a file or append files to the playlist) - this keyword is only relevant if the method action_files() gets overridden @keyword mime_types: list of mime types specifying the files to which the actions given by the keyword 'file_actions' can be applied, this may be general types like 'audio' or 'video' but also specific types like 'audio/mp3' or 'video/quicktime' (setting this to None means all mime types are supported) - this keyword is only relevant if the method action_files() gets overridden @keyword search_mask: list of fields to search the players library for (e.g. artist, genre, any, ...) - if set method request_search() should be overridden @attention: When overriding, call super class implementation first! """ self.__name = name # init config (config inits logging) self.config = config.Config(self.__name) # init misc fields serial.Bin.HOST_ENCODING = self.config.player_encoding self.__clients = [] self.__state = PlayerState() self.__progress = Progress() self.__item_id = None self.__item_info = None self.__item_img = None flags = self.__util_calc_flags(playback_known, volume_known, repeat_known, shuffle_known, progress_known) self.__info = PlayerInfo(name, flags, max_rating, file_actions, search_mask) self.__sync_triggers = {} self.__poll_ival = max(500, int(poll * 1000)) self.__poll_sid = 0 self.stopped = True self.__server_bluetooth = None self.__server_wifi = None if self.config.fb_root_dirs: self.__filelib = files.FileSystemLibrary( self.config.fb_root_dirs, mime_types, self.config.fb_show_extensions, False ) else: log.info("file browser is disabled") if "REMUCO_TESTSHELL" in os.environ: from remuco import testshell testshell.setup(self) log.debug("init done")
def _sighandler(signum, frame): """Used by Manager. """ log.info("received signal %i" % signum) if _ml is not None: _ml.quit()
def __io_hup(self, fd, cond): """ GObject callback function (when other side disconnected). """ log.info("client %s disconnected" % self) self.disconnect() return False
def notify(title, text): log.info("%s: %s" % (title, text))
def _start_pa(pa): """Start the given player adapter with error handling.""" log.info("start player adapter") try: pa.start() except StandardError, e: log.error("failed to start player adapter (%s)" % e) return False except Exception, e: log.exception("** BUG ** %s", e) return False else: log.info("player adapter started") return True def _stop_pa(pa): """Stop the given player adapter with error handling.""" log.info("stop player adapter") try: pa.stop() except Exception, e: log.exception("** BUG ** %s", e) else: log.info("player adapter stopped")