class RazerDaemon(DBusService): """ Daemon class This class sets up the main run loop which serves DBus messages. The logger is initialised in this module as well as finding and initialising devices. Serves the following functions via DBus * getDevices - Returns a list of serial numbers * enableTurnOffOnScreensaver - Starts/Continues the run loop on the screensaver thread * disableTurnOffOnScreensaver - Pauses the run loop on the screensaver thread """ BUS_NAME = 'org.razer' def __init__(self, verbose=False, log_dir=None, console_log=False, run_dir=None, config_file=None, test_dir=None): setproctitle.setproctitle('openrazer-daemon') # pylint: disable=no-member # Expanding ~ as python doesn't do it by default, also creating dirs if needed try: if log_dir is not None: log_dir = os.path.expanduser(log_dir) os.makedirs(log_dir, exist_ok=True) if run_dir is not None: run_dir = os.path.expanduser(run_dir) os.makedirs(run_dir, exist_ok=True) except NotADirectoryError as e: print("Failed to create {}".format(e.filename), file=sys.stderr) sys.exit(1) if config_file is not None: config_file = os.path.expanduser(config_file) if not os.path.exists(config_file): print("Config file {} does not exist.".format(config_file), file=sys.stderr) sys.exit(1) self._test_dir = test_dir self._run_dir = run_dir self._config_file = config_file self._config = configparser.ConfigParser() self.read_config(config_file) # Logging log_level = logging.INFO if verbose or self._config.getboolean('General', 'verbose_logging'): log_level = logging.DEBUG self.logger = self._create_logger(log_dir, log_level, console_log) # Check for plugdev group if not self._check_plugdev_group(): self.logger.critical("User is not a member of the plugdev group") sys.exit(1) # Setup DBus to use gobject main loop dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) DBusService.__init__(self, self.BUS_NAME, '/org/razer') self._init_signals() self._main_loop = GLib.MainLoop() # Listen for input events from udev self._init_udev_monitor() # Load Classes self._device_classes = openrazer_daemon.hardware.get_device_classes() self.logger.info("Initialising Daemon (v%s). Pid: %d", __version__, os.getpid()) self._init_screensaver_monitor() self._razer_devices = DeviceCollection() self._load_devices(first_run=True) # Add DBus methods methods = { # interface, method, callback, in-args, out-args ('razer.devices', 'getDevices', self.get_serial_list, None, 'as'), ('razer.devices', 'supportedDevices', self.supported_devices, None, 's'), ('razer.devices', 'enableTurnOffOnScreensaver', self.enable_turn_off_on_screensaver, 'b', None), ('razer.devices', 'getOffOnScreensaver', self.get_off_on_screensaver, None, 'b'), ('razer.devices', 'syncEffects', self.sync_effects, 'b', None), ('razer.devices', 'getSyncEffects', self.get_sync_effects, None, 'b'), ('razer.daemon', 'version', self.version, None, 's'), ('razer.daemon', 'stop', self.stop, None, None), } for m in methods: self.logger.debug("Adding {}.{} method to DBus".format(m[0], m[1])) self.add_dbus_method(m[0], m[1], m[2], in_signature=m[3], out_signature=m[4]) self._collecting_udev = False self._collecting_udev_devices = [] # TODO remove self.sync_effects( self._config.getboolean('Startup', 'sync_effects_enabled')) # TODO ====== @dbus.service.signal('razer.devices') def device_removed(self): self.logger.debug("Emitted Device Remove Signal") @dbus.service.signal('razer.devices') def device_added(self): self.logger.debug("Emitted Device Added Signal") def _create_logger(self, log_dir, log_level, want_console_log): """ Initializes a logger and returns it. :param log_dir: If not None, specifies the directory to create the log file in :param log_level: The log level of messages to print :param want_console_log: True if we should print to the console :rtype:logging.Logger """ logger = logging.getLogger('razer') logger.setLevel(log_level) formatter = logging.Formatter( '%(asctime)s | %(name)-30s | %(levelname)-8s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S') # Don't propagate to default logger logger.propagate = 0 if want_console_log: console_logger = logging.StreamHandler() console_logger.setLevel(log_level) console_logger.setFormatter(formatter) logger.addHandler(console_logger) if log_dir is not None: log_file = os.path.join(log_dir, 'razer.log') file_logger = logging.handlers.RotatingFileHandler( log_file, maxBytes=16777216, backupCount=10) # 16MiB file_logger.setLevel(log_level) file_logger.setFormatter(formatter) logger.addHandler(file_logger) return logger def _check_plugdev_group(self): """ Check if the user is a member of the plugdev group. For the root user, this always returns True :rtype: bool """ if getpass.getuser() == 'root': return True try: return grp.getgrnam('plugdev').gr_gid in os.getgroups() except KeyError: pass return False def _init_udev_monitor(self): self._udev_context = Context() udev_monitor = Monitor.from_netlink(self._udev_context) udev_monitor.filter_by(subsystem='hid') self._udev_observer = MonitorObserver(udev_monitor, callback=self._udev_input_event, name='device-monitor') def _init_screensaver_monitor(self): try: self._screensaver_monitor = ScreensaverMonitor(self) self._screensaver_monitor.monitoring = self._config.getboolean( 'Startup', 'devices_off_on_screensaver') except dbus.exceptions.DBusException as e: self.logger.error( "Failed to init ScreensaverMonitor: {}".format(e)) def _init_signals(self): """ Heinous hack to properly handle signals on the mainloop. Necessary if we want to use the mainloop run() functionality. """ def signal_action(signum): """ Action to take when a signal is trapped """ self.quit(signum) def idle_handler(): """ GLib idle handler to propagate signals """ GLib.idle_add(signal_action, priority=GLib.PRIORITY_HIGH) def handler(*args): """ Unix signal handler """ signal_action(args[0]) def install_glib_handler(sig): """ Choose a compatible method and install the handler """ unix_signal_add = None if hasattr(GLib, "unix_signal_add"): unix_signal_add = GLib.unix_signal_add elif hasattr(GLib, "unix_signal_add_full"): unix_signal_add = GLib.unix_signal_add_full if unix_signal_add: unix_signal_add(GLib.PRIORITY_HIGH, sig, handler, sig) else: print("Can't install GLib signal handler!") for sig in signal.SIGINT, signal.SIGTERM, signal.SIGHUP: signal.signal(sig, idle_handler) GLib.idle_add(install_glib_handler, sig, priority=GLib.PRIORITY_HIGH) def read_config(self, config_file): """ Read in the config file and set the defaults :param config_file: Config file :type config_file: str or None """ # Generate sections as trying to access a value even if a default exists will die if the section does not for section in ('General', 'Startup', 'Statistics'): self._config[section] = {} self._config['DEFAULT'] = { 'verbose_logging': True, 'sync_effects_enabled': True, 'devices_off_on_screensaver': True, 'key_statistics': False, } if config_file is not None and os.path.exists(config_file): self._config.read(config_file) def get_off_on_screensaver(self): """ Returns if turn off on screensaver :return: Result :rtype: bool """ return self._screensaver_monitor.monitoring def enable_turn_off_on_screensaver(self, enable): """ Enable the turning off of devices when the screensaver is active """ self._screensaver_monitor.monitoring = enable def supported_devices(self): result = { cls.__name__: (cls.USB_VID, cls.USB_PID) for cls in self._device_classes } return json.dumps(result) def version(self): """ Get the daemon version :return: Version string :rtype: str """ return __version__ def suspend_devices(self): """ Suspend all devices """ for device in self._razer_devices: device.dbus.suspend_device() def resume_devices(self): """ Resume all devices """ for device in self._razer_devices: device.dbus.resume_device() def get_serial_list(self): """ Get list of devices serials """ serial_list = self._razer_devices.serials() self.logger.debug('DBus called get_serial_list') return serial_list def sync_effects(self, enabled): """ Sync the effects across the devices :param enabled: True to sync effects :type enabled: bool """ # Todo perhaps move logic to device collection for device in self._razer_devices.devices: device.dbus.effect_sync = enabled def get_sync_effects(self): """ Sync the effects across the devices :return: True if any devices sync effects :rtype: bool """ result = False for device in self._razer_devices.devices: result |= device.dbus.effect_sync return result def _load_devices(self, first_run=False): """ Go through supported devices and load them Loops through the available hardware classes, loops through each device in the system and adds it if needs be. """ if first_run: # Just some pretty output max_name_len = max( [len(cls.__name__) for cls in self._device_classes]) + 2 for cls in self._device_classes: format_str = 'Loaded device specification: {0:-<' + str( max_name_len) + '} ({1:04x}:{2:04X})' self.logger.debug( format_str.format(cls.__name__ + ' ', cls.USB_VID, cls.USB_PID)) if self._test_dir is not None: device_list = os.listdir(self._test_dir) test_mode = True else: device_list = list( self._udev_context.list_devices(subsystem='hid')) test_mode = False device_number = 0 for device in device_list: for device_class in self._device_classes: # Interoperability between generic list of 0000:0000:0000.0000 and pyudev if test_mode: sys_name = device sys_path = os.path.join(self._test_dir, device) else: sys_name = device.sys_name sys_path = device.sys_path if sys_name in self._razer_devices: continue if device_class.match( sys_name, sys_path ): # Check it matches sys/ ID format and has device_type file self.logger.info('Found device.%d: %s', device_number, sys_name) # TODO add testdir support # Basically find the other usb interfaces device_match = sys_name.split('.')[0] additional_interfaces = [] if not test_mode: for alt_device in device_list: if device_match in alt_device.sys_name and alt_device.sys_name != sys_name: additional_interfaces.append( alt_device.sys_path) # Checking permissions test_file = os.path.join(sys_path, 'device_type') file_group_id = os.stat(test_file).st_gid file_group_name = grp.getgrgid(file_group_id)[0] if os.getgid( ) != file_group_id and file_group_name != 'plugdev': self.logger.critical( "Could not access {0}/device_type, file is not owned by plugdev" .format(sys_path)) break razer_device = device_class( sys_path, device_number, self._config, testing=self._test_dir is not None, additional_interfaces=sorted(additional_interfaces)) # Wireless devices sometimes don't listen count = 0 while count < 3: # Loop to get serial, exit early if it gets one device_serial = razer_device.get_serial() if len(device_serial) > 0: break time.sleep(0.1) count += 1 else: logging.warning( "Could not get serial for device {0}. Skipping". format(sys_name)) continue self._razer_devices.add(sys_name, device_serial, razer_device) device_number += 1 def _add_device(self, device): """ Add device event from udev :param device: Udev Device :type device: pyudev.device._device.Device """ device_number = len(self._razer_devices) for device_class in self._device_classes: sys_name = device.sys_name sys_path = device.sys_path if sys_name in self._razer_devices: continue if device_class.match( sys_name, sys_path ): # Check it matches sys/ ID format and has device_type file self.logger.info('Found valid device.%d: %s', device_number, sys_name) razer_device = device_class(sys_path, device_number, self._config, testing=self._test_dir is not None) # Its a udev event so currently the device hasn't been chmodded yet time.sleep(0.2) # Wireless devices sometimes don't listen device_serial = razer_device.get_serial() if len(device_serial) > 0: # Add Device self._razer_devices.add(sys_name, device_serial, razer_device) self.device_added() else: logging.warning( "Could not get serial for device {0}. Skipping".format( sys_name)) else: # Basically find the other usb interfaces device_match = sys_name.split('.')[0] for d in self._razer_devices: if device_match in d.device_id and d.device_id != sys_name: if not sys_path in d.dbus.additional_interfaces: d.dbus.additional_interfaces.append(sys_path) return def _remove_device(self, device): """ Remove device event from udev :param device: Udev Device :type device: pyudev.device._device.Device """ device_id = device.sys_name try: device = self._razer_devices[device_id] device.dbus.close() device.dbus.remove_from_connection() self.logger.warning("Removing %s", device_id) # Delete device del self._razer_devices[device.device_id] self.device_removed() except IndexError: # Why didn't i set it up as KeyError # It will return "extra" events for the additional usb interfaces bound to the driver pass def _udev_input_event(self, device): """ Function called by the Udev monitor (#observerPattern) :param device: Udev device :type device: pyudev.device._device.Device """ self.logger.debug('Device event [%s]: %s', device.action, device.device_path) if device.action == 'add': if self._collecting_udev: self._collecting_udev_devices.append(device) return else: self._collecting_udev_devices = [device] self._collecting_udev = True t = threading.Thread(target=self._collecting_udev_method, args=(device, )) t.start() elif device.action == 'remove': self._remove_device(device) def _collecting_udev_method(self, device): time.sleep(2) # delay to let udev add all devices that we want # Sort the devices self._collecting_udev_devices.sort(key=lambda x: x.sys_path, reverse=True) for d in self._collecting_udev_devices: self._add_device(d) self._collecting_udev = False def run(self): """ Run the daemon """ self.logger.info('Serving DBus') # Start listening for device changes self._udev_observer.start() # Start the mainloop try: self._main_loop.run() except KeyboardInterrupt: self.logger.debug('Shutting down') def stop(self): """ Wrapper for quit """ self.quit(None) def quit(self, signum): """ Quit by stopping the main loop, observer, and screensaver thread """ # pylint: disable=unused-argument if signum is None: self.logger.info('Stopping daemon.') else: self.logger.info('Stopping daemon on signal %d', signum) self._main_loop.quit() # Stop udev monitor self._udev_observer.send_stop() for device in self._razer_devices: device.dbus.close()
def _stop(self): MonitorObserver.send_stop(self)
class RazerDaemon(DBusService): """ Daemon class This class sets up the main run loop which serves DBus messages. The logger is initialised in this module as well as finding and initialising devices. Serves the following functions via DBus * getDevices - Returns a list of serial numbers * enableTurnOffOnScreensaver - Starts/Continues the run loop on the screensaver thread * disableTurnOffOnScreensaver - Pauses the run loop on the screensaver thread """ BUS_PATH = 'org.razer' def __init__(self, verbose=False, log_dir=None, console_log=False, run_dir=None, config_file=None, test_dir=None): # Check if process exists exit_code = subprocess.call(['pgrep', 'razer-service'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) if exit_code == 0: print("Daemon already exists. Please stop that one.", file=sys.stderr) exit(-1) setproctitle.setproctitle('razer-service') # Expanding ~ as python doesnt do it by default, also creating dirs if needed if log_dir is not None: log_dir = os.path.expanduser(log_dir) os.makedirs(log_dir, mode=0o750, exist_ok=True) if run_dir is not None: run_dir = os.path.expanduser(run_dir) os.makedirs(run_dir, mode=0o750, exist_ok=True) if config_file is not None: config_file = os.path.expanduser(config_file) os.makedirs(os.path.dirname(config_file), mode=0o750, exist_ok=True) self._test_dir = test_dir self._data_dir = run_dir self._config_file = config_file self._config = configparser.ConfigParser() self.read_config(config_file) # Setup DBus to use gobject main loop dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) DBusService.__init__(self, self.BUS_PATH, '/org/razer') self._init_signals() self._main_loop = GObject.MainLoop() # Listen for input events from udev self._udev_context = Context() udev_monitor = Monitor.from_netlink(self._udev_context) udev_monitor.filter_by(subsystem='input') self._udev_observer = MonitorObserver(udev_monitor, callback=self._udev_input_event, name='device-monitor') # Logging logging_level = logging.INFO if verbose or self._config.getboolean('General', 'verbose_logging'): logging_level = logging.DEBUG self.logger = logging.getLogger('razer') self.logger.setLevel(logging_level) formatter = logging.Formatter( '%(asctime)s | %(name)-30s | %(levelname)-8s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S') # Dont propagate to default logger self.logger.propagate = 0 if console_log: console_logger = logging.StreamHandler() console_logger.setLevel(logging_level) console_logger.setFormatter(formatter) self.logger.addHandler(console_logger) if log_dir is not None: log_file = os.path.join(log_dir, 'razer.log') file_logger = logging.handlers.RotatingFileHandler( log_file, maxBytes=16777216, backupCount=10) # 16MiB file_logger.setLevel(logging_level) file_logger.setFormatter(formatter) self.logger.addHandler(file_logger) self.logger.info("Initialising Daemon (v%s). Pid: %d", __version__, os.getpid()) # Setup screensaver thread self._screensaver_thread = ScreensaverThread( self, active=self._config.getboolean('Startup', 'devices_off_on_screensaver')) self._screensaver_thread.start() self._razer_devices = DeviceCollection() self._load_devices(first_run=True) # Add DBus methods self.logger.info("Adding razer.devices.getDevices method to DBus") self.add_dbus_method('razer.devices', 'getDevices', self.get_serial_list, out_signature='as') self.logger.info( "Adding razer.devices.enableTurnOffOnScreensaver method to DBus") self.add_dbus_method('razer.devices', 'enableTurnOffOnScreensaver', self.enable_turn_off_on_screensaver) self.logger.info( "Adding razer.devices.disableTurnOffOnScreensaver method to DBus") self.add_dbus_method('razer.devices', 'disableTurnOffOnScreensaver', self.disable_turn_off_on_screensaver) self.logger.info("Adding razer.devices.syncEffects method to DBus") self.add_dbus_method('razer.devices', 'syncEffects', self.sync_effects, in_signature='b') self.logger.info("Adding razer.daemon.version method to DBus") self.add_dbus_method('razer.daemon', 'version', self.version, out_signature='s') self.logger.info("Adding razer.daemon.stop method to DBus") self.add_dbus_method('razer.daemon', 'stop', self.stop) # TODO remove self.sync_effects( self._config.getboolean('Startup', 'sync_effects_enabled')) # TODO ====== def _init_signals(self): """ Heinous hack to properly handle signals on the mainloop. Necessary if we want to use the mainloop run() functionality. """ def signal_action(signum): """ Action to take when a signal is trapped """ self.quit(signum) def idle_handler(): """ GLib idle handler to propagate signals """ GLib.idle_add(signal_action, priority=GLib.PRIORITY_HIGH) def handler(*args): """ Unix signal handler """ signal_action(args[0]) def install_glib_handler(sig): """ Choose a compatible method and install the handler """ unix_signal_add = None if hasattr(GLib, "unix_signal_add"): unix_signal_add = GLib.unix_signal_add elif hasattr(GLib, "unix_signal_add_full"): unix_signal_add = GLib.unix_signal_add_full if unix_signal_add: unix_signal_add(GLib.PRIORITY_HIGH, sig, handler, sig) else: print("Can't install GLib signal handler!") for sig in signal.SIGINT, signal.SIGTERM, signal.SIGHUP: signal.signal(sig, idle_handler) GLib.idle_add(install_glib_handler, sig, priority=GLib.PRIORITY_HIGH) def read_config(self, config_file): """ Read in the config file and set the defaults :param config_file: Config file :type config_file: str or None """ # Generate sections as trying to access a value even if a default exists will die if the section does not for section in ('General', 'Startup', 'Statistics'): self._config[section] = {} self._config['DEFAULT'] = { 'verbose_logging': True, 'sync_effects_enabled': True, 'devices_off_on_screensaver': True, 'key_statistics': False, } if config_file is not None and os.path.exists(config_file): self._config.read(config_file) def enable_turn_off_on_screensaver(self): """ Enable the turning off of devices when the screensaver is active """ self._screensaver_thread.active = True def disable_turn_off_on_screensaver(self): """ Disable the turning off of devices when the screensaver is active """ self._screensaver_thread.active = False def version(self): """ Get the daemon version :return: Version string :rtype: str """ return __version__ def suspend_devices(self): """ Suspend all devices """ for device in self._razer_devices: device.dbus.suspend_device() def resume_devices(self): """ Resume all devices """ for device in self._razer_devices: device.dbus.resume_device() def get_serial_list(self): """ Get list of devices serials """ serial_list = self._razer_devices.serials() self.logger.debug('DBus called get_serial_list') return serial_list def sync_effects(self, enabled): """ Sync the effects across the devices :param enabled: True to sync effects :type enabled: bool """ # Todo perhaps move logic to device collection for device in self._razer_devices.devices: device.dbus.effect_sync = enabled def _load_devices(self, first_run=False): """ Go through supported devices and load them Loops through the available hardware classes, loops through each device in the system and adds it if needs be. """ classes = razer_daemon.hardware.get_device_classes() if first_run: # Just some pretty output max_name_len = max([len(cls.__name__) for cls in classes]) + 2 for cls in classes: format_str = 'Loaded device specification: {0:-<' + str( max_name_len) + '} ({1:04x}:{2:04X})' self.logger.debug( format_str.format(cls.__name__ + ' ', cls.USB_VID, cls.USB_PID)) for device in self._udev_context.list_devices(subsystem='hid'): device_number = 0 for device_class in classes: if device.sys_name in self._razer_devices: continue if device_class.match( device.sys_name, device.parent.sys_path ): # Check it matches sys/ ID format and has device_type file self.logger.info('Found device.%d: %s', device_number, device.sys_name) razer_device = device_class(device.sys_path, device_number, self._config, testing=self._test_dir is not None) # Wireless devices sometimes dont listen count = 0 while count < 3: # Loop to get serial, exit early if it gets one device_serial = razer_device.get_serial() if len(device_serial) > 0: break count += 1 else: logging.warning( "Could not get serial for device {0}. Skipping". format(device.sys_name)) continue self._razer_devices.add(device.sys_name, device_serial, razer_device) device_number += 1 def _remove_devices(self): """ Go through the list of current devices and if they no longer exist then remove them """ hid_devices = [ dev.sys_name for dev in self._udev_context.list_devices(subsystem='hid') ] devices_to_remove = [ dev for dev in self._razer_devices if dev not in hid_devices ] for device in devices_to_remove: if self._test_dir is not None: # Remove from DBus device.dbus.remove_from_connection() # Remove device self.logger.warning("Device %s is missing. Removing from DBus", device.device_id) del self._razer_devices[device.device_id] def _udev_input_event(self, device): self.logger.debug('Device event [%s]: %s', device.action, device.device_path) if device.action == 'add': self._load_devices() elif device.action == 'remove': self._remove_devices() def run(self): """ Run the daemon """ self.logger.info('Serving DBus') # Start listening for device changes self._udev_observer.start() # Start the mainloop try: self._main_loop.run() except KeyboardInterrupt: self.logger.debug('Shutting down') def stop(self): """ Wrapper for quit """ self.quit(None) def quit(self, signum): """ Quit by stopping the main loop, observer, and screensaver thread """ # pylint: disable=unused-argument if signum is None: self.logger.info('Stopping daemon.') else: self.logger.info('Stopping daemon on signal %d', signum) self._main_loop.quit() # Stop udev monitor self._udev_observer.send_stop() # Stop screensaver self._screensaver_thread.shutdown = True self._screensaver_thread.join(timeout=2) if self._screensaver_thread.is_alive(): self.logger.warning('Could not stop the screensaver thread') for device in self._razer_devices: device.dbus.close()
class RazerDaemon(DBusService): """ Daemon class This class sets up the main run loop which serves DBus messages. The logger is initialised in this module as well as finding and initialising devices. Serves the following functions via DBus * getDevices - Returns a list of serial numbers * enableTurnOffOnScreensaver - Starts/Continues the run loop on the screensaver thread * disableTurnOffOnScreensaver - Pauses the run loop on the screensaver thread """ BUS_PATH = 'org.razer' def __init__(self, verbose=False, log_dir=None, console_log=False, run_dir=None, config_file=None, test_dir=None): # Check if process exists exit_code = subprocess.call(['pgrep', 'razer-service'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) if exit_code == 0: print("Daemon already exists. Please stop that one.", file=sys.stderr) exit(-1) setproctitle.setproctitle('razer-service') # Expanding ~ as python doesnt do it by default, also creating dirs if needed if log_dir is not None: log_dir = os.path.expanduser(log_dir) os.makedirs(log_dir, mode=0o750, exist_ok=True) if run_dir is not None: run_dir = os.path.expanduser(run_dir) os.makedirs(run_dir, mode=0o750, exist_ok=True) if config_file is not None: config_file = os.path.expanduser(config_file) os.makedirs(os.path.dirname(config_file), mode=0o750, exist_ok=True) self._test_dir = test_dir self._data_dir = run_dir self._config_file = config_file self._config = configparser.ConfigParser() self.read_config(config_file) # Setup DBus to use gobject main loop dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) DBusService.__init__(self, self.BUS_PATH, '/org/razer') self._init_signals() self._main_loop = GObject.MainLoop() # Listen for input events from udev self._udev_context = Context() udev_monitor = Monitor.from_netlink(self._udev_context) udev_monitor.filter_by(subsystem='input') self._udev_observer = MonitorObserver(udev_monitor, callback=self._udev_input_event, name='device-monitor') # Logging logging_level = logging.INFO if verbose or self._config.getboolean('General', 'verbose_logging'): logging_level = logging.DEBUG self.logger = logging.getLogger('razer') self.logger.setLevel(logging_level) formatter = logging.Formatter('%(asctime)s | %(name)-30s | %(levelname)-8s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S') # Dont propagate to default logger self.logger.propagate = 0 if console_log: console_logger = logging.StreamHandler() console_logger.setLevel(logging_level) console_logger.setFormatter(formatter) self.logger.addHandler(console_logger) if log_dir is not None: log_file = os.path.join(log_dir, 'razer.log') file_logger = logging.handlers.RotatingFileHandler(log_file, maxBytes=16777216, backupCount=10) # 16MiB file_logger.setLevel(logging_level) file_logger.setFormatter(formatter) self.logger.addHandler(file_logger) self.logger.info("Initialising Daemon (v%s). Pid: %d", __version__, os.getpid()) # Setup screensaver thread self._screensaver_thread = ScreensaverThread(self, active=self._config.getboolean('Startup', 'devices_off_on_screensaver')) self._screensaver_thread.start() self._razer_devices = DeviceCollection() self._load_devices(first_run=True) # Add DBus methods self.logger.info("Adding razer.devices.getDevices method to DBus") self.add_dbus_method('razer.devices', 'getDevices', self.get_serial_list, out_signature='as') self.logger.info("Adding razer.devices.enableTurnOffOnScreensaver method to DBus") self.add_dbus_method('razer.devices', 'enableTurnOffOnScreensaver', self.enable_turn_off_on_screensaver) self.logger.info("Adding razer.devices.disableTurnOffOnScreensaver method to DBus") self.add_dbus_method('razer.devices', 'disableTurnOffOnScreensaver', self.disable_turn_off_on_screensaver) self.logger.info("Adding razer.devices.syncEffects method to DBus") self.add_dbus_method('razer.devices', 'syncEffects', self.sync_effects, in_signature='b') self.logger.info("Adding razer.daemon.version method to DBus") self.add_dbus_method('razer.daemon', 'version', self.version, out_signature='s') self.logger.info("Adding razer.daemon.stop method to DBus") self.add_dbus_method('razer.daemon', 'stop', self.stop) # TODO remove self.sync_effects(self._config.getboolean('Startup', 'sync_effects_enabled')) # TODO ====== def _init_signals(self): """ Heinous hack to properly handle signals on the mainloop. Necessary if we want to use the mainloop run() functionality. """ def signal_action(signum): """ Action to take when a signal is trapped """ self.quit(signum) def idle_handler(): """ GLib idle handler to propagate signals """ GLib.idle_add(signal_action, priority=GLib.PRIORITY_HIGH) def handler(*args): """ Unix signal handler """ signal_action(args[0]) def install_glib_handler(sig): """ Choose a compatible method and install the handler """ unix_signal_add = None if hasattr(GLib, "unix_signal_add"): unix_signal_add = GLib.unix_signal_add elif hasattr(GLib, "unix_signal_add_full"): unix_signal_add = GLib.unix_signal_add_full if unix_signal_add: unix_signal_add(GLib.PRIORITY_HIGH, sig, handler, sig) else: print("Can't install GLib signal handler!") for sig in signal.SIGINT, signal.SIGTERM, signal.SIGHUP: signal.signal(sig, idle_handler) GLib.idle_add(install_glib_handler, sig, priority=GLib.PRIORITY_HIGH) def read_config(self, config_file): """ Read in the config file and set the defaults :param config_file: Config file :type config_file: str or None """ # Generate sections as trying to access a value even if a default exists will die if the section does not for section in ('General', 'Startup', 'Statistics'): self._config[section] = {} self._config['DEFAULT'] = { 'verbose_logging': True, 'sync_effects_enabled': True, 'devices_off_on_screensaver': True, 'key_statistics': False, } if config_file is not None and os.path.exists(config_file): self._config.read(config_file) def enable_turn_off_on_screensaver(self): """ Enable the turning off of devices when the screensaver is active """ self._screensaver_thread.active = True def disable_turn_off_on_screensaver(self): """ Disable the turning off of devices when the screensaver is active """ self._screensaver_thread.active = False def version(self): """ Get the daemon version :return: Version string :rtype: str """ return __version__ def suspend_devices(self): """ Suspend all devices """ for device in self._razer_devices: device.dbus.suspend_device() def resume_devices(self): """ Resume all devices """ for device in self._razer_devices: device.dbus.resume_device() def get_serial_list(self): """ Get list of devices serials """ serial_list = self._razer_devices.serials() self.logger.debug('DBus called get_serial_list') return serial_list def sync_effects(self, enabled): """ Sync the effects across the devices :param enabled: True to sync effects :type enabled: bool """ # Todo perhaps move logic to device collection for device in self._razer_devices.devices: device.dbus.effect_sync = enabled def _load_devices(self, first_run=False): """ Go through supported devices and load them Loops through the available hardware classes, loops through each device in the system and adds it if needs be. """ classes = razer_daemon.hardware.get_device_classes() if first_run: # Just some pretty output max_name_len = max([len(cls.__name__) for cls in classes]) + 2 for cls in classes: format_str = 'Loaded device specification: {0:-<' + str(max_name_len) + '} ({1:04x}:{2:04X})' self.logger.debug(format_str.format(cls.__name__ + ' ', cls.USB_VID, cls.USB_PID)) for device in self._udev_context.list_devices(subsystem='hid'): device_number = 0 for device_class in classes: if device.sys_name in self._razer_devices: continue if device_class.match(device.sys_name, device.parent.sys_path): # Check it matches sys/ ID format and has device_type file self.logger.info('Found device.%d: %s', device_number, device.sys_name) razer_device = device_class(device.sys_path, device_number, self._config, testing=self._test_dir is not None) # Wireless devices sometimes dont listen count = 0 while count < 3: # Loop to get serial, exit early if it gets one device_serial = razer_device.get_serial() if len(device_serial) > 0: break count += 1 else: logging.warning("Could not get serial for device {0}. Skipping".format(device.sys_name)) continue self._razer_devices.add(device.sys_name, device_serial, razer_device) device_number += 1 def _remove_devices(self): """ Go through the list of current devices and if they no longer exist then remove them """ hid_devices = [dev.sys_name for dev in self._udev_context.list_devices(subsystem='hid')] devices_to_remove = [dev for dev in self._razer_devices if dev not in hid_devices] for device in devices_to_remove: if self._test_dir is not None: # Remove from DBus device.dbus.remove_from_connection() # Remove device self.logger.warning("Device %s is missing. Removing from DBus", device.device_id) del self._razer_devices[device.device_id] def _udev_input_event(self, device): self.logger.debug('Device event [%s]: %s', device.action, device.device_path) if device.action == 'add': self._load_devices() elif device.action == 'remove': self._remove_devices() def run(self): """ Run the daemon """ self.logger.info('Serving DBus') # Start listening for device changes self._udev_observer.start() # Start the mainloop try: self._main_loop.run() except KeyboardInterrupt: self.logger.debug('Shutting down') def stop(self): """ Wrapper for quit """ self.quit(None) def quit(self, signum): """ Quit by stopping the main loop, observer, and screensaver thread """ # pylint: disable=unused-argument if signum is None: self.logger.info('Stopping daemon.') else: self.logger.info('Stopping daemon on signal %d', signum) self._main_loop.quit() # Stop udev monitor self._udev_observer.send_stop() # Stop screensaver self._screensaver_thread.shutdown = True self._screensaver_thread.join(timeout=2) if self._screensaver_thread.is_alive(): self.logger.warning('Could not stop the screensaver thread') for device in self._razer_devices: device.dbus.close()
class RazerDaemon(DBusService): """ Daemon class This class sets up the main run loop which serves DBus messages. The logger is initialised in this module as well as finding and initialising devices. Serves the following functions via DBus * getDevices - Returns a list of serial numbers * enableTurnOffOnScreensaver - Starts/Continues the run loop on the screensaver thread * disableTurnOffOnScreensaver - Pauses the run loop on the screensaver thread """ BUS_NAME = 'org.razer' def __init__(self, verbose=False, log_dir=None, console_log=False, run_dir=None, config_file=None, test_dir=None): setproctitle.setproctitle('openrazer-daemon') # pylint: disable=no-member # Expanding ~ as python doesn't do it by default, also creating dirs if needed try: if log_dir is not None: log_dir = os.path.expanduser(log_dir) os.makedirs(log_dir, exist_ok=True) if run_dir is not None: run_dir = os.path.expanduser(run_dir) os.makedirs(run_dir, exist_ok=True) except NotADirectoryError as e: print("Failed to create {}".format(e.filename), file=sys.stderr) sys.exit(1) if config_file is not None: config_file = os.path.expanduser(config_file) if not os.path.exists(config_file): print("Config file {} does not exist.".format(config_file), file=sys.stderr) sys.exit(1) self._test_dir = test_dir self._run_dir = run_dir self._config_file = config_file self._config = configparser.ConfigParser() self.read_config(config_file) # Logging log_level = logging.INFO if verbose or self._config.getboolean('General', 'verbose_logging'): log_level = logging.DEBUG self.logger = self._create_logger(log_dir, log_level, console_log) # Check for plugdev group if not self._check_plugdev_group(): self.logger.critical("User is not a member of the plugdev group") sys.exit(1) # Setup DBus to use gobject main loop dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) DBusService.__init__(self, self.BUS_NAME, '/org/razer') self._init_signals() self._main_loop = GLib.MainLoop() # Listen for input events from udev self._init_udev_monitor() # Load Classes self._device_classes = openrazer_daemon.hardware.get_device_classes() self.logger.info("Initialising Daemon (v%s). Pid: %d", __version__, os.getpid()) self._init_screensaver_monitor() self._razer_devices = DeviceCollection() self._load_devices(first_run=True) # Add DBus methods methods = { # interface, method, callback, in-args, out-args ('razer.devices', 'getDevices', self.get_serial_list, None, 'as'), ('razer.devices', 'supportedDevices', self.supported_devices, None, 's'), ('razer.devices', 'enableTurnOffOnScreensaver', self.enable_turn_off_on_screensaver, 'b', None), ('razer.devices', 'getOffOnScreensaver', self.get_off_on_screensaver, None, 'b'), ('razer.devices', 'syncEffects', self.sync_effects, 'b', None), ('razer.devices', 'getSyncEffects', self.get_sync_effects, None, 'b'), ('razer.daemon', 'version', self.version, None, 's'), ('razer.daemon', 'stop', self.stop, None, None), } for m in methods: self.logger.debug("Adding {}.{} method to DBus".format(m[0], m[1])) self.add_dbus_method(m[0], m[1], m[2], in_signature=m[3], out_signature=m[4]) self._collecting_udev = False self._collecting_udev_devices = [] # TODO remove self.sync_effects(self._config.getboolean('Startup', 'sync_effects_enabled')) # TODO ====== @dbus.service.signal('razer.devices') def device_removed(self): self.logger.debug("Emitted Device Remove Signal") @dbus.service.signal('razer.devices') def device_added(self): self.logger.debug("Emitted Device Added Signal") def _create_logger(self, log_dir, log_level, want_console_log): """ Initializes a logger and returns it. :param log_dir: If not None, specifies the directory to create the log file in :param log_level: The log level of messages to print :param want_console_log: True if we should print to the console :rtype:logging.Logger """ logger = logging.getLogger('razer') logger.setLevel(log_level) formatter = logging.Formatter('%(asctime)s | %(name)-30s | %(levelname)-8s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S') # Don't propagate to default logger logger.propagate = 0 if want_console_log: console_logger = logging.StreamHandler() console_logger.setLevel(log_level) console_logger.setFormatter(formatter) logger.addHandler(console_logger) if log_dir is not None: log_file = os.path.join(log_dir, 'razer.log') file_logger = logging.handlers.RotatingFileHandler(log_file, maxBytes=16777216, backupCount=10) # 16MiB file_logger.setLevel(log_level) file_logger.setFormatter(formatter) logger.addHandler(file_logger) return logger def _check_plugdev_group(self): """ Check if the user is a member of the plugdev group. For the root user, this always returns True :rtype: bool """ if getpass.getuser() == 'root': return True try: return grp.getgrnam('plugdev').gr_gid in os.getgroups() except KeyError: pass return False def _init_udev_monitor(self): self._udev_context = Context() udev_monitor = Monitor.from_netlink(self._udev_context) udev_monitor.filter_by(subsystem='hid') self._udev_observer = MonitorObserver(udev_monitor, callback=self._udev_input_event, name='device-monitor') def _init_screensaver_monitor(self): try: self._screensaver_monitor = ScreensaverMonitor(self) self._screensaver_monitor.monitoring = self._config.getboolean('Startup', 'devices_off_on_screensaver') except dbus.exceptions.DBusException as e: self.logger.error("Failed to init ScreensaverMonitor: {}".format(e)) def _init_signals(self): """ Heinous hack to properly handle signals on the mainloop. Necessary if we want to use the mainloop run() functionality. """ def signal_action(signum): """ Action to take when a signal is trapped """ self.quit(signum) def idle_handler(): """ GLib idle handler to propagate signals """ GLib.idle_add(signal_action, priority=GLib.PRIORITY_HIGH) def handler(*args): """ Unix signal handler """ signal_action(args[0]) def install_glib_handler(sig): """ Choose a compatible method and install the handler """ unix_signal_add = None if hasattr(GLib, "unix_signal_add"): unix_signal_add = GLib.unix_signal_add elif hasattr(GLib, "unix_signal_add_full"): unix_signal_add = GLib.unix_signal_add_full if unix_signal_add: unix_signal_add(GLib.PRIORITY_HIGH, sig, handler, sig) else: print("Can't install GLib signal handler!") for sig in signal.SIGINT, signal.SIGTERM, signal.SIGHUP: signal.signal(sig, idle_handler) GLib.idle_add(install_glib_handler, sig, priority=GLib.PRIORITY_HIGH) def read_config(self, config_file): """ Read in the config file and set the defaults :param config_file: Config file :type config_file: str or None """ # Generate sections as trying to access a value even if a default exists will die if the section does not for section in ('General', 'Startup', 'Statistics'): self._config[section] = {} self._config['DEFAULT'] = { 'verbose_logging': True, 'sync_effects_enabled': True, 'devices_off_on_screensaver': True, 'key_statistics': False, } if config_file is not None and os.path.exists(config_file): self._config.read(config_file) def get_off_on_screensaver(self): """ Returns if turn off on screensaver :return: Result :rtype: bool """ return self._screensaver_monitor.monitoring def enable_turn_off_on_screensaver(self, enable): """ Enable the turning off of devices when the screensaver is active """ self._screensaver_monitor.monitoring = enable def supported_devices(self): result = {cls.__name__: (cls.USB_VID, cls.USB_PID) for cls in self._device_classes} return json.dumps(result) def version(self): """ Get the daemon version :return: Version string :rtype: str """ return __version__ def suspend_devices(self): """ Suspend all devices """ for device in self._razer_devices: device.dbus.suspend_device() def resume_devices(self): """ Resume all devices """ for device in self._razer_devices: device.dbus.resume_device() def get_serial_list(self): """ Get list of devices serials """ serial_list = self._razer_devices.serials() self.logger.debug('DBus called get_serial_list') return serial_list def sync_effects(self, enabled): """ Sync the effects across the devices :param enabled: True to sync effects :type enabled: bool """ # Todo perhaps move logic to device collection for device in self._razer_devices.devices: device.dbus.effect_sync = enabled def get_sync_effects(self): """ Sync the effects across the devices :return: True if any devices sync effects :rtype: bool """ result = False for device in self._razer_devices.devices: result |= device.dbus.effect_sync return result def _load_devices(self, first_run=False): """ Go through supported devices and load them Loops through the available hardware classes, loops through each device in the system and adds it if needs be. """ if first_run: # Just some pretty output max_name_len = max([len(cls.__name__) for cls in self._device_classes]) + 2 for cls in self._device_classes: format_str = 'Loaded device specification: {0:-<' + str(max_name_len) + '} ({1:04x}:{2:04X})' self.logger.debug(format_str.format(cls.__name__ + ' ', cls.USB_VID, cls.USB_PID)) if self._test_dir is not None: device_list = os.listdir(self._test_dir) test_mode = True else: device_list = list(self._udev_context.list_devices(subsystem='hid')) test_mode = False device_number = 0 for device in device_list: for device_class in self._device_classes: # Interoperability between generic list of 0000:0000:0000.0000 and pyudev if test_mode: sys_name = device sys_path = os.path.join(self._test_dir, device) else: sys_name = device.sys_name sys_path = device.sys_path if sys_name in self._razer_devices: continue if device_class.match(sys_name, sys_path): # Check it matches sys/ ID format and has device_type file self.logger.info('Found device.%d: %s', device_number, sys_name) # TODO add testdir support # Basically find the other usb interfaces device_match = sys_name.split('.')[0] additional_interfaces = [] if not test_mode: for alt_device in device_list: if device_match in alt_device.sys_name and alt_device.sys_name != sys_name: additional_interfaces.append(alt_device.sys_path) # Checking permissions test_file = os.path.join(sys_path, 'device_type') file_group_id = os.stat(test_file).st_gid file_group_name = grp.getgrgid(file_group_id)[0] if os.getgid() != file_group_id and file_group_name != 'plugdev': self.logger.critical("Could not access {0}/device_type, file is not owned by plugdev".format(sys_path)) break razer_device = device_class(sys_path, device_number, self._config, testing=self._test_dir is not None, additional_interfaces=sorted(additional_interfaces)) # Wireless devices sometimes don't listen count = 0 while count < 3: # Loop to get serial, exit early if it gets one device_serial = razer_device.get_serial() if len(device_serial) > 0: break time.sleep(0.1) count += 1 else: logging.warning("Could not get serial for device {0}. Skipping".format(sys_name)) continue self._razer_devices.add(sys_name, device_serial, razer_device) device_number += 1 def _add_device(self, device): """ Add device event from udev :param device: Udev Device :type device: pyudev.device._device.Device """ device_number = len(self._razer_devices) for device_class in self._device_classes: sys_name = device.sys_name sys_path = device.sys_path if sys_name in self._razer_devices: continue if device_class.match(sys_name, sys_path): # Check it matches sys/ ID format and has device_type file self.logger.info('Found valid device.%d: %s', device_number, sys_name) razer_device = device_class(sys_path, device_number, self._config, testing=self._test_dir is not None) # Its a udev event so currently the device hasn't been chmodded yet time.sleep(0.2) # Wireless devices sometimes don't listen device_serial = razer_device.get_serial() if len(device_serial) > 0: # Add Device self._razer_devices.add(sys_name, device_serial, razer_device) self.device_added() else: logging.warning("Could not get serial for device {0}. Skipping".format(sys_name)) else: # Basically find the other usb interfaces device_match = sys_name.split('.')[0] for d in self._razer_devices: if device_match in d.device_id and d.device_id != sys_name: if not sys_path in d.dbus.additional_interfaces: d.dbus.additional_interfaces.append(sys_path) return def _remove_device(self, device): """ Remove device event from udev :param device: Udev Device :type device: pyudev.device._device.Device """ device_id = device.sys_name try: device = self._razer_devices[device_id] device.dbus.close() device.dbus.remove_from_connection() self.logger.warning("Removing %s", device_id) # Delete device del self._razer_devices[device.device_id] self.device_removed() except IndexError: # Why didn't i set it up as KeyError # It will return "extra" events for the additional usb interfaces bound to the driver pass def _udev_input_event(self, device): """ Function called by the Udev monitor (#observerPattern) :param device: Udev device :type device: pyudev.device._device.Device """ self.logger.debug('Device event [%s]: %s', device.action, device.device_path) if device.action == 'add': if self._collecting_udev: self._collecting_udev_devices.append(device) return else: self._collecting_udev_devices = [device] self._collecting_udev = True t = threading.Thread(target=self._collecting_udev_method, args=(device,)) t.start() elif device.action == 'remove': self._remove_device(device) def _collecting_udev_method(self, device): time.sleep(2) # delay to let udev add all devices that we want # Sort the devices self._collecting_udev_devices.sort(key=lambda x: x.sys_path, reverse=True) for d in self._collecting_udev_devices: self._add_device(d) self._collecting_udev = False def run(self): """ Run the daemon """ self.logger.info('Serving DBus') # Start listening for device changes self._udev_observer.start() # Start the mainloop try: self._main_loop.run() except KeyboardInterrupt: self.logger.debug('Shutting down') def stop(self): """ Wrapper for quit """ self.quit(None) def quit(self, signum): """ Quit by stopping the main loop, observer, and screensaver thread """ # pylint: disable=unused-argument if signum is None: self.logger.info('Stopping daemon.') else: self.logger.info('Stopping daemon on signal %d', signum) self._main_loop.quit() # Stop udev monitor self._udev_observer.send_stop() for device in self._razer_devices: device.dbus.close()
class GC_CModule_UdevMon(GC_CModule): MODULE_ID = 'udevmon' def __init__(self, gcclient): self.gcclient = gcclient logger.debug('Initialize Module: %s', self.MODULE_ID) # Running State self.Running = True # Setup pyudev contect and monitor: # 1 - Ensure the udev context and monitor is setup: self.setupUdevMonitor() # 2 - Get and send the current device list: gccommand = {} gccommand[GC_Utility.GC_MODULEID] = self.MODULE_ID response = self.getCurrentUSBDevices() logger.debug('Sending udev init data...') self.gcclient.sendResult(gccommand, response) # 3 - Start the Udev Monitor: self.startUdevMonitor(callback=self.udevMonitorHandler) def setupUdevMonitor(self, filter_subsystem='usb'): logger.debug('Setting up udev context...') self.udev_context = Context() logger.debug('Setting up monitor context...') self.udev_monitor = Monitor.from_netlink(self.udev_context) logger.debug('Setting up monitor filter...') self.udev_monitor.filter_by(subsystem=filter_subsystem) def startUdevMonitor(self, callback): observer_name = self.MODULE_ID + '-observer' logger.debug('Setting up udev monitor: %s' % observer_name) self.udev_observer = MonitorObserver(self.udev_monitor, callback=callback, name=observer_name) self.udev_observer.start() def udevMonitorHandler(self, device): """ Watch udev event and handle each of them. """ self.parseDeviceObj(device) def parseDeviceObj(self, device): #logger.debug('Udev Device :: PCI Slot Name: %s' % pprint.pformat(device.find_parent('pci')['PCI_SLOT_NAME'])) new_device_obj = {} # Get the device converted to a standard dict: (better for JSON sending) for item in device.items(): new_device_obj[item[0]] = item[1] # Get the tags, if they are there: tags_from_device = list(device.tags) if len(tags_from_device) > 0: new_device_obj['TAGS'] = tags_from_device else: new_device_obj['TAGS'] = [] #logger.debug('Udev Device Dict:\n%s' % pprint.pformat(new_device_obj)) return new_device_obj def getCurrentUSBDevices(self, filter_subsystem='usb'): logger.debug('Getting current USB Devices...') finalObj = {} parsed_devices = [] # Parse all USB Devices: for device in self.udev_context.list_devices( subsystem=filter_subsystem): new_device_obj = self.parseDeviceObj(device) parsed_devices.append(new_device_obj) # Prep for output: finalObj['current_devices'] = parsed_devices #logger.debug('Udev Devices Dict:\n%s' % pprint.pformat(finalObj)) return finalObj # jEnc = json.JSONEncoder(skipkeys=True) # json_blob = jEnc.encode(finalObj) # logger.debug('Udev Devices JSON:\n%s' % pprint.pformat(json_blob)) """ Function: handleTask Description: handle download commands """ def handleTask(self, gccommand): #self.gcclient.log(GC_Utility.DEBUG, 'downloadFile : Sending result...'); logger.debug('Sending result...') self.gcclient.sendResult(gccommand, response) def getModuleId(self): return self.MODULE_ID def quit(self): self.Running = False self.udev_observer.send_stop() return True