Пример #1
0
    def refresh_devices(self, device=None):
        """Keep the devices up to date."""
        now = time.time()
        if now - 10 > self.refreshed_devices_at:
            logger.debug('Refreshing because last info is too old')
            refresh_devices()
            self.refreshed_devices_at = now
            return

        if device is not None:
            if device.startswith('/dev/input/'):
                for group in get_devices().values():
                    if device in group['paths']:
                        break
                else:
                    logger.debug('Refreshing because path unknown')
                    refresh_devices()
                    self.refreshed_devices_at = now
                    return
            else:
                if device not in get_devices():
                    logger.debug('Refreshing because name unknown')
                    refresh_devices()
                    self.refreshed_devices_at = now
                    return
Пример #2
0
    def test_refresh_devices_on_start(self):
        if os.path.exists(get_config_path('xmodmap.json')):
            os.remove(get_config_path('xmodmap.json'))

        ev = (EV_KEY, 9)
        keycode_to = 100
        device = '9876 name'
        # this test only makes sense if this device is unknown yet
        self.assertIsNone(get_devices().get(device))
        custom_mapping.change(Key(*ev, 1), 'a')
        system_mapping.clear()
        system_mapping._set('a', keycode_to)

        # make the daemon load the file instead
        with open(get_config_path('xmodmap.json'), 'w') as file:
            json.dump(system_mapping._mapping, file, indent=4)
        system_mapping.clear()

        preset = 'foo'
        custom_mapping.save(get_preset_path(device, preset))
        config.set_autoload_preset(device, preset)
        push_events(device, [
            new_event(*ev, 1)
        ])
        self.daemon = Daemon()

        # make sure the devices are populated
        get_devices()
        fixtures[self.new_fixture] = {
            'capabilities': {evdev.ecodes.EV_KEY: [ev[1]]},
            'phys': '9876 phys',
            'info': evdev.device.DeviceInfo(4, 5, 6, 7),
            'name': device
        }

        self.daemon.set_config_dir(get_config_path())
        self.daemon.start_injecting(device, preset)

        # test if the injector called refresh_devices successfully
        self.assertIsNotNone(get_devices().get(device))

        time.sleep(0.1)
        self.assertTrue(uinput_write_history_pipe[0].poll())

        event = uinput_write_history_pipe[0].recv()
        self.assertEqual(event.t, (EV_KEY, keycode_to, 1))

        self.daemon.stop_injecting(device)
        self.assertEqual(self.daemon.get_state(device), STOPPED)
Пример #3
0
 def test_get_devices(self):
     pipe = FakePipe()
     _GetDevices(pipe).run()
     self.assertDictEqual(
         pipe.devices, {
             'device 1': {
                 'paths': [
                     '/dev/input/event11', '/dev/input/event10',
                     '/dev/input/event13'
                 ],
                 'devices': ['device 1 foo', 'device 1', 'device 1'],
                 'types': [KEYBOARD, MOUSE]
             },
             'device 2': {
                 'paths': ['/dev/input/event20'],
                 'devices': ['device 2'],
                 'types': [KEYBOARD]
             },
             'gamepad': {
                 'paths': ['/dev/input/event30'],
                 'devices': ['gamepad'],
                 'types': [GAMEPAD]
             },
             'key-mapper device 2': {
                 'paths': ['/dev/input/event40'],
                 'devices': ['key-mapper device 2'],
                 'types': [KEYBOARD]
             },
         })
     self.assertDictEqual(pipe.devices, get_devices(include_keymapper=True))
