no .terminate anymore, proper multiprocessing coverage, some minor cleanup in injector

pull/14/head
sezanzeb 4 years ago
parent f4ca07490d
commit 17445558fb

@ -1,4 +1,5 @@
[run] [run]
concurrency = multiprocessing
branch = True branch = True
source = /usr/lib/python3.8/site-packages/keymapper source = keymapper
concurrency = multiprocessing
debug = multiproc

@ -59,7 +59,8 @@ groups
##### Git/pip ##### Git/pip
```bash ```bash
sudo pip install git+https://github.com/sezanzeb/key-mapper.git git clone https://github.com/sezanzeb/key-mapper.git
cd key-mapper && sudo python3 setup.py install
``` ```
##### Manjaro/Arch ##### Manjaro/Arch
@ -93,12 +94,12 @@ sudo dpkg -i python3-key-mapper_0.1.0-1_all.deb
- [x] support timed macros, maybe using some sort of syntax - [x] support timed macros, maybe using some sort of syntax
- [x] add to the AUR, provide .deb file - [x] add to the AUR, provide .deb file
- [ ] automatically load presets when devices get plugged in after login - [ ] automatically load presets when devices get plugged in after login
- [ ] support gamepads as keyboard and mouse combi - [ ] support gamepads as keyboard and mouse combi (partially done)
## Tests ## Tests
```bash ```bash
pylint keymapper --extension-pkg-whitelist=evdev pylint keymapper --extension-pkg-whitelist=evdev
sudo pip install . && coverage run tests/test.py sudo pip install -e . && coverage run tests/test.py
coverage combine && coverage report -m coverage combine && coverage report -m
``` ```

@ -27,7 +27,7 @@ import json
import shutil import shutil
import copy import copy
from keymapper.paths import CONFIG, touch from keymapper.paths import CONFIG, USER, touch
from keymapper.logger import logger from keymapper.logger import logger

@ -138,8 +138,14 @@ class Daemon(service.Object):
return True return True
@dbus.service.method('keymapper.Interface') @dbus.service.method('keymapper.Interface', in_signature='b')
def stop(self): def stop(self, terminate=False):
"""Stop all mapping injections.""" """Stop all injections and end the service.
Raises dbus.exceptions.DBusException in your main process.
"""
for injector in self.injectors.values(): for injector in self.injectors.values():
injector.stop_injecting() injector.stop_injecting()
if terminate:
exit(0)

