Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
release any keys or buttons before the event pipeline shuts down
  • Loading branch information
jonasBoss committed Apr 10, 2022
1 parent cf58d58 commit 0338427
Show file tree
Hide file tree
Showing 16 changed files with 158 additions and 9 deletions.
6 changes: 6 additions & 0 deletions inputremapper/injection/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


"""Stores injection-process wide information."""
import asyncio
from typing import Awaitable, List, Dict, Tuple, Protocol, Set

import evdev
Expand Down Expand Up @@ -90,6 +91,11 @@ def __init__(self, preset: Preset):

self._create_callbacks()

def reset(self) -> None:
"""call the reset method for each handler in the context"""
for handlers in self._handlers.values():
[handler.reset() for handler in handlers]

def _create_callbacks(self) -> None:
"""add the notify method from all _handlers to self.callbacks"""
for event, handler_list in self._handlers.items():
Expand Down
35 changes: 33 additions & 2 deletions inputremapper/injection/event_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,38 @@
import asyncio
import evdev
from inputremapper.logger import logger
from inputremapper.input_event import InputEvent
from inputremapper.input_event import InputEvent, EventActions
from inputremapper.injection.context import Context


class _ReadLoop:
def __init__(self, device: evdev.InputDevice, stop_event: asyncio.Event):
self.iterator = device.async_read_loop().__aiter__()
self.stop_event = stop_event
self.wait_for_stop = asyncio.Task(stop_event.wait())

def __aiter__(self):
return self

def __anext__(self):
if self.stop_event.is_set():
raise StopAsyncIteration

return self.future()

async def future(self):
ev_task = asyncio.Task(self.iterator.__anext__())
stop_task = self.wait_for_stop
done, pending = await asyncio.wait(
{ev_task, stop_task},
return_when=asyncio.FIRST_COMPLETED,
)
if stop_task in done:
raise StopAsyncIteration

return done.pop().result()


class EventReader:
"""Reads input events from a single device and distributes them.
Expand All @@ -43,6 +71,7 @@ def __init__(
context: Context,
source: evdev.InputDevice,
forward_to: evdev.UInput,
stop_event: asyncio.Event,
) -> None:
"""Initialize all mapping_handlers
Expand All @@ -58,6 +87,7 @@ def __init__(
self._source = source
self._forward_to = forward_to
self.context = context
self.stop_event = stop_event

def send_to_handlers(self, event: InputEvent) -> bool:
"""Send the event to callback."""
Expand Down Expand Up @@ -132,9 +162,10 @@ async def run(self):
self._source.fd,
)

async for event in self._source.async_read_loop():
async for event in _ReadLoop(self._source, self.stop_event):
await self.handle(InputEvent.from_event(event))

self.context.reset()
# This happens all the time in tests because the async_read_loop stops when
# there is nothing to read anymore. Otherwise tests would block.
logger.error('The async_read_loop for "%s" stopped early', self._source.path)
12 changes: 11 additions & 1 deletion inputremapper/injection/injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class Injector(multiprocessing.Process):
_state: int
_msg_pipe: multiprocessing.Pipe
_consumer_controls: List[EventReader]
_stop_event: asyncio.Event

regrab_timeout = 0.2

Expand All @@ -118,6 +119,7 @@ def __init__(self, group: _Group, preset: Preset) -> None:
self.context = None # only needed inside the injection process

self._consumer_controls = []
self._stop_event = None

super().__init__(name=group)

Expand Down Expand Up @@ -262,6 +264,11 @@ async def _msg_listener(self) -> None:
msg = self._msg_pipe[0].recv()
if msg == CLOSE:
logger.debug("Received close signal")
self._stop_event.set()
# give the event pipeline some time to reset devices
# before shutting the loop down
await asyncio.sleep(0.1)

# stop the event loop and cause the process to reach its end
# cleanly. Using .terminate prevents coverage from working.
loop.stop()
Expand Down Expand Up @@ -306,6 +313,7 @@ def run(self) -> None:
# create this within the process after the event loop creation,
# so that the macros use the correct loop
self.context = Context(self.preset)
self._stop_event = asyncio.Event()