Пример #4
0
    def initialize_gamepad_config(self):
        """Set slider and dropdown values when a gamepad is selected."""
        devices = get_devices()
        if GAMEPAD in devices[self.selected_device]['types']:
            self.get('gamepad_separator').show()
            self.get('gamepad_config').show()
        else:
            self.get('gamepad_separator').hide()
            self.get('gamepad_config').hide()
            return

        left_purpose = self.get('left_joystick_purpose')
        right_purpose = self.get('right_joystick_purpose')
        speed = self.get('joystick_mouse_speed')

        with HandlerDisabled(left_purpose, self.on_left_joystick_changed):
            value = custom_mapping.get('gamepad.joystick.left_purpose')
            left_purpose.set_active_id(value)

        with HandlerDisabled(right_purpose, self.on_right_joystick_changed):
            value = custom_mapping.get('gamepad.joystick.right_purpose')
            right_purpose.set_active_id(value)

        with HandlerDisabled(speed, self.on_joystick_mouse_speed_changed):
            value = custom_mapping.get('gamepad.joystick.pointer_speed')
            range_value = math.log(value, 2)
            speed.set_value(range_value)
Пример #5
0
def get_any_preset():
    """Return the first found tuple of (device, preset)."""
    devices = get_devices().keys()
    if len(devices) == 0:
        return None, None
    any_device = list(devices)[0]
    any_preset = (get_presets(any_device) or [None])[0]
    return any_device, any_preset
Пример #6
0
    def test_skip_camera(self):
        def list_devices():
            return ['/foo/bar', '/dev/input/event30']

        fixtures['/foo/bar'] = {
            'name': 'camera',
            'phys': 'abcd1',
            'info': evdev.DeviceInfo(1, 2, 3, 4),
            'capabilities': {
                evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_CAMERA]
            }
        }

        with mock.patch('evdev.list_devices', list_devices):
            refresh_devices()
            self.assertNotIn('camera', get_devices())
            self.assertIn('gamepad', get_devices())
Пример #7
0
    def test_device_with_only_ev_abs(self):
        def list_devices():
            return ['/foo/bar', '/dev/input/event30']

        # could be anything, a lot of devices have ABS_X capabilities,
        # so it is not treated as gamepad joystick and since it also
        # doesn't have key capabilities, there is nothing to map.
        fixtures['/foo/bar'] = {
            'name': 'qux',
            'phys': 'abcd2',
            'info': evdev.DeviceInfo(1, 2, 3, 4),
            'capabilities': {
                evdev.ecodes.EV_ABS: [evdev.ecodes.ABS_X]
            }
        }

        with mock.patch('evdev.list_devices', list_devices):
            refresh_devices()
            self.assertIn('gamepad', get_devices())
            self.assertNotIn('qux', get_devices())
Пример #8
0
    def start_injecting(self, device, path, xmodmap_path=None):
        """Start injecting the preset for the device.

        Returns True on success.

        Parameters
        ----------
        device : string
            The name of the device
        path : string
            Path to the preset. The daemon, if started via systemctl, has no
            knowledge of the user and their home path, so the complete
            absolute path needs to be provided here.
        xmodmap_path : string, None
            Path to a dump of the xkb mappings, to provide more human
            readable keys in the correct keyboard layout to the service.
            The service cannot use `xmodmap -pke` because it's running via
            systemd.
        """
        if device not in get_devices():
            logger.debug('Devices possibly outdated, refreshing')
            refresh_devices()

        # reload the config, since it may have been changed
        config.load_config()
        if self.injectors.get(device) is not None:
            self.injectors[device].stop_injecting()

        mapping = Mapping()
        try:
            mapping.load(path)
        except FileNotFoundError as error:
            logger.error(str(error))
            return False

        if xmodmap_path is not None:
            try:
                with open(xmodmap_path, 'r') as file:
                    xmodmap = json.load(file)
                    logger.debug('Using keycodes from "%s"', xmodmap_path)
                    system_mapping.update(xmodmap)
                    # the service now has process wide knowledge of xmodmap
                    # keys of the users session
            except FileNotFoundError:
                logger.error('Could not find "%s"', xmodmap_path)

        try:
            injector = KeycodeInjector(device, mapping)
            injector.start_injecting()
            self.injectors[device] = injector
        except OSError:
            return False

        return True
