fix(hidpp): guard message listener lifetimes#299
Conversation
Greptile SummaryThis PR fixes a latent deadlock in the HID++ message dispatch loop where a listener that tried to add or remove another listener during dispatch would deadlock on the
Confidence Score: 5/5Safe to merge; the deadlock fix is correctly scoped and all eight listener call sites are fully migrated. The dispatch loop change is minimal and correct — two non-overlapping lock windows replace one that covered both matching and dispatch. The Weak-backed RAII guard handles all lifetime orderings cleanly, and the regression test directly exercises the previously deadlocking scenario. All feature and receiver structs are completely migrated with no leftover manual Drop impls or raw handles. channel.rs is the only file worth a second look — the missing #[must_use] on MessageListenerGuard is a minor gap worth addressing before the guard API sees wider use. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant RT as Read Thread
participant PM as pending_messages Mutex
participant ML as message_listeners Mutex
participant L1 as Listener 1 (Arc)
participant L2 as Listener 2 (Arc)
RT->>PM: lock → match response predicate
PM-->>RT: release lock
RT->>ML: lock → clone all Arc-Fn into Vec
ML-->>RT: release lock
Note over RT: dispatch outside both locks
RT->>L1: listener(msg, matched)
L1->>ML: remove_msg_listener(hdl) no deadlock
RT->>L2: listener(msg, matched)
Note over L2: guard drop also safe here
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant RT as Read Thread
participant PM as pending_messages Mutex
participant ML as message_listeners Mutex
participant L1 as Listener 1 (Arc)
participant L2 as Listener 2 (Arc)
RT->>PM: lock → match response predicate
PM-->>RT: release lock
RT->>ML: lock → clone all Arc-Fn into Vec
ML-->>RT: release lock
Note over RT: dispatch outside both locks
RT->>L1: listener(msg, matched)
L1->>ML: remove_msg_listener(hdl) no deadlock
RT->>L2: listener(msg, matched)
Note over L2: guard drop also safe here
Reviews (2): Last reviewed commit: "fix(hidpp): keep listener guard opaque" | Re-trigger Greptile |
| impl MessageListenerGuard { | ||
| /// Returns the raw listener handle managed by this guard. | ||
| pub fn handle(&self) -> u32 { | ||
| self.hdl | ||
| } |
There was a problem hiding this comment.
handle() leaks the raw handle out of an otherwise opaque RAII guard
MessageListenerGuard::handle() is public but is never called anywhere in this PR (nor in any migrated site). Exposing it creates a footgun: a caller who retrieves the handle and passes it to remove_msg_listener will cause the guard's drop to silently skip the removal (entry already gone), while the guard still "looks active" to its owner. If the use case is only internal debugging or a future escape-hatch, pub(crate) would prevent accidental misuse from outside the crate, or the method could be removed entirely now that the migrated callers no longer need it.
f5e0d10 to
23daf93
Compare
Summary
Validation
Stacked on #298.