fix(hid): park the Windows HID read on a permanently dead handle - #779
Open
yuzi-co wants to merge 2 commits into
Open
fix(hid): park the Windows HID read on a permanently dead handle#779yuzi-co wants to merge 2 commits into
yuzi-co wants to merge 2 commits into
Conversation
`RawHidChannel::read_report` documents that an `Err` is transient — the `hidpp` read loop logs it and retries — so an implementation must not surface a condition that will never clear. The non-Windows transport honours that by parking on `HidError::Disconnected`; the Windows one propagated it, so unplugging the cable of a mouse that is also paired over its receiver spun the read loop at 100% of a core indefinitely, 14k iterations per second, and every unplug leaked another such thread. `WindowsHidppChannel` also never overrode `is_connected`, so it reported the trait default of `true` forever. Inventory therefore never learned the channel was dead, `CHANNEL_EVICT_AFTER` never fired, and the gesture watcher re-armed the vanished route once a second for the life of the process. The receiver keeps the HID node enumerated once the cable goes, so node-vanish eviction could not cover for it either. Park on `Disconnected` in all three read paths, track the state in an `AtomicBool`, and report it through `is_connected`. `write_report` marks it too, by downcast — `HidEndpoint::write_report` boxes the `async_hid` error before the channel sees it, so a plain `matches!` there would silently never match, which is what the new test pins down. Fixes AprilNEA#777
Greptile SummaryThe PR fixes Windows HID channel retirement after a permanent disconnect.
Confidence Score: 5/5The PR appears safe to merge. The previously reported fallback-masking failure is fixed because a typed
|
| Filename | Overview |
|---|---|
| crates/openlogi-hid/src/channel/transport/windows.rs | The early return preserves the typed disconnect through the write path, allowing the existing downcast and channel-retirement logic to address the prior finding. |
Reviews (2): Last reviewed commit: "fix(hid): skip the native write fallback..." | Re-trigger Greptile
The fallback exists to work around async-hid output-report quirks on a live device. Reopening a device that is gone can only fail, and the `NativeWriteError` it returns replaces the typed `HidError::Disconnected` the caller needs — so `is_permanent_disconnect` missed it and the channel kept reporting itself connected until the read path caught up. Not hypothetical: in the hardware run for the parent commit the fallback fired three times, all after the disconnect and none before, ending in `AllMethodsFailed`. Surface `Disconnected` directly instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unplugging the cable of a mouse that is also paired over its receiver left
openlogi-agentburning 100% of a CPU core forever. Replugging did not clear it,and every unplug leaked another spinning thread.
RawHidChannel::read_reportdocuments that anErris transient — thehidppread loop logs it and retries — so an implementation must not surface a condition
that will never clear, and should park instead.
AsyncHidChannel(thecfg(not(windows))transport) honours that.WindowsHidppChannelnever did: itpropagated
async_hid::HidError::Disconnectedstraight into the retry loop,which then spun at ~14,000 iterations per second.
The same impl also never overrode
is_connected, so it reported the traitdefault of
truefor the rest of the process's life.inventory::ledgertherefore never learned the channel was dead,
CHANNEL_EVICT_AFTERnever fired,and the gesture watcher re-armed the vanished route once a second indefinitely.
Node-vanish eviction could not cover for it either — the receiver keeps the HID
node enumerated after the cable goes, which is exactly the case
ledger.rscalls out as needing
is_connected.macOS and Linux are unaffected; they compile the other implementation.
Changes
openlogi-hid —
crates/openlogi-hid/src/channel/transport/windows.rsWindowsHidppChannel::read_reportparks onHidError::Disconnectedin allthree read paths — both
tokio::select!arms and the single-endpoint arm —instead of returning an error the read loop retries.
connected: AtomicBool, cleared bymark_disconnected,and report it through a new
is_connectedoverride so the inventory ledgercan evict the channel.
write_reportmarks the disconnect too.HidEndpoint::write_reportboxes theasync_hiderror before the channel sees it, so that path recognises thevariant by downcast; a plain
matches!there would silently never fire, whichis what the new unit test pins down.
HidEndpoint::write_reportno longer attempts the native Windows fallback on aDisconnectedhandle. That fallback works around async-hid output-reportquirks on a live device; reopening a device that is gone can only fail, and
the
NativeWriteErrorit returned replaced the typedDisconnectedthechannel needs to retire itself (review finding,
c49ee67).Testing
Two pre-existing failures on this host, both unrelated to this diff (which
touches one file in
openlogi-hid):xtask'scommands::ci::jobs::tests::ci_yml_runs_what_this_runner_runs— alsofails on
masterwith the change reverted.openlogi-permissions(
unresolved link to PermissionStatus::Unknown, from0f028a8), the knownWindows-only breakage.
cargo doc -p openlogi-hid --no-deps --document-private-itemsunderRUSTDOCFLAGS="-D warnings"is clean.The Linux/macOS cross-lint from
.claude/rules/cross-platform.mdcould not runlocally — only
x86_64-pc-windows-msvcis installed on this host and devenv isunavailable. Audited by hand against
masterinstead: every item added here sitsinside a
#[cfg(target_os = "windows")]block, the only ungated items in the fileare pre-existing and untouched, and the new names (
is_permanent_disconnect,park_disconnected, theconnectedfield) collide with nothing.mark_disconnectedshares a name withAsyncHidChannel's, on a different type inthe
cfg(not(windows))impl. CI'sclippy,clippy (windows),tests (linux),tests (macos)and MSRV lanes are green, which is the actual confirmation.Verified on hardware (Windows 11, G502 LIGHTSPEED on receiver
c539plus USBcable
c08d), running each patched build through the exact repro — dongleconnected, cable added, cable removed — and sampling the agent for four minutes
afterwards:
9e2c214c49ee67read_report errortrace linesre-armingwarningsHID channel disconnectedAllMethodsFailed* the 2.8% sample is at agent startup, before the cable cycle; every
post-disconnect window is at or below 0.06 s.
HID channel disconnectedrising from one to two onc49ee67is the write pathmarking the disconnect instead of losing it to
NativeWriteError.The device recovers rather than wedging: the probe fails transiently, the ledger
replays the last-known identity, and the receiver path re-enumerates the G502 on
slot 1 with
capture re-arm generation=2.Fixes #777