Пример #9
0
    def test_refresh_devices_on_start(self):
        ev = (EV_KEY, 9)
        keycode_to = 100
        device = '9876 name'
        # this test only makes sense if this device is unknown yet
        self.assertIsNone(get_devices().get(device))
        custom_mapping.change(Key(*ev, 1), 'a')
        system_mapping.clear()
        system_mapping._set('a', keycode_to)
        preset = 'foo'
        custom_mapping.save(get_preset_path(device, preset))
        config.set_autoload_preset(device, preset)
        pending_events[device] = [InputEvent(*ev, 1)]
        self.daemon = Daemon()
        preset_path = get_preset_path(device, preset)

        # make sure the devices are populated
        get_devices()
        fixtures[self.new_fixture] = {
            'capabilities': {
                evdev.ecodes.EV_KEY: [ev[1]]
            },
            'phys': '9876 phys',
            'info': 'abcd',
            'name': device
        }

        self.daemon.start_injecting(device, preset_path)

        # test if the injector called refresh_devices successfully
        self.assertIsNotNone(get_devices().get(device))

        event = uinput_write_history_pipe[0].recv()
        self.assertEqual(event.type, EV_KEY)
        self.assertEqual(event.code, keycode_to)
        self.assertEqual(event.value, 1)

        self.daemon.stop_injecting(device)
        self.assertFalse(self.daemon.is_injecting(device))
Пример #10
0
    def start_injecting(self):
        """Start injecting keycodes."""
        if self.stopped or self._process is not None:
            # So that there is less concern about integrity when putting
            # stuff into self. Each injector object can only be
            # started once.
            raise Exception('Please construct a new injector instead')

        if self.device not in get_devices():
            logger.error('Cannot inject for unknown device "%s"', self.device)
            return

        self._process = multiprocessing.Process(target=self._start_injecting)
        self._process.start()
Пример #11
0
    def test_are_new_devices_available(self):
        self.create_helper()
        set_devices({})

        # read stuff from the helper, which includes the devices
        self.assertFalse(reader.are_new_devices_available())
        reader.read()

        self.assertTrue(reader.are_new_devices_available())
        # a bit weird, but it assumes the gui handled that and returns
        # false afterwards
        self.assertFalse(reader.are_new_devices_available())

        # send the same devices again
        reader._get_event({'type': 'devices', 'message': get_devices()})
        self.assertFalse(reader.are_new_devices_available())

        # send changed devices
        message = {**get_devices()}
        del message['device 1']
        reader._get_event({'type': 'devices', 'message': message})
        self.assertTrue(reader.are_new_devices_available())
        self.assertFalse(reader.are_new_devices_available())
Пример #12
0
    def test_refresh_devices_for_unknown_paths(self):
        device = '9876 name'
        # this test only makes sense if this device is unknown yet
        self.assertIsNone(get_devices().get(device))

        self.daemon = Daemon()

        # make sure the devices are populated
        get_devices()

        self.daemon.refresh_devices()

        fixtures[self.new_fixture] = {
            'capabilities': {evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A]},
            'phys': '9876 phys',
            'info': evdev.device.DeviceInfo(4, 5, 6, 7),
            'name': device
        }

        self.daemon._autoload(self.new_fixture)

        # test if the injector called refresh_devices successfully
        self.assertIsNotNone(get_devices().get(device))
Пример #13
0
def find_newest_preset(device=None):
    """Get a tuple of (device, preset) that was most recently modified
    in the users home directory.

    If no device has been configured yet, return an arbitrary device.

    Parameters
    ----------
    device : string
        If set, will return the newest preset for the device or None
    """
    # sort the oldest files to the front in order to use pop to get the newest
    if device is None:
        paths = sorted(
            glob.glob(os.path.join(get_preset_path(), '*/*.json')),
            key=os.path.getmtime
        )
    else:
        paths = sorted(
            glob.glob(os.path.join(get_preset_path(device), '*.json')),
            key=os.path.getmtime
        )

    if len(paths) == 0:
        logger.debug('No presets found')
        return get_any_preset()

    online_devices = get_devices().keys()

    newest_path = None
    while len(paths) > 0:
        # take the newest path
        path = paths.pop()
        preset = os.path.split(path)[1]
        device = os.path.split(os.path.split(path)[0])[1]
        if device in online_devices:
            newest_path = path
            break

    if newest_path is None:
        logger.debug('None of the configured devices is currently online')
        return get_any_preset()

    preset = os.path.splitext(preset)[0]
    logger.debug('The newest preset is "%s", "%s"', device, preset)

    return device, preset
