Skip to content

fix: Steam Controller survives suspend/resume (hidraw/evdev hotplug hardening) - #14

Merged
moyoseph merged 3 commits into
mainfrom
fix/hidraw-hotplug-resume
Sep 25, 2026
Merged

moyoseph merged 3 commits into
mainfrom
fix/hidraw-hotplug-resume

Conversation

@moyoseph

Copy link
Copy Markdown
Owner

Problem

On the reference HTPC, the Steam Controller stopped unblanking the TV after every suspend/resume cycle until the agent was restarted.

Root causes

Three compounding bugs in the _HotplugMonitor skeleton:

  1. HidrawMonitor.WATCH_DIR = b"/dev/hidraw" is not a directory — hidraw nodes live directly in /dev. inotify_add_watch failed silently, so hidraw hotplug ran only on the 45 s fallback rescan.
  2. EOF reads never evicted a device — after USB re-enumeration a dead fd reads b"" (not OSError), so it was fed to _handle forever.
  3. Same-name re-creation was invisible to _scan — a node deleted and re-created as the same /dev/hidrawN is present in both glob and _fds, so it was never reopened; the stale fd and its stale detector persisted indefinitely.

Fix

  • Watch /dev with name filtering (WATCH_PREFIX = b"hidraw" / b"event"), parsing the inotify event stream so unrelated /dev churn doesn't trigger rescans; IN_Q_OVERFLOW forces one. Mask gains IN_ATTRIB so udev's post-create ACL application retries an EACCES open.
  • Evict on EOF.
  • _scan revalidates open fds against the node's (st_ino, st_rdev) and reopens on mismatch — a replaced hidraw node gets a fresh SteamControllerActivity (fresh warmup + runaway guard).
  • The periodic rescan now always runs as a backstop, even with inotify active.

The evdev InputMonitor gains the same hardening.

Also includes a small chore commit fixing two pre-existing errors surfaced by mypy 2.x (unnarrowed Future.exception() raise; list/set variable redefinition).

Tests

New coverage: inotify name filtering (match / non-match / queue overflow), EOF eviction (fd + detector), same-name inode-swap reopen with fresh detector, unchanged-node fd stability, stat-failure staleness, WATCH_DIR/WATCH_PREFIX regression assert, Linux-only real inotify watch on /dev. 524 passed, ruff + mypy clean.

🤖 Generated with Claude Code

Mohamed Yosef and others added 2 commits September 25, 2026 14:02
raise of an unnarrowed Future.exception() and a list/set variable
redefinition — both pre-existing on main, tripped by the newer mypy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Steam Controller went dead after every sleep/wake cycle because the
hotplug skeleton couldn't see USB re-enumeration:

- HidrawMonitor watched /dev/hidraw for inotify events — that path is not
  a directory (nodes are /dev/hidrawN), so inotify setup failed silently
  and hotplug degraded to the 45s rescan. Watch /dev instead, filtering
  events to hidraw* names (new WATCH_PREFIX; InputMonitor filters event*).
- An EOF read from a re-enumerated device never evicted the dead fd
  (only OSError did); it now does.
- A node re-created under the SAME name was never reopened (_scan dedups
  by path). _scan now revalidates open fds against the node's inode/rdev
  and reopens on mismatch — which also hands hidraw nodes a fresh
  detector (fresh warmup + runaway guard).
- The periodic rescan now always runs, even when inotify is available,
  as a backstop for events lost across suspend.
- Watch mask gains IN_ATTRIB: udev applies the uaccess ACL after
  IN_CREATE, so a create-time EACCES open gets retried on the chmod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A suspend can leave the hidraw nodes fully intact (no re-enumeration, as
observed on the reference box) — in that case none of the hotplug fixes
engage, but the detectors' volatility masks are stale and the controller's
post-resume re-sync burst can permanently latch the runaway guard,
killing detection until the agent restarts.

Detect the resume directly: (CLOCK_BOOTTIME - CLOCK_MONOTONIC) grows by
exactly the time spent suspended, so a jump in that delta is a reliable,
logind-free resume signal. On a jump, every watched node gets a fresh
SteamControllerActivity (fresh warmup + guard). Controller sleep/wake
does not move the delta, so the first press after a controller nap is
still detected immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@moyoseph
moyoseph merged commit 37f1fd4 into main Sep 25, 2026
6 checks passed
@moyoseph
moyoseph deleted the fix/hidraw-hotplug-resume branch September 25, 2026 11:40
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.

1 participant