diff --git a/keymapper/daemon.py b/keymapper/daemon.py
index 60b7d94a..57aa0bf4 100644
--- a/keymapper/daemon.py
+++ b/keymapper/daemon.py
@@ -443,7 +443,7 @@ class Daemon:
try:
injector = Injector(device, mapping)
- injector.start_injecting()
+ injector.start()
self.injectors[device] = injector
except OSError:
# I think this will never happen, probably leftover from
diff --git a/keymapper/injection/injector.py b/keymapper/injection/injector.py
index 69986342..c1f44e56 100644
--- a/keymapper/injection/injector.py
+++ b/keymapper/injection/injector.py
@@ -129,7 +129,7 @@ def is_in_capabilities(key, capabilities):
return False
-class Injector:
+class Injector(multiprocessing.Process):
"""Keeps injecting events in the background based on mapping and config.
Is a process to make it non-blocking for the rest of the code and to
@@ -139,7 +139,7 @@ class Injector:
regrab_timeout = 0.5
def __init__(self, device, mapping):
- """Start injecting keycodes based on custom_mapping.
+ """Setup a process to start injecting keycodes based on custom_mapping.
Parameters
----------
@@ -148,14 +148,57 @@ class Injector:
mapping : Mapping
"""
self.device = device
-
self.mapping = mapping
-
- self._process = None
- self._msg_pipe = multiprocessing.Pipe()
self._key_to_code = self._map_keys_to_codes()
- self._state = UNKNOWN
self._event_producer = None
+ self._state = UNKNOWN
+ self._msg_pipe = multiprocessing.Pipe()
+ super().__init__()
+
+ # Functions to interact with the running process:
+
+ def get_state(self):
+ """Get the state of the injection.
+
+ Can be safely called from the main process.
+ """
+ # slowly figure out what is going on
+ alive = self.is_alive()
+
+ if self._state == UNKNOWN and not alive:
+ # didn't start yet
+ return self._state
+
+ # if it is alive, it is definitely at least starting up
+ if self._state == UNKNOWN and alive:
+ self._state = STARTING
+
+ # if there is a message available, it might have finished starting up
+ if self._state == STARTING and self._msg_pipe[1].poll():
+ msg = self._msg_pipe[1].recv()
+ if msg == OK:
+ self._state = RUNNING
+
+ if msg == NO_GRAB:
+ self._state = NO_GRAB
+
+ if self._state in [STARTING, RUNNING] and not alive:
+ self._state = FAILED
+ logger.error('Injector was unexpectedly found stopped')
+
+ return self._state
+
+ @ensure_numlock
+ def stop_injecting(self):
+ """Stop injecting keycodes.
+
+ Can be safely called from the main procss.
+ """
+ logger.info('Stopping injecting keycodes for device "%s"', self.device)
+ self._msg_pipe[1].send(CLOSE)
+ self._state = STOPPED
+
+ # Process internal stuff:
def _forwards_joystick(self):
"""If at least one of the joysticks remains a regular joystick."""
@@ -205,40 +248,6 @@ class Injector:
return key_to_code
- def start_injecting(self):
- """Start injecting keycodes."""
- if 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._state = STARTING
- self._process = multiprocessing.Process(target=self._start_injecting)
- self._process.start()
-
- def get_state(self):
- """Get the state of the injection."""
- # only at this point the actual state is figured out
- if self._state == STARTING and self._msg_pipe[1].poll():
- msg = self._msg_pipe[1].recv()
- if msg == OK:
- self._state = RUNNING
-
- if msg == NO_GRAB:
- self._state = NO_GRAB
-
- alive = self._process is not None and self._process.is_alive()
- if self._state in [STARTING, RUNNING] and not alive:
- self._state = FAILED
- logger.error('Injector was unexpectedly found stopped')
-
- return self._state
-
def _grab_device(self, path):
"""Try to grab the device, return None if not needed/possible."""
try:
@@ -386,7 +395,7 @@ class Injector:
loop.stop()
return
- def _start_injecting(self):
+ def run(self):
"""The injection worker that keeps injecting until terminated.
Stuff is non-blocking by using asyncio in order to do multiple things
@@ -395,6 +404,10 @@ class Injector:
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
+
# 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
@@ -565,10 +578,3 @@ class Injector:
'The consumer for "%s" stopped early',
source.path
)
-
- @ensure_numlock
- def stop_injecting(self):
- """Stop injecting keycodes."""
- logger.info('Stopping injecting keycodes for device "%s"', self.device)
- self._msg_pipe[1].send(CLOSE)
- self._state = STOPPED
diff --git a/keymapper/injection/macros.py b/keymapper/injection/macros.py
index c13a063b..3d362884 100644
--- a/keymapper/injection/macros.py
+++ b/keymapper/injection/macros.py
@@ -112,7 +112,6 @@ class _Macro:
handler : function
Will receive int code and value for an EV_KEY event to write
"""
- # TODO test handler
self.running = True
for _, task in self.tasks:
coroutine = task(handler)
diff --git a/readme/pylint.svg b/readme/pylint.svg
index 6d3f49fe..cc38f2d4 100644
--- a/readme/pylint.svg
+++ b/readme/pylint.svg
@@ -17,7 +17,7 @@
pylint
- 9.74
- 9.74
+ 9.75
+ 9.75
\ No newline at end of file
diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py
index 3aa268af..fc6048f8 100644
--- a/tests/testcases/test_injector.py
+++ b/tests/testcases/test_injector.py
@@ -103,12 +103,12 @@ class TestInjector(unittest.TestCase):
self.assertGreaterEqual(self.failed, 1)
self.assertEqual(self.injector.get_state(), UNKNOWN)
- self.injector.start_injecting()
+ self.injector.start()
self.assertEqual(self.injector.get_state(), STARTING)
# since none can be grabbed, the process will terminate. But that
# actually takes quite some time.
time.sleep(1.5)
- self.assertFalse(self.injector._process.is_alive())
+ self.assertFalse(self.injector.is_alive())
self.assertEqual(self.injector.get_state(), NO_GRAB)
def test_grab_device_1(self):
@@ -333,7 +333,7 @@ class TestInjector(unittest.TestCase):
]
self.injector = Injector('gamepad', custom_mapping)
- self.injector.start_injecting()
+ self.injector.start()
# wait for the injector to start sending, at most 1s
uinput_write_history_pipe[0].poll(1)
@@ -381,7 +381,7 @@ class TestInjector(unittest.TestCase):
custom_mapping.change(Key((1, BTN_A, 1)), 'b')
system_mapping._set('b', 77)
self.injector = Injector('gamepad', custom_mapping)
- self.injector.start_injecting()
+ self.injector.start()
# wait for the injector to start sending, at most 1s
uinput_write_history_pipe[0].poll(1)
@@ -411,7 +411,7 @@ class TestInjector(unittest.TestCase):
custom_mapping.change(Key((EV_ABS, ABS_Z, 1)), 'b')
system_mapping._set('b', 77)
self.injector = Injector('gamepad', custom_mapping)
- self.injector.start_injecting()
+ self.injector.start()
# wait for the injector to start sending, at most 1s
uinput_write_history_pipe[0].poll(1)
@@ -428,10 +428,10 @@ class TestInjector(unittest.TestCase):
custom_mapping.set('gamepad.joystick.right_purpose', NONE)
self.injector = Injector('gamepad', custom_mapping)
# the stop message will be available in the pipe right away,
- # so _start_injecting won't block and just stop. all the stuff
+ # so run won't block and just stop. all the stuff
# will be initialized though, so that stuff can be tested
self.injector.stop_injecting()
- self.injector._start_injecting()
+ self.injector.run()
# not in a process, so the event_producer state can be checked
self.assertEqual(self.injector._event_producer.max_abs, MAX_ABS)
self.assertIsNotNone(self.injector._event_producer.mouse_uinput)
@@ -441,7 +441,7 @@ class TestInjector(unittest.TestCase):
custom_mapping.set('gamepad.joystick.right_purpose', BUTTONS)
self.injector = Injector('gamepad', custom_mapping)
self.injector.stop_injecting()
- self.injector._start_injecting()
+ self.injector.run()
self.assertIsNone(self.injector._event_producer.max_abs, MAX_ABS)
self.assertIsNone(self.injector._event_producer.mouse_uinput)
@@ -450,7 +450,7 @@ class TestInjector(unittest.TestCase):
custom_mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.injector = Injector('device 1', custom_mapping)
self.injector.stop_injecting()
- self.injector._start_injecting()
+ self.injector.run()
# not a gamepad, so _event_producer is not initialized for that.
# it can still debounce stuff though
self.assertIsNone(self.injector._event_producer.max_abs)
@@ -494,7 +494,7 @@ class TestInjector(unittest.TestCase):
self.injector = Injector('device 2', custom_mapping)
self.assertEqual(self.injector.get_state(), UNKNOWN)
- self.injector.start_injecting()
+ self.injector.start()
self.assertEqual(self.injector.get_state(), STARTING)
uinput_write_history_pipe[0].poll(timeout=1)
@@ -553,7 +553,7 @@ class TestInjector(unittest.TestCase):
self.assertEqual(history[6], (3124, 3564, 6542))
time.sleep(0.1)
- self.assertTrue(self.injector._process.is_alive())
+ self.assertTrue(self.injector.is_alive())
numlock_after = is_numlock_on()
self.assertEqual(numlock_before, numlock_after)
@@ -603,7 +603,7 @@ class TestInjector(unittest.TestCase):
input = InputDevice('/dev/input/event30')
self.injector._grab_device = lambda *args: input
- self.injector.start_injecting()
+ self.injector.start()
uinput_write_history_pipe[0].poll(timeout=1)
time.sleep(EVENT_READ_TIMEOUT * 10)
return read_write_history_pipe()
@@ -668,7 +668,7 @@ class TestInjector(unittest.TestCase):
self.assertIn(REL_HWHEEL, device.capabilities()[EV_REL])
self.assertIn(device.path, get_devices()[device_name]['paths'])
- self.injector.start_injecting()
+ self.injector.start()
# wait for the first injected key down event
uinput_write_history_pipe[0].poll(timeout=1)
@@ -724,7 +724,7 @@ class TestInjector(unittest.TestCase):
self.injector._modify_capabilities = _modify_capabilities
try:
- self.injector._start_injecting()
+ self.injector.run()
except Stop:
pass