Skip to content

windows: deliver hotplug ENUMERATE pass asynchronously on the event context#823

Open
Youw wants to merge 2 commits into
connection-callbackfrom
hotplug-async-enumerate-windows
Open

windows: deliver hotplug ENUMERATE pass asynchronously on the event context#823
Youw wants to merge 2 commits into
connection-callbackfrom
hotplug-async-enumerate-windows

Conversation

@Youw

@Youw Youw commented Jul 13, 2026

Copy link
Copy Markdown
Member

Implements the hotplug contract documented via #790 for the windows backend (windows/hid.c only).

What changed

  • Asynchronous ENUMERATE pass. hid_hotplug_register_callback no longer runs the HID_API_HOTPLUG_ENUMERATE pass synchronously on the calling thread. Instead it takes a deep-copied snapshot of the matching entries of the cached device list (under the hotplug critical section, only when the flag is set and events includes DEVICE_ARRIVED) into a per-callback replay list, and submits a threadpool work item (CreateThreadpoolWork/SubmitThreadpoolWork) that delivers it on the same critical-section-serialized context that delivers live CM_Register_Notification events. The pass never fires from within register itself and never on an application thread.
  • Exactly once, pass-before-live. Live dispatch flushes a callback's pending replay before delivering any live event to it, and is bounded to the callbacks present at event time, so a callback registered from inside another callback picks the in-flight device up from its snapshot rather than from the live loop (previously it could see it twice). device->next is now NULL for every invocation, including the synthetic ones (the old sync pass handed out devices still linked into the cached list).
  • Callback return value honored during the pass (Hotplug: HID_API_HOTPLUG_ENUMERATE should honour callback return value #793, windows): a non-zero return stops the remainder of the pass, frees the undelivered snapshot and deregisters the callback.
  • Deregister return value: 0 only when the handle was found and deregistered; -1 for unknown, stale or already-self-deregistered handles (safe no-op).
  • Error strings: all register/deregister failure paths (including argument validation) set the global error retrievable via hid_error(NULL); *callback_handle is zeroed on any registration failure.
  • Teardown safety: CM_Unregister_Notification is documented to deadlock when called from its own notification callback, and it also deadlocks if called while holding a lock the notification callback takes. The teardown now detaches the notification handle under the critical section and unregisters outside it; when the last callback removes itself from inside the notification callback, the actual teardown is deferred to the threadpool work item. hid_exit frees undelivered snapshots, waits for in-flight work items (WaitForThreadpoolWorkCallbacks) before deleting the critical section, and sweeps anything a last-gasp notification appended to the device cache.

Design notes

  • The work item is a single reusable PTP_WORK created with the first callback registration and closed by hid_exit. I used CreateThreadpoolWork rather than TrySubmitThreadpoolCallback because teardown needs WaitForThreadpoolWorkCallbacks to guarantee no work item can touch the critical section after hid_exit deletes it; the fire-and-forget API offers no way to wait.
  • Replay flushing is idempotent and runs under the critical section with mutex_in_use set: whichever of the work item or a live event acquires the lock first delivers the pass, the other finds replay == NULL.
  • Registration reuses an already-registered CM notification when a deferred teardown has not run yet (and refreshes the cached device list), instead of failing as the old code did.
  • The threadpool API needs Vista-level declarations, so the file now raises _WIN32_WINNT to 0x0600 when it is set lower (the dynamically-resolved CM notification API already requires Windows 8 at runtime).

Addresses #793 for the windows backend.

Assisted-by: claude-code:claude-fable-5

Youw added 2 commits July 14, 2026 01:40
…nt context

Implement the hotplug contract documented in #790 for the windows backend:

The HID_API_HOTPLUG_ENUMERATE initial pass is now a deep-copied
registration-time snapshot of the matching connected devices, replayed on
a threadpool work item (the same critical-section-serialized event context
that delivers live events) instead of running synchronously inside
hid_hotplug_register_callback on the application thread. A callback's
pending snapshot is flushed before any live event is delivered to it, so
each device connection is reported exactly once and the pass always
precedes live events; a non-zero callback return stops the remainder of
the pass and deregisters the callback.

Also per the contract:
- hid_hotplug_deregister_callback returns -1 for unknown or stale handles
  (0 only when the callback was found and deregistered)
- every register/deregister failure path sets the global error string,
  and *callback_handle is zeroed on registration failure
- undelivered snapshots are freed on deregistration and hid_exit
- CM_Unregister_Notification is no longer called from within the
  notification callback (forbidden, deadlocks) nor while holding the
  hotplug critical section (deadlocks against in-flight notifications):
  the teardown is detached under the lock and performed outside it,
  deferred to the work item when triggered from the notification itself

Addresses #793 for the windows backend.

Assisted-by: claude-code:claude-fable-5
…TE pass

- Arm the CM notification BEFORE taking the registration-time device
  snapshot, and deduplicate arrivals by path in the notification callback:
  a device connecting between the two is now caught by the notification
  and absorbed by the dedupe instead of being missed by both (it was
  reported by neither before)
- Only (re)build the device cache when actually arming a notification;
  while one is attached the cache is kept current by its callbacks
  (re-enumerating during a deferred teardown could double-report)
- Serialize new-notification arming against in-flight
  CM_Unregister_Notification calls (pending counter + condition variable):
  a detached handle stays live at the OS until its unregistration
  completes, and overlapping registrations would deliver events twice;
  hid_exit waits for the same quiescence before destroying state
- Treat a failed CM_Unregister_Notification as a poisoned epoch: hid_exit
  reports the failure, returns -1 and deliberately leaks the critical
  section, work item and loaded DLLs rather than let a still-live
  notification touch destroyed state
- Guard the machinery bootstrap with a statically-initialized SRWLOCK
  (two racing first registrations could both initialize the critical
  section), and serialize all mutations of the global error string with
  another SRWLOCK (concurrent thread-safe failures could double-free it)
- Distinguish a genuinely failed enumeration from an empty system at
  registration (fail the registration on the former), and clear the stale
  "No HID devices found" error on successful registration
- Fail registration with an error when the callback handle space is
  exhausted instead of signed-overflowing and wrapping onto live handles
- Replace { 0 } struct initializers with memset in hid.c for
  -Wmissing-field-initializers cleanliness when built as C++ with GNU
  compilers; use the WinAPI error helper for CreateThreadpoolWork failures

Addresses round-1 review of the windows part of #793.

Assisted-by: claude-code:claude-fable-5
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