Skip to content

fix(injector): read from all devnodes sharing an origin_hash - #1314

Open
nickbock wants to merge 1 commit into
sezanzeb:mainfrom
nickbock:fix/origin-hash-node-collision
Open

nickbock wants to merge 1 commit into
sezanzeb:mainfrom
nickbock:fix/origin-hash-node-collision

Conversation

@nickbock

Copy link
Copy Markdown

Fixes #1309

Problem

On a device that exposes multiple /dev/input/event* nodes, the injector could grab and start its read loop on the wrong node, so the mapped button never produced any output. Observed with a SteelSeries Aerox 9 Wireless: the mapped button emits on one event node, but the injector read from a different one and nothing was injected. A single-interface device (e.g. a plain keyboard) was unaffected.

Root cause

Device selection in Injector keyed everything on get_device_hash(), which is md5(str(capabilities) + name). Two issues:

  1. Hash collisions. That hash is not unique per devnode. Some hardware (the Aerox 9 among them) exposes several event nodes with byte-identical capabilities and name, so they produce the same origin_hash. _find_input_device built devices_by_hash = {hash: device} and _grab_devices returned {hash: device} — both collapse colliding nodes to a single entry, so only one twin was grabbed/read. If the event is emitted on the other twin, it is silently missed.

  2. Over-strict capability gate. _find_input_device additionally required the mapped code to be present in the node's advertised capabilities. Nodes that emit codes outside their static capabilities had their valid origin_hash discarded, and _update_preset then overwrote it via the device-type fallback — pointing the injector at a different node.

Fix

  • Replace _find_input_device with _find_input_devices, returning every devnode whose hash matches the origin_hash (and dropping the capability gate — a matching hash already guarantees identical capabilities to recording time, so the recorded node is authoritative).
  • Key _grab_devices by path instead of hash, so colliding nodes are all grabbed.
  • Start one read loop per devnode in run(). Forwarding devices stay keyed by hash (one per unique hash; colliding nodes are interchangeable for forwarding).

When origin_hash is absent or matches nothing (old presets / configs moved between machines), behavior is unchanged: the existing _find_input_device_fallback still runs.

Tests

Adds two regression tests in tests/unit/test_injector.py:

  • test_grabs_all_nodes_sharing_an_origin_hash — two hash-identical nodes are both grabbed (the Aerox 9 case).
  • test_reads_from_origin_hash_node_on_multi_interface_device — a node that under-reports the mapped code in its capabilities is still honored, not overwritten by the fallback.

Both fail on main and pass with the fix. test_injector.py, test_event_pipeline, test_groups, test_event_reader and test_context pass (the two pre-existing subprocess-spawning failures in test_injector fail identically on main and are unrelated to this change).

Verified live on the affected SteelSeries Aerox 9 Wireless: the mapped button now works.

🤖 Generated with Claude Code

@nickbock
nickbock force-pushed the fix/origin-hash-node-collision branch from dde9a7f to 4afe426 Compare June 14, 2026 08:12
@nickbock
nickbock force-pushed the fix/origin-hash-node-collision branch from 4afe426 to 070c36f Compare June 27, 2026 06:45
When a hardware device exposes multiple /dev/input/event nodes, the
injector picked which node(s) to grab and start read loops on by keying
devices on get_device_hash(). Two problems caused it to read from the
wrong node and silently miss events:

1. Hash collisions. get_device_hash is md5(capabilities + name), which is
   not unique per devnode. Some devices (e.g. SteelSeries Aerox 9 Wireless)
   expose several event nodes with byte-identical capabilities and name, so
   they share an origin_hash. Keying devices_by_hash / grabbed_devices by
   hash collapsed these collisions to a single entry, so the injector
   grabbed and read from the wrong twin and never saw the mapped button.

2. Over-strict capability gate. _find_input_device also required the mapped
   code to be in the node's advertised capabilities, which discarded a
   valid origin_hash for nodes that emit codes outside their static
   capabilities, causing _update_preset to fall back to a different node.

Replace _find_input_device with _find_input_devices, which returns every
devnode whose hash matches the origin_hash (no capability gate). Key
_grab_devices by path instead of hash so colliding nodes are all grabbed,
and start one read loop per devnode. Forwarding devices stay keyed by hash
(one per unique hash, since colliding nodes are interchangeable there).

Adds regression tests for both the hash-collision and capability-underreport
cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@nickbock
nickbock force-pushed the fix/origin-hash-node-collision branch from 070c36f to 2d9a6b6 Compare June 27, 2026 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Listening to wrong /dev/input/eventXX

1 participant