Пример #14
0
    def _handle_commands(self):
        """Handle all unread commands."""
        # wait for something to do
        select.select([self._commands], [], [])

        while self._commands.poll():
            cmd = self._commands.recv()
            logger.debug('Received command "%s"', cmd)
            if cmd == TERMINATE:
                logger.debug('Helper terminates')
                sys.exit(0)
            if cmd == GET_DEVICES:
                refresh_devices()
                self._send_devices()
            elif cmd in get_devices():
                self.device_name = cmd
            else:
                logger.error('Received unknown command "%s"', cmd)
Пример #15
0
    def _get_event(self, message):
        """Return an InputEvent if the message contains one. None otherwise."""
        message_type = message['type']
        message_body = message['message']

        if message_type == 'devices':
            # result of get_devices in the helper
            if message_body != get_devices():
                logger.debug('Received %d devices', len(message_body))
                set_devices(message_body)
                self._devices_updated = True
            return None

        if message_type == 'event':
            return evdev.InputEvent(*message_body)

        logger.error('Received unknown message "%s"', message)
        return None
Пример #16
0
    def populate_devices(self):
        """Make the devices selectable."""
        devices = get_devices()
        device_selection = self.get('device_selection')

        with HandlerDisabled(device_selection, self.on_select_device):
            self.device_store.clear()
            for device in devices:
                types = devices[device]['types']
                if len(types) > 0:
                    device_type = sorted(types, key=ICON_PRIORITIES.index)[0]
                    icon_name = ICON_NAMES[device_type]
                else:
                    icon_name = None

                self.device_store.append([icon_name, device])

        self.select_newest_preset()
Пример #17
0
    def initialize_gamepad_config(self):
        """Set slider and dropdown values when a gamepad is selected."""
        devices = get_devices()
        if devices[self.selected_device]['gamepad']:
            self.get('gamepad_separator').show()
            self.get('gamepad_config').show()
        else:
            self.get('gamepad_separator').hide()
            self.get('gamepad_config').hide()
            return

        left_purpose = custom_mapping.get('gamepad.joystick.left_purpose')
        self.get('left_joystick_purpose').set_active_id(left_purpose)

        right_purpose = custom_mapping.get('gamepad.joystick.right_purpose')
        self.get('right_joystick_purpose').set_active_id(right_purpose)

        pointer_speed = custom_mapping.get('gamepad.joystick.pointer_speed')
        range_value = math.log(pointer_speed, 2)
        self.get('joystick_mouse_speed').set_value(range_value)
Пример #18
0
def path_to_device_name(path):
    """Find the name of the get_devices group this path belongs to.

    The group name is commonly referred to as "device".

    Parameters
    ----------
    path : str
    """
    if not path.startswith('/dev/input/'):
        # already the name
        return path

    devices = get_devices()
    for device in devices:
        for candidate_path in devices[device]['paths']:
            if path == candidate_path:
                return device

    logger.debug('Device path %s is not managed by key-mapper', path)
    return None
Пример #19
0
    def _autoload(self, device):
        """Check if autoloading is a good idea, and if so do it.

        Parameters
        ----------
        device : str
            Device name. Expects a key that is present in get_devices().
            Can also be a path starting with /dev/input/
        """
        self.refresh_devices(device)

        device = path_to_device_name(device)
        if device not in get_devices():
            # even after refresh_devices, the device is not in
            # get_devices(), so it's either not relevant for key-mapper,
            # or not connected yet
            return

        preset = config.get(['autoload', device], log_unknown=False)

        if preset is None:
            # no autoloading is configured for this device
            return

        if not isinstance(preset, str):
            # might be broken due to a previous bug
            config.remove(['autoload', device])
            config.save_config()
            return

        logger.info('Autoloading "%s"', device)

        if not self.autoload_history.may_autoload(device, preset):
            logger.info(
                'Not autoloading the same preset "%s" again for device "%s"',
                preset, device)
            return

        self.start_injecting(device, preset)
        self.autoload_history.remember(device, preset)
Пример #20
0
 def test_get_devices_2(self):
     self.assertDictEqual(
         get_devices(), {
             'device 1': {
                 'paths': [
                     '/dev/input/event11', '/dev/input/event10',
                     '/dev/input/event13'
                 ],
                 'devices': ['device 1 foo', 'device 1', 'device 1'],
                 'types': [KEYBOARD, MOUSE]
             },
             'device 2': {
                 'paths': ['/dev/input/event20'],
                 'devices': ['device 2'],
                 'types': [KEYBOARD]
             },
             'gamepad': {
                 'paths': ['/dev/input/event30'],
                 'devices': ['gamepad'],
                 'types': [GAMEPAD]
             },
         })
Пример #21
0
 def test_get_devices_2(self):
     self.assertDictEqual(
         get_devices(), {
             'device 1': {
                 'paths': [
                     '/dev/input/event11', '/dev/input/event10',
                     '/dev/input/event13'
                 ],
                 'devices': ['device 1 foo', 'device 1', 'device 1'],
                 'gamepad':
                 False
             },
             'device 2': {
                 'paths': ['/dev/input/event20'],
                 'devices': ['device 2'],
                 'gamepad': False
             },
             'gamepad': {
                 'paths': ['/dev/input/event30'],
                 'devices': ['gamepad'],
                 'gamepad': True
             },
         })
Пример #22
0
    def start_reading(self, device_name):
        """Tell the evdev lib to start looking for keycodes.

        If read is called without prior start_reading, no keycodes
        will be available.
        """
        self.stop_reading()

        # make sure this sees up to date devices, including those created
        # by key-mapper
        refresh_devices()

        self.virtual_devices = []

        for name, group in get_devices().items():
            if device_name not in name:
                continue

            # Watch over each one of the potentially multiple devices per
            # hardware
            for path in group['paths']:
                try:
                    device = evdev.InputDevice(path)
                except FileNotFoundError:
                    continue

                if evdev.ecodes.EV_KEY in device.capabilities():
                    self.virtual_devices.append(device)

            logger.debug(
                'Starting reading keycodes from "%s"',
                '", "'.join([device.name for device in self.virtual_devices]))

        pipe = multiprocessing.Pipe()
        self._pipe = pipe
        self._process = threading.Thread(target=self._read_worker)
        self._process.start()
Пример #23
0
 def test_stubs(self):
     self.assertIn('device 1', get_devices())
Пример #24
0
 def populate_devices(self):
     """Make the devices selectable."""
     devices = get_devices()
     device_selection = self.get('device_selection')
     for device in devices:
         device_selection.append(device, device)
Пример #25
0
    def test_wheel(self):
        # this tests both keycode_mapper and event_producer, and it seems
        # to test stuff not covered in test_keycode_mapper, so it's a quite
        # important one.

        # wheel release events are made up with a debouncer

        # map those two to stuff
        w_up = (EV_REL, REL_WHEEL, -1)
        hw_right = (EV_REL, REL_HWHEEL, 1)

        # should be forwarded and present in the capabilities
        hw_left = (EV_REL, REL_HWHEEL, -1)

        custom_mapping.change(Key(*hw_right), 'k(b)')
        custom_mapping.change(Key(*w_up), 'c')

        system_mapping.clear()
        code_b = 91
        code_c = 92
        system_mapping._set('b', code_b)
        system_mapping._set('c', code_c)

        device_name = 'device 1'
        push_events(device_name, [
            new_event(*w_up),
        ] * 10 + [
            new_event(*hw_right),
            new_event(*w_up),
        ] * 5 + [new_event(*hw_left)])

        self.injector = Injector(device_name, custom_mapping)

        device = InputDevice('/dev/input/event11')
        # make sure this test uses a device that has the needed capabilities
        # for the injector to grab it
        self.assertIn(EV_REL, device.capabilities())
        self.assertIn(REL_WHEEL, device.capabilities()[EV_REL])
        self.assertIn(REL_HWHEEL, device.capabilities()[EV_REL])
        self.assertIn(device.path, get_devices()[device_name]['paths'])

        self.injector.start()

        # wait for the first injected key down event
        uinput_write_history_pipe[0].poll(timeout=1)
        self.assertTrue(uinput_write_history_pipe[0].poll())
        event = uinput_write_history_pipe[0].recv()
        self.assertEqual(event.t, (EV_KEY, code_c, 1))

        time.sleep(EVENT_READ_TIMEOUT * 5)
        # in 5 more read-loop ticks, nothing new should have happened
        self.assertFalse(uinput_write_history_pipe[0].poll())

        time.sleep(EVENT_READ_TIMEOUT * 6)
        # 5 more and it should be within the second phase in which
        # the horizontal wheel is used. add some tolerance
        self.assertTrue(uinput_write_history_pipe[0].poll())
        event = uinput_write_history_pipe[0].recv()
        self.assertEqual(event.t, (EV_KEY, code_b, 1))

        time.sleep(EVENT_READ_TIMEOUT * 10 + 5 / 60)
        # after 21 read-loop ticks all events should be consumed, wait for
        # at least 3 (=5) producer-ticks so that the debouncers are triggered.
        # Key-up events for both wheel events should be written now that no
        # new key-down event arrived.
        events = read_write_history_pipe()
        self.assertEqual(events.count((EV_KEY, code_b, 0)), 1)
        self.assertEqual(events.count((EV_KEY, code_c, 0)), 1)
        self.assertEqual(events.count(hw_left), 1)  # the unmapped wheel

        # the unmapped wheel won't get a debounced release command, it's
        # forwarded as is
        self.assertNotIn((EV_REL, REL_HWHEEL, 0), events)

        self.assertEqual(len(events), 3)
Пример #26
0
    def start_injecting(self, device, preset):
        """Start injecting the preset for the device.

        Returns True on success. If an injection is already ongoing for
        the specified device it will stop it automatically first.

        Parameters
        ----------
        device : string
            The name of the device
        preset : string
            The name of the preset
        """
        self.refresh_devices(device)

        device = path_to_device_name(device)

        if self.config_dir is None:
            logger.error(
                'Tried to start an injection without configuring the daemon '
                'first via set_config_dir.')
            return False

        if device not in get_devices():
            logger.error('Could not find device "%s"', device)
            return False

        preset_path = os.path.join(self.config_dir, 'presets', device,
                                   f'{preset}.json')

        mapping = Mapping()
        try:
            mapping.load(preset_path)
        except FileNotFoundError as error:
            logger.error(str(error))
            return False

        if self.injectors.get(device) is not None:
            self.stop_injecting(device)

        # Path to a dump of the xkb mappings, to provide more human
        # readable keys in the correct keyboard layout to the service.
        # The service cannot use `xmodmap -pke` because it's running via
        # systemd.
        xmodmap_path = os.path.join(self.config_dir, 'xmodmap.json')
        try:
            with open(xmodmap_path, 'r') as file:
                # do this for each injection to make sure it is up to
                # date when the system layout changes.
                xmodmap = json.load(file)
                logger.debug('Using keycodes from "%s"', xmodmap_path)
                system_mapping.update(xmodmap)
                # the service now has process wide knowledge of xmodmap
                # keys of the users session
        except FileNotFoundError:
            logger.error('Could not find "%s"', xmodmap_path)

        try:
            injector = Injector(device, mapping)
            injector.start()
            self.injectors[device] = injector
        except OSError:
            # I think this will never happen, probably leftover from
            # some earlier version
            return False

        return True
Пример #27
0
    def run(self):
        """The injection worker that keeps injecting until terminated.

        Stuff is non-blocking by using asyncio in order to do multiple things
        somewhat concurrently.

        Use this function as starting point in a process. It creates
        the loops needed to read and map events and keeps running them.
        """
        if self.device not in get_devices():
            logger.error('Cannot inject for unknown device "%s"', self.device)
            return

        group = get_devices()[self.device]

        logger.info('Starting injecting the mapping for "%s"', self.device)

        # create a new event loop, because somehow running an infinite loop
        # that sleeps on iterations (event_producer) in one process causes
        # another injection process to screw up reading from the grabbed
        # device.
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        # create this within the process after the event loop creation,
        # so that the macros use the correct loop
        self.context = Context(self.mapping)

        self._event_producer = EventProducer(self.context)

        numlock_state = is_numlock_on()
        coroutines = []

        # where mapped events go to.
        # See the Context docstring on why this is needed.
        self.context.uinput = evdev.UInput(
            name=self.get_udef_name(self.device, 'mapped'),
            phys=DEV_NAME,
            events=self._construct_capabilities(GAMEPAD in group['types'])
        )

        # Watch over each one of the potentially multiple devices per hardware
        for path in group['paths']:
            source = self._grab_device(path)
            if source is None:
                # this path doesn't need to be grabbed for injection, because
                # it doesn't provide the events needed to execute the mapping
                continue

            # certain capabilities can have side effects apparently. with an
            # EV_ABS capability, EV_REL won't move the mouse pointer anymore.
            # so don't merge all InputDevices into one UInput device.
            gamepad = classify(source) == GAMEPAD
            forward_to = evdev.UInput(
                name=self.get_udef_name(source.name, 'forwarded'),
                phys=DEV_NAME,
                events=self._copy_capabilities(source)
            )

            # actual reading of events
            coroutines.append(self._event_consumer(source, forward_to))

            # The event source of the current iteration will deliver events
            # that are needed for this. It is that one that will be mapped
            # to a mouse-like devnode.
            if gamepad and self.context.joystick_as_mouse():
                self._event_producer.set_abs_range_from(source)

        if len(coroutines) == 0:
            logger.error('Did not grab any device')
            self._msg_pipe[0].send(NO_GRAB)
            return

        coroutines.append(self._msg_listener())

        # run besides this stuff
        coroutines.append(self._event_producer.run())

        # set the numlock state to what it was before injecting, because
        # grabbing devices screws this up
        set_numlock(numlock_state)

        self._msg_pipe[0].send(OK)

        try:
            loop.run_until_complete(asyncio.gather(*coroutines))
        except RuntimeError:
            # stopped event loop most likely
            pass
        except OSError as error:
            logger.error(str(error))

        if len(coroutines) > 0:
            # expected when stop_injecting is called,
            # during normal operation as well as tests this point is not
            # reached otherwise.
            logger.debug('asyncio coroutines ended')
Пример #28
0
    def _start_injecting(self):
        """The injection worker that keeps injecting until terminated.

        Stuff is non-blocking by using asyncio in order to do multiple things
        somewhat concurrently.

        Use this function as starting point in a process. It creates
        the loops needed to read and map events and keeps running them.
        """
        # create a new event loop, because somehow running an infinite loop
        # that sleeps on iterations (ev_abs_mapper) in one process causes
        # another injection process to screw up reading from the grabbed
        # device.
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        numlock_state = is_numlock_on()

        loop = asyncio.get_event_loop()
        coroutines = []

        logger.info('Starting injecting the mapping for "%s"', self.device)

        paths = get_devices()[self.device]['paths']

        # Watch over each one of the potentially multiple devices per hardware
        for path in paths:
            source, abs_to_rel = self._prepare_device(path)
            if source is None:
                continue

            # each device needs own macro instances to add a custom handler
            logger.debug('Parsing macros for %s', path)
            macros = {}
            for key, output in self.mapping:
                if is_this_a_macro(output):
                    macro = parse(output, self.mapping)
                    if macro is None:
                        continue

                    for permutation in key.get_permutations():
                        macros[permutation.keys] = macro

            if len(macros) == 0:
                logger.debug('No macros configured')

            # certain capabilities can have side effects apparently. with an
            # EV_ABS capability, EV_REL won't move the mouse pointer anymore.
            # so don't merge all InputDevices into one UInput device.
            uinput = evdev.UInput(name=f'{DEV_NAME} {self.device}',
                                  phys=DEV_NAME,
                                  events=self._modify_capabilities(
                                      macros, source, abs_to_rel))

            logger.spam('Injected capabilities for "%s": %s', path,
                        uinput.capabilities(verbose=True))

            def handler(*args, uinput=uinput):
                # this ensures that the right uinput is used for macro_write,
                # because this is within a loop
                self._macro_write(*args, uinput)

            for macro in macros.values():
                macro.set_handler(handler)

            # actual reading of events
            coroutines.append(
                self._event_consumer(macros, source, uinput, abs_to_rel))

            # mouse movement injection based on the results of the
            # event consumer
            if abs_to_rel:
                self.abs_state[0] = 0
                self.abs_state[1] = 0
                coroutines.append(
                    ev_abs_mapper(self.abs_state, source, uinput,
                                  self.mapping))

        if len(coroutines) == 0:
            logger.error('Did not grab any device')
            return

        coroutines.append(self._msg_listener(loop))

        # set the numlock state to what it was before injecting, because
        # grabbing devices screws this up
        set_numlock(numlock_state)

        try:
            loop.run_until_complete(asyncio.gather(*coroutines))
        except RuntimeError:
            # stopped event loop most likely
            pass
        except OSError as error:
            logger.error(str(error))

        if len(coroutines) > 0:
            logger.debug('asyncio coroutines ended')
Пример #29
0
    def _start_reading(self):
        """Tell the evdev lib to start looking for keycodes.

        If read is called without prior start_reading, no keycodes
        will be available.

        This blocks forever until it discovers a new command on the socket.
        """
        device_name = self.device_name

        rlist = {}

        if device_name is None:
            logger.error('device_name is None')
            return

        group = get_devices().get(device_name)

        if group is None:
            # a device possibly disappeared due to refresh_devices
            self.device_name = None
            logger.error('%s disappeared', device_name)
            return

        virtual_devices = []
        # Watch over each one of the potentially multiple devices per
        # hardware
        for path in group['paths']:
            try:
                device = evdev.InputDevice(path)
            except FileNotFoundError:
                continue

            if evdev.ecodes.EV_KEY in device.capabilities():
                virtual_devices.append(device)

        if len(virtual_devices) == 0:
            logger.debug('No interesting device for "%s"', device_name)
            return

        for device in virtual_devices:
            rlist[device.fd] = device

        logger.debug('Starting reading keycodes from "%s"',
                     '", "'.join([device.name for device in virtual_devices]))

        rlist[self._commands] = self._commands

        while True:
            ready_fds = select.select(rlist, [], [])
            if len(ready_fds[0]) == 0:
                # whatever, happens for sockets sometimes. Maybe the socket
                # is closed and select has nothing to select from?
                continue

            for fd in ready_fds[0]:
                if rlist[fd] == self._commands:
                    # all commands will cause the reader to start over
                    # (possibly for a different device).
                    # _handle_commands will check what is going on
                    return

                device = rlist[fd]

                try:
                    event = device.read_one()
                    if event:
                        self._send_event(event, device)
                except OSError:
                    logger.debug('Device "%s" disappeared', device.path)
                    return
Пример #30
0
 def _send_devices(self):
     """Send the get_devices datastructure to the gui."""
     self._results.send({'type': 'devices', 'message': get_devices()})