Skip to content

linux: deliver hotplug ENUMERATE pass asynchronously on the event context#822

Open
Youw wants to merge 1 commit into
connection-callbackfrom
hotplug-async-enumerate-linux
Open

linux: deliver hotplug ENUMERATE pass asynchronously on the event context#822
Youw wants to merge 1 commit into
connection-callbackfrom
hotplug-async-enumerate-linux

Conversation

@Youw

@Youw Youw commented Jul 13, 2026

Copy link
Copy Markdown
Member

Implements the hotplug contract documented in hidapi/hidapi.h (#790) for the linux/hidraw backend.

What changed

Asynchronous ENUMERATE pass. hid_hotplug_register_callback() no longer runs the HID_API_HOTPLUG_ENUMERATE initial pass synchronously on the calling thread. Instead it takes a deep-copied, registration-time snapshot of the matching entries of the internal device cache (only when HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED is requested), stores it on the callback record (replay), and the udev monitor thread delivers it as synthetic "arrived" events:

  • never on an application thread and never from within the register call itself;
  • before any live events for that callback — hid_internal_invoke_callbacks() flushes a callback's pending snapshot before handing it a live event, and the monitor thread also flushes pending snapshots at the top of its loop (the existing 5 ms select timeout caps the latency, no extra wakeup fd needed);
  • exactly once per device connection: the snapshot is taken under the hotplug mutex, so a device arrival is reported either by the pass or as a live event, never both;
  • a non-zero callback return stops the remainder of the pass and deregisters the callback (undelivered snapshot entries are freed), same as for live events.

This also covers registrations made while the monitor thread is already running, including registrations made from within another hotplug callback.

device->next is now NULL for every invocation. The udev arrival path used to pass the chained multi-usage list to callbacks (the old TODO); each entry is now temporarily detached for the duration of its invocation, keeping the internal cache intact for later "left" matching.

Error strings and out-parameter. Every failure path of register/deregister (NULL callback, empty/unknown events bits, unknown flags bits, allocation/udev/thread-start failures, unknown or stale deregister handle) now sets the global error string retrievable via hid_error(NULL), and register sets *callback_handle to 0 on failure before any error return.

Monitor-thread lifecycle fixes. Previously, when the last callback removed itself on the monitor thread (non-zero return or in-callback deregister), the thread exited on its own, was never joined, and freed the shared device list / udev monitor unlocked — racing a subsequent first registration that re-creates them. Now the exiting thread releases nothing; joining and monitoring-context teardown are centralized in hid_internal_hotplug_cleanup() (guarded by a thread_started flag) and a new first registration reaps the previous thread before re-creating the context. To make joining with the mutex held deadlock-free, the monitor thread only ever acquires the mutex with trylock and backs off briefly on contention, so it can always make progress to its exit check.

Design decisions / deviations

  • The replay_pending context flag is read unlocked at the top of the monitor loop (mirroring the existing unlocked hotplug_cbs check) and re-checked/cleared under the mutex; worst case the pass is picked up one 5 ms iteration later.
  • On out-of-memory while deep-copying the snapshot, the pass delivers as much as could be copied instead of failing the registration.
  • hid_enumerate(0, 0) inside the first registration may set the "No HID devices found" global error on an empty system; register clears it, since an empty system is not a registration failure.

Verification

  • gcc -fsyntax-only -Wall -Wextra -pedantic -Werror and g++ -fsyntax-only -Wall -Wextra -Werror clean; full CMake builds (C and HIDAPI_BUILD_AS_CXX=ON with -Werror) pass.
  • The WSL2 kernel used for testing has no uhid/hidraw support (CONFIG_UHID not set), so the tests: virtual HID device test harness for all four backends (hidraw, libusb, winapi, darwin) #815 virtual-device harness could not drive real udev events. Instead a throwaway whitebox harness (including linux/hid.c directly and injecting fake entries into the hotplug device cache) exercised: async delivery on the monitor thread (never the registering thread), VID/PID filtering of the snapshot, next == NULL on every invocation, exactly-once vs. later-connected devices, non-zero return stopping the pass, self-deregistration and nested registration from within a callback, stale-handle deregister, last-callback self-removal followed by re-registration (thread reaping), hid_exit() with an undelivered snapshot, and all argument-validation error strings. 10/10 runs clean under ASan+UBSan+LSan; TSan reports only the pre-existing intentional unlocked flag reads. Live udev arrival/removal delivery was not exercised at runtime and deserves a check on real hardware.

Addresses #792 and #793 for the linux/hidraw backend.

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

Implements the hotplug contract documented in hidapi.h for the hidraw
backend:

- hid_hotplug_register_callback() now takes a deep-copied snapshot of
  the matching connected devices and the udev monitor thread replays it
  as synthetic "arrived" events: never on an application thread, never
  from within the register call itself, and always before any live
  events for that callback; a non-zero return stops the remainder of
  the pass and deregisters the callback.
- Live "arrived" events are delivered as one invocation per device
  entry with next == NULL (multi-usage devices previously exposed the
  chained list to callbacks).
- All register/deregister failure paths set the global error string
  and register resets *callback_handle to 0 on failure.
- The monitor thread only takes the mutex with trylock and releases no
  shared state on exit; joining and monitoring-context teardown are
  centralized in hid_internal_hotplug_cleanup(), fixing an unjoined
  thread and a teardown race when the last callback removes itself on
  the monitor thread and a later registration re-creates the context.

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