# grab devices as early as possible. If events appear that won't get
# released anymore before the grab they appear to be held down
Expand Down Expand Up @@ -349,7 +357,9 @@ def run(self) -> None:
raise e

# actually doing things
consumer_control = EventReader(self.context, source, forward_to)
consumer_control = EventReader(
self.context, source, forward_to, self._stop_event
)
coroutines.append(consumer_control.run())
self._consumer_controls.append(consumer_control)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def notify(
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput = None,
forward: evdev.UInput,
supress: bool = False,
) -> bool:
if event.type_and_code != self._input_event.type_and_code:
Expand Down Expand Up @@ -121,3 +121,7 @@ def notify(
forward=forward,
supress=supress,
)

def reset(self) -> None:
self._active = False
self._sub_handler.reset()
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ def notify(
asyncio.ensure_future(self._run())
return True

def reset(self) -> None:
self._stop = True

@staticmethod
def _calc_qubic(x: float, k: float) -> float:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ def notify(

return False

def reset(self) -> None:
self._last_value = 0
self._active = False
self._sub_handler.reset()

def needs_wrapping(self) -> bool:
return True

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ def notify(
self._output_state = bool(event.value)
return self._sub_handler.notify(event, source, forward, supress)

def reset(self) -> None:
self._sub_handler.reset()
for key in self._key_map:
self._key_map[key] = False
self._output_state = False

def get_active(self) -> bool:
"""return if all keys in the keymap are set to True"""
return False not in self._key_map.values()
Expand Down
4 changes: 4 additions & 0 deletions inputremapper/injection/mapping_handlers/hierarchy_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def notify(
handler.notify(event, source, forward, supress=True)
return success

def reset(self) -> None:
for sub_handler in self.handlers:
sub_handler.reset()

def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
if self._input_event.type == EV_ABS and self._input_event.value != 0:
return {EventCombination(self._input_event): HandlerEnums.abs2btn}
Expand Down
7 changes: 7 additions & 0 deletions inputremapper/injection/mapping_handlers/key_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ def notify(self, event: InputEvent, *_, **__) -> bool:
except exceptions.Error:
return False

def reset(self) -> None:
logger.debug("resetting key_handler")
if self._active:
event_tuple = (*self._maps_to, 0)
global_uinputs.write(event_tuple, self.mapping.target_uinput)
self._active = False

def needs_wrapping(self) -> bool:
return True

Expand Down
5 changes: 5 additions & 0 deletions inputremapper/injection/mapping_handlers/macro_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ def f(ev_type, code, value) -> None:

return True

def reset(self) -> None:
self._active = False
if self._macro.is_holding():
self._macro.release_trigger()

def needs_wrapping(self) -> bool:
return True

Expand Down
4 changes: 4 additions & 0 deletions inputremapper/injection/mapping_handlers/mapping_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ def notify(
) -> bool:
...

def reset(self) -> None:
"""reset the state of the handler e.g. release any buttons"""
...


class HandlerEnums(enum.Enum):
# converting to btn
Expand Down
3 changes: 3 additions & 0 deletions inputremapper/injection/mapping_handlers/null_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ def notify(
supress: bool = False,
) -> bool:
return True

def reset(self) -> None:
pass
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def notify(
# the axis is below the threshold and the stage_release function is running
event = event.modify(value=0, action=EventActions.as_key)
logger.debug_key(event.event_tuple, "sending to sub_handler")
self._abort_release = True # abort the stage release
self._abort_release = True
self._active = False
return self._sub_handler.notify(event, source, forward, supress)
else:
Expand All @@ -123,3 +123,10 @@ def notify(
asyncio.ensure_future(self._stage_release(source, forward, supress))
self._active = True
return self._sub_handler.notify(event, source, forward, supress)

def reset(self) -> None:
if self._active:
self._abort_release = True

self._active = False
self._sub_handler.reset()
57 changes: 55 additions & 2 deletions tests/unit/test_event_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def setUp(self):
# print("in setup")
# global_uinputs.prepare_all()
self.forward_uinput = evdev.UInput()
self.stop_event = asyncio.Event()

def tearDown(self) -> None:
cleanup()
Expand All @@ -94,10 +95,11 @@ def get_event_reader(
self, preset: Preset, source: evdev.InputDevice
) -> EventReader:
context = Context(preset)
return EventReader(context, source, self.forward_uinput)
return EventReader(context, source, self.forward_uinput, self.stop_event)

async def test_any_event_as_button(self):
"""as long as there is an event handler and a mapping we should be able to map anything to a button"""
"""as long as there is an event handler and a mapping we should be able
to map anything to a button"""

w_down = (
EV_ABS,
Expand Down Expand Up @@ -194,6 +196,57 @@ async def test_any_event_as_button(self):
self.assertEqual(history.count((EV_KEY, code_a, 0)), 1)
self.assertEqual(history.count((EV_KEY, code_s, 0)), 1)

async def test_reset_releases_keys(self):
"""make sure that macros and keys are releases when the stop event is set"""
preset = Preset()
preset.add(get_key_mapping(combination="1,1,1", output_symbol="hold(a)"))
preset.add(get_key_mapping(combination="1,2,1", output_symbol="b"))
preset.add(
get_key_mapping(combination="1,3,1", output_symbol="modify(c,hold(d))")
)
event_reader = self.get_event_reader(preset, InputDevice("/dev/input/event10"))

a = system_mapping.get("a")
b = system_mapping.get("b")
c = system_mapping.get("c")
d = system_mapping.get("d")

await self.send_events(
[
InputEvent.from_tuple((1, 1, 1)),
InputEvent.from_tuple((1, 2, 1)),
InputEvent.from_tuple((1, 3, 1)),
],
event_reader,
)
await asyncio.sleep(0.1)

fw_history = convert_to_internal_events(self.forward_uinput.write_history)
kb_history = convert_to_internal_events(
global_uinputs.get_uinput("keyboard").write_history
)

self.assertEqual(len(fw_history), 0)
# a down, b down, c down, d down
self.assertEqual(len(kb_history), 4)

event_reader.context.reset()
await asyncio.sleep(0.1)

fw_history = convert_to_internal_events(self.forward_uinput.write_history)
kb_history = convert_to_internal_events(
global_uinputs.get_uinput("keyboard").write_history
)

self.assertEqual(len(fw_history), 0)
# all a, b, c, d down+up
self.assertEqual(len(kb_history), 8)
kb_history = kb_history[-4:]
self.assertIn((1, a, 0), kb_history)
self.assertIn((1, b, 0), kb_history)
self.assertIn((1, c, 0), kb_history)
self.assertIn((1, d, 0), kb_history)

async def test_abs_to_rel(self):
"""map gamepad EV_ABS events to EV_REL events"""

Expand Down
3 changes: 2 additions & 1 deletion tests/unit/test_event_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
class TestEventReader(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.gamepad_source = evdev.InputDevice("/dev/input/event30")
self.stop_event = asyncio.Event()
self.preset = Preset()

def tearDown(self):
Expand All @@ -62,7 +63,7 @@ def setup(self, source, mapping):
forward_to = evdev.UInput()
context = Context(mapping)
context.uinput = evdev.UInput()
consumer_control = EventReader(context, source, forward_to)
consumer_control = EventReader(context, source, forward_to, self.stop_event)
# for consumer in consumer_control._consumers:
# consumer._abs_range = (-10, 10)
asyncio.ensure_future(consumer_control.run())
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,7 @@ async def test_if_tap_none(self):
self.assertListEqual(self.result, [])

# second param none
macro = parse("if_tap(key(y), , 50)", self.context)
macro = parse("if_tap(key(y), , 50)", self.context, DummyMapping)
self.assertEqual(len(macro.child_macros), 1)
y = system_mapping.get("y")
macro.press_trigger()
Expand Down

0 comments on commit 0338427

Please sign in to comment.