@ -39,9 +39,7 @@ from keymapper.dev.macros import parse
DEV_NAME = 'key-mapper' DEV_NAME = 'key-mapper'
DEVICE_CREATED = 1 CLOSE = 0
FAILED = 2
DEVICE_SKIPPED = 3
def is_numlock_on(): def is_numlock_on():
@ -67,8 +65,8 @@ def toggle_numlock():
logger.debug('numlockx not found, trying to inject a keycode') logger.debug('numlockx not found, trying to inject a keycode')
# and this doesn't always work. # and this doesn't always work.
device = evdev.UInput( device = evdev.UInput(
name=f'key-mapper numlock-control', name=f'{DEV_NAME} numlock-control',
phys='key-mapper', phys=DEV_NAME,
) )
device.write(EV_KEY, evdev.ecodes.KEY_NUMLOCK, 1) device.write(EV_KEY, evdev.ecodes.KEY_NUMLOCK, 1)
device.syn() device.syn()
@ -110,10 +108,7 @@ class KeycodeInjector:
self.device = device self.device = device
self.mapping = mapping self.mapping = mapping
self._process = None self._process = None
self._msg_pipe = multiprocessing.Pipe()
def __del__(self):
if self._process is not None:
self._process.terminate()
def start_injecting(self): def start_injecting(self):
"""Start injecting keycodes.""" """Start injecting keycodes."""
@ -121,11 +116,16 @@ class KeycodeInjector:
self._process.start() self._process.start()
def _prepare_device(self, path): def _prepare_device(self, path):
"""Try to grab the device, return if not needed/possible.""" """Try to grab the device, return if not needed/possible.
Also return if ABS events are changed to REL mouse movements,
because the capabilities of the returned device are changed
so this cannot be checked later anymore.
"""
device = evdev.InputDevice(path) device = evdev.InputDevice(path)
if device is None: if device is None:
return None return None, False
capabilities = device.capabilities(absinfo=False) capabilities = device.capabilities(absinfo=False)
@ -136,15 +136,15 @@ class KeycodeInjector:
needed = True needed = True
break break
can_do_abs = evdev.ecodes.ABS_X in capabilities.get(EV_ABS, []) map_ABS = self.map_ABS(device)
if self.map_abs_to_rel() and can_do_abs: if map_ABS:
needed = True needed = True
if not needed: if not needed:
# skipping reading and checking on events from those devices # skipping reading and checking on events from those devices
# may be beneficial for performance. # may be beneficial for performance.
logger.debug('No need to grab %s', path) logger.debug('No need to grab %s', path)
return None return None, False
attempts = 0 attempts = 0
while True: while True:
@ -164,22 +164,25 @@ class KeycodeInjector:
if attempts >= 4: if attempts >= 4:
logger.error('Cannot grab %s, it is possibly in use', path) logger.error('Cannot grab %s, it is possibly in use', path)
return None return None, False
time.sleep(0.15) time.sleep(0.15)
return device return device, map_ABS
def map_abs_to_rel(self): def map_ABS(self, device):
# TODO offer configuration via the UI if a gamepad is elected # TODO offer configuration via the UI if a gamepad is elected
return True capabilities = device.capabilities(absinfo=False)
return evdev.ecodes.ABS_X in capabilities.get(EV_ABS, [])
def _modify_capabilities(self, input_device): def _modify_capabilities(self, input_device, map_ABS):
"""Adds all keycode into a copy of a devices capabilities. """Adds all keycode into a copy of a devices capabilities.
Prameters Prameters
--------- ---------
input_device : evdev.InputDevice input_device : evdev.InputDevice
map_ABS : bool
if ABS capabilities should be removed in favor of REL
""" """
ecodes = evdev.ecodes ecodes = evdev.ecodes
@ -196,9 +199,8 @@ class KeycodeInjector:
if keycode is not None: if keycode is not None:
capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET) capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET)
if self.map_abs_to_rel(): if map_ABS:
if capabilities.get(ecodes.EV_ABS): del capabilities[ecodes.EV_ABS]
del capabilities[ecodes.EV_ABS]
capabilities[ecodes.EV_REL] = [ capabilities[ecodes.EV_REL] = [
evdev.ecodes.REL_X, evdev.ecodes.REL_X,
evdev.ecodes.REL_Y, evdev.ecodes.REL_Y,
@ -214,23 +216,37 @@ class KeycodeInjector:
return capabilities return capabilities
async def _msg_listener(self, loop):
"""Wait for messages from the main process to do special stuff."""
while True:
frame_available = asyncio.Event()
loop.add_reader(self._msg_pipe[0].fileno(), frame_available.set)
await frame_available.wait()
frame_available.clear()
msg = self._msg_pipe[0].recv()
if msg == CLOSE:
logger.debug('Received close signal')
# stop the event loop and cause the process to reach its end
# cleanly. Using .terminate prevents coverage from working.
loop.stop()
return
def _start_injecting(self): def _start_injecting(self):
"""The injection worker that keeps injecting until terminated. """The injection worker that keeps injecting until terminated.
Stuff is non-blocking by using asyncio in order to do multiple things Stuff is non-blocking by using asyncio in order to do multiple things
somewhat concurrently. somewhat concurrently.
""" """
# TODO do select.select insted of async_read_loop
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
coroutines = [] coroutines = []
logger.info('Starting injecting the mapping for %s', self.device) logger.info('Starting injecting the mapping for %s', self.device)
paths = get_devices()[self.device]['paths'] paths = get_devices()[self.device]['paths']
devices = [self._prepare_device(path) for path in paths]
# Watch over each one of the potentially multiple devices per hardware # Watch over each one of the potentially multiple devices per hardware
for input_device in devices: for path in paths:
input_device, map_ABS = self._prepare_device(path)
if input_device is None: if input_device is None:
continue continue
@ -238,17 +254,19 @@ class KeycodeInjector:
# EV_ABS capability, EV_REL won't move the mouse pointer anymore. # EV_ABS capability, EV_REL won't move the mouse pointer anymore.
# so don't merge all InputDevices into one UInput device. # so don't merge all InputDevices into one UInput device.
uinput = evdev.UInput( uinput = evdev.UInput(
name=f'key-mapper {self.device}', name=f'{DEV_NAME} {self.device}',
phys='key-mapper', phys=DEV_NAME,
events=self._modify_capabilities(input_device) events=self._modify_capabilities(input_device, map_ABS)
) )
# TODO separate file
# keycode injection # keycode injection
coroutine = self._keycode_loop(input_device, uinput) coroutine = self._keycode_loop(input_device, uinput, map_ABS)
coroutines.append(coroutine) coroutines.append(coroutine)
# TODO separate file
# mouse movement injection # mouse movement injection
if self.map_abs_to_rel(): if map_ABS:
self.abs_x = 0 self.abs_x = 0
self.abs_y = 0 self.abs_y = 0
# events only take ints, so a movement of 0.3 needs to add # events only take ints, so a movement of 0.3 needs to add
@ -262,7 +280,13 @@ class KeycodeInjector:
logger.error('Did not grab any device') logger.error('Did not grab any device')
return return
loop.run_until_complete(asyncio.gather(*coroutines)) coroutines.append(self._msg_listener(loop))
try:
loop.run_until_complete(asyncio.gather(*coroutines))
except RuntimeError:
# stopped event loop most likely
pass
if len(coroutines) > 0: if len(coroutines) > 0:
logger.debug('asyncio coroutines ended') logger.debug('asyncio coroutines ended')
@ -336,7 +360,7 @@ class KeycodeInjector:
rel_x rel_x
) )
async def _keycode_loop(self, device, keymapper_device): async def _keycode_loop(self, device, keymapper_device, map_ABS):
"""Inject keycodes for one of the virtual devices. """Inject keycodes for one of the virtual devices.
Parameters Parameters
@ -345,6 +369,8 @@ class KeycodeInjector:
where to read keycodes from where to read keycodes from
keymapper_device : evdev.UInput keymapper_device : evdev.UInput
where to write keycodes to where to write keycodes to
map_ABS : bool
the value of map_ABS() for the original device
""" """
# Parse all macros beforehand # Parse all macros beforehand
logger.debug('Parsing macros') logger.debug('Parsing macros')
@ -363,7 +389,7 @@ class KeycodeInjector:
) )
async for event in device.async_read_loop(): async for event in device.async_read_loop():
if self.map_abs_to_rel() and event.type == EV_ABS: if map_ABS and event.type == EV_ABS:
if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]: if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]:
continue continue
if event.code == evdev.ecodes.ABS_X: if event.code == evdev.ecodes.ABS_X:
@ -438,5 +464,4 @@ class KeycodeInjector:
def stop_injecting(self): def stop_injecting(self):
"""Stop injecting keycodes.""" """Stop injecting keycodes."""
logger.info('Stopping injecting keycodes for device "%s"', self.device) logger.info('Stopping injecting keycodes for device "%s"', self.device)
if self._process is not None and self._process.is_alive(): self._msg_pipe[1].send(CLOSE)
self._process.terminate()

@ -31,6 +31,9 @@ from keymapper.getdevices import get_devices, refresh_devices
from keymapper.state import KEYCODE_OFFSET from keymapper.state import KEYCODE_OFFSET
CLOSE = 1
class _KeycodeReader: class _KeycodeReader:
"""Keeps reading keycodes in the background for the UI to use. """Keeps reading keycodes in the background for the UI to use.
@ -47,15 +50,9 @@ class _KeycodeReader:
self.stop_reading() self.stop_reading()
def stop_reading(self): def stop_reading(self):
# TODO something like this for the injector?
if self._process is not None:
logger.debug('Terminating reader process')
self._process.terminate()
self._process = None
if self._pipe is not None: if self._pipe is not None:
logger.debug('Closing reader pipe') logger.debug('Sending close msg to reader')
self._pipe[0].close() self._pipe[0].send(CLOSE)
self._pipe = None self._pipe = None
def clear(self): def clear(self):
@ -77,8 +74,7 @@ class _KeycodeReader:
self.virtual_devices = [] self.virtual_devices = []
for name, group in get_devices(include_keymapper=True).items(): for name, group in get_devices().items():
# also find stuff like "key-mapper {device}"
if device_name not in name: if device_name not in name:
continue continue
@ -99,35 +95,46 @@ class _KeycodeReader:
) )
pipe = multiprocessing.Pipe() pipe = multiprocessing.Pipe()
self._process = multiprocessing.Process(
target=self._read_worker,
args=(pipe[1],)
)
self._process.start()
self._pipe = pipe self._pipe = pipe
self._process = multiprocessing.Process(target=self._read_worker)
self._process.start()
def _consume_event(self, event, pipe): def _consume_event(self, event):
"""Write the event code into the pipe if it is a key-down press.""" """Write the event code into the pipe if it is a key-down press."""
# value: 1 for down, 0 for up, 2 for hold. # value: 1 for down, 0 for up, 2 for hold.
if self._pipe[1].closed:
logger.debug('Pipe closed, reader stops.')
exit(0)
if event.type == evdev.ecodes.EV_KEY and event.value == 1: if event.type == evdev.ecodes.EV_KEY and event.value == 1:
logger.spam( logger.spam(
'got code:%s value:%s', 'got code:%s value:%s',
event.code + KEYCODE_OFFSET, event.value event.code + KEYCODE_OFFSET, event.value
) )
pipe.send(event.code + KEYCODE_OFFSET) self._pipe[1].send(event.code + KEYCODE_OFFSET)
def _read_worker(self, pipe): def _read_worker(self):
"""Process that reads keycodes and buffers them into a pipe.""" """Process that reads keycodes and buffers them into a pipe."""
# using a process that blocks instead of read_one made it easier # using a process that blocks instead of read_one made it easier
# to debug via the logs, because the UI was not polling properly # to debug via the logs, because the UI was not polling properly
# at some point which caused logs for events not to be written. # at some point which caused logs for events not to be written.
rlist = {device.fd: device for device in self.virtual_devices} rlist = {device.fd: device for device in self.virtual_devices}
rlist[self._pipe[1]] = self._pipe[1]
while True: while True:
ready = select.select(rlist, [], [])[0] ready = select.select(rlist, [], [])[0]
for fd in ready: for fd in ready:
readable = rlist[fd]
if isinstance(readable, multiprocessing.connection.Connection):
msg = readable.recv()
if msg == CLOSE:
logger.debug('Reader stopped')
return
continue
try: try:
for event in rlist[fd].read(): for event in rlist[fd].read():
self._consume_event(event, pipe) self._consume_event(event)
except OSError: except OSError:
logger.debug( logger.debug(
'Device "%s" disappeared from the reader', 'Device "%s" disappeared from the reader',

@ -69,6 +69,9 @@ def get_selected_row_bg():
return color.to_string() return color.to_string()
# TODO show if the preset is being injected
class Window: class Window:
"""User Interface.""" """User Interface."""
def __init__(self): def __init__(self):

@ -62,6 +62,13 @@ class Formatter(logging.Formatter):
SPAM: 34, SPAM: 34,
logging.INFO: 32, logging.INFO: 32,
}.get(record.levelno, 0) }.get(record.levelno, 0)
# if this runs in a separate process, write down the pid
# to debug exit codes and such
pid = ''
if os.getpid() != logger.main_pid:
pid = f'pid {os.getpid()}, '
if debug: if debug:
self._style._fmt = ( # noqa self._style._fmt = ( # noqa
'\033[1m' # bold '\033[1m' # bold
@ -69,7 +76,7 @@ class Formatter(logging.Formatter):
f'%(levelname)s' f'%(levelname)s'
'\033[0m' # end style '\033[0m' # end style
f'\033[{color}m' # color f'\033[{color}m' # color
': %(filename)s, line %(lineno)d, %(message)s' f': {pid}%(filename)s, line %(lineno)d, %(message)s'
'\033[0m' # end style '\033[0m' # end style
) )
else: else:
@ -85,6 +92,7 @@ handler.setFormatter(Formatter())
logger.addHandler(handler) logger.addHandler(handler)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
logging.getLogger('asyncio').setLevel(logging.WARNING) logging.getLogger('asyncio').setLevel(logging.WARNING)
logger.main_pid = os.getpid()
def is_debug(): def is_debug():

@ -0,0 +1,13 @@
#!/usr/bin/env bash
# in case setup.py does nothing instead of something.
# call via `./scripts/build.sh`
# try both ways of installation
sudo pip3 install .
sudo python3 setup.py install
# copy crucial files
sudo cp bin/* /usr/bin/ -r
sudo mkdir /usr/share/key-mapper
sudo cp data/* /usr/share/key-mapper -r

@ -158,10 +158,18 @@ def patch_select():
import select import select
def new_select(rlist, *args): def new_select(rlist, *args):
return ([ ret = []
device for device in rlist for thing in rlist:
if len(pending_events.get(device, [])) > 0 if hasattr(thing, 'poll') and thing.poll():
],) # the reader receives msgs through pipes. If there is one
# ready, provide the pipe
ret.append(thing)
continue
if len(pending_events.get(thing, [])) > 0:
ret.append(thing)
return [ret, [], []]
select.select = new_select select.select = new_select

@ -20,13 +20,9 @@
import os import os
import sys
import multiprocessing import multiprocessing
import unittest import unittest
import time import time
from unittest.mock import patch
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
import dbus import dbus
import evdev import evdev
@ -61,12 +57,10 @@ class TestDBusDaemon(unittest.TestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
cls.interface.stop() try:
time.sleep(0.1) cls.interface.stop(True)
cls.process.terminate() except dbus.exceptions.DBusException:
time.sleep(0.1) pass
os.system('pkill -f key-mapper-service')
time.sleep(0.1)
def test_can_connect(self): def test_can_connect(self):
self.assertIsInstance(self.interface, dbus.Interface) self.assertIsInstance(self.interface, dbus.Interface)

@ -58,8 +58,9 @@ class TestInjector(unittest.TestCase):
self.injector.stop_injecting() self.injector.stop_injecting()
self.injector = None self.injector = None
evdev.InputDevice.grab = self.grab evdev.InputDevice.grab = self.grab
if pending_events.get('device 2') is not None: keys = list(pending_events.keys())
del pending_events['device 2'] for key in keys:
del pending_events[key]
clear_write_history() clear_write_history()
def test_modify_capabilities(self): def test_modify_capabilities(self):
@ -80,7 +81,11 @@ class TestInjector(unittest.TestCase):
maps_to = system_mapping['a'] - KEYCODE_OFFSET maps_to = system_mapping['a'] - KEYCODE_OFFSET
self.injector = KeycodeInjector('foo', mapping) self.injector = KeycodeInjector('foo', mapping)
capabilities = self.injector._modify_capabilities(FakeDevice()) fake_device = FakeDevice()
capabilities = self.injector._modify_capabilities(
fake_device,
map_ABS=False
)
self.assertIn(EV_KEY, capabilities) self.assertIn(EV_KEY, capabilities)
keys = capabilities[EV_KEY] keys = capabilities[EV_KEY]
@ -97,17 +102,30 @@ class TestInjector(unittest.TestCase):
path = '/dev/input/event10' path = '/dev/input/event10'
# this test needs to pass around all other constraints of # this test needs to pass around all other constraints of
# _prepare_device # _prepare_device
device = self.injector._prepare_device(path) device, map_ABS = self.injector._prepare_device(path)
self.assertFalse(map_ABS)
self.assertEqual(self.failed, 2) self.assertEqual(self.failed, 2)
# success on the third try # success on the third try
device.name = fixtures[path]['name'] device.name = fixtures[path]['name']
def test_gamepad_capabilities(self):
self.injector = KeycodeInjector('gamepad', custom_mapping)
path = '/dev/input/event30'
device, map_ABS = self.injector._prepare_device(path)
self.assertTrue(map_ABS)
capabilities = self.injector._modify_capabilities(device, map_ABS)
self.assertNotIn(evdev.ecodes.EV_ABS, capabilities)
self.assertIn(evdev.ecodes.EV_REL, capabilities)
def test_skip_unused_device(self): def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping # skips a device because its capabilities are not used in the mapping
custom_mapping.change(10, 'a') custom_mapping.change(10, 'a')
self.injector = KeycodeInjector('device 1', custom_mapping) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event11' path = '/dev/input/event11'
device = self.injector._prepare_device(path) device, map_ABS = self.injector._prepare_device(path)
self.assertFalse(map_ABS)
self.assertEqual(self.failed, 0) self.assertEqual(self.failed, 0)
self.assertIsNone(device) self.assertIsNone(device)
@ -115,7 +133,7 @@ class TestInjector(unittest.TestCase):
# skips a device because its capabilities are not used in the mapping # skips a device because its capabilities are not used in the mapping
self.injector = KeycodeInjector('device 1', custom_mapping) self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event11' path = '/dev/input/event11'
device = self.injector._prepare_device(path) device, _ = self.injector._prepare_device(path)
# make sure the test uses a fixture without interesting capabilities # make sure the test uses a fixture without interesting capabilities
capabilities = evdev.InputDevice(path).capabilities() capabilities = evdev.InputDevice(path).capabilities()
@ -145,7 +163,7 @@ class TestInjector(unittest.TestCase):
def test_abs_to_rel(self): def test_abs_to_rel(self):
# maps gamepad joystick events to mouse events # maps gamepad joystick events to mouse events
# TODO enable this somewhere so that map_abs_to_rel returns true # TODO enable this somewhere so that map_ABS returns true
# in the .json file of the mapping. # in the .json file of the mapping.
config.set('gamepad.non_linearity', 1) config.set('gamepad.non_linearity', 1)
pointer_speed = 80 pointer_speed = 80
@ -182,6 +200,11 @@ class TestInjector(unittest.TestCase):
event = uinput_write_history_pipe[0].recv() event = uinput_write_history_pipe[0].recv()
history.append((event.type, event.code, event.value)) history.append((event.type, event.code, event.value))
if history[0][0] == EV_ABS:
raise AssertionError(
'The injector probably just forwarded them unchanged'
)
# movement is written at 60hz and it takes `divisor` steps to # movement is written at 60hz and it takes `divisor` steps to
# move 1px. take it times 2 for both x and y events. # move 1px. take it times 2 for both x and y events.
self.assertGreater(len(history), 60 * sleep * 0.9 * 2 / divisor) self.assertGreater(len(history), 60 * sleep * 0.9 * 2 / divisor)

@ -50,8 +50,10 @@ def gtk_iteration():
Gtk.main_iteration() Gtk.main_iteration()
def launch(argv=None, bin_path='/bin/key-mapper-gtk'): def launch(argv=None):
"""Start key-mapper-gtk with the command line argument array argv.""" """Start key-mapper-gtk with the command line argument array argv."""
bin_path = os.path.join(os.getcwd(), 'bin', 'key-mapper-gtk')
if not argv: if not argv:
argv = ['-d'] argv = ['-d']

@ -52,6 +52,10 @@ class TestReader(unittest.TestCase):
Event(evdev.events.EV_KEY, CODE_3, 1) Event(evdev.events.EV_KEY, CODE_3, 1)
] ]
keycode_reader.start_reading('device 1') keycode_reader.start_reading('device 1')
# sending anything arbitrary does not stop the pipe
keycode_reader._pipe[0].send(1234)
time.sleep(EVENT_READ_TIMEOUT * 5) time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), CODE_3 + 8) self.assertEqual(keycode_reader.read(), CODE_3 + 8)
self.assertIsNone(keycode_reader.read()) self.assertIsNone(keycode_reader.read())
@ -67,8 +71,10 @@ class TestReader(unittest.TestCase):
self.assertIsNone(keycode_reader.read()) self.assertIsNone(keycode_reader.read())
def test_keymapper_devices(self): def test_keymapper_devices(self):
# In order to show pressed keycodes on the ui while the device is # Don't read from keymapper devices, their keycodes are not
# grabbed, read from that as well. # representative for the original key. As long as this is not
# intentionally programmed it won't even do that. But it was at some
# point.
pending_events['key-mapper device 2'] = [ pending_events['key-mapper device 2'] = [
Event(evdev.events.EV_KEY, CODE_1, 1), Event(evdev.events.EV_KEY, CODE_1, 1),
Event(evdev.events.EV_KEY, CODE_2, 1), Event(evdev.events.EV_KEY, CODE_2, 1),
@ -76,7 +82,6 @@ class TestReader(unittest.TestCase):
] ]
keycode_reader.start_reading('device 2') keycode_reader.start_reading('device 2')
time.sleep(EVENT_READ_TIMEOUT * 5) time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), CODE_3 + 8)
self.assertIsNone(keycode_reader.read()) self.assertIsNone(keycode_reader.read())
def test_clear(self): def test_clear(self):

Loading…
Cancel
Save