Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
99d3ba2
linux: deliver the hotplug ENUMERATE pass asynchronously
Youw Jul 13, 2026
d9c9926
mac: deliver the hotplug ENUMERATE pass asynchronously on the event t…
Youw Jul 13, 2026
cd6eeca
windows: deliver the hotplug ENUMERATE pass asynchronously on the eve…
Youw Jul 13, 2026
ffa28c8
libusb: deliver the hotplug ENUMERATE pass asynchronously on the even…
Youw Jul 13, 2026
1506abe
mac: address round-1 review findings for the hotplug backend
Youw Jul 13, 2026
a3350c4
windows: address round-1 review findings on the async hotplug ENUMERA…
Youw Jul 13, 2026
b447616
linux: address the hotplug review findings
Youw Jul 14, 2026
31b6d7d
libusb: fix the hotplug races, lost events and failure paths found in…
Youw Jul 14, 2026
93086d9
mac: fix the deadlocks and races found in the round-2 hotplug review
Youw Jul 14, 2026
1f621c1
windows: address round-2 review findings on the async hotplug ENUMERA…
Youw Jul 14, 2026
5e3fa82
tests: hotplug API and virtual-device hotplug scenarios
Youw Jul 14, 2026
0ddecf2
windows: address round-3 review findings on the async hotplug ENUMERA…
Youw Jul 14, 2026
b27d1d0
linux: fix the hotplug teardown races and the monitor-thread error wr…
Youw Jul 14, 2026
ee3e7e0
libusb: keep the global error state, the device paths and hid_exit() …
Youw Jul 14, 2026
4667fab
windows: dedupe hotplug arrivals against the cache, not the wall clock
Youw Jul 14, 2026
7494623
linux: detach the hotplug monitor thread and lock its monitor_fd read
Youw Jul 14, 2026
d1e8e7e
windows: make the hotplug cache lookups allocation-free
Youw Jul 14, 2026
77d4124
linux: join the hotplug monitor thread and stop fabricating removals
Youw Jul 14, 2026
06c3032
libusb: close hid_exit() teardown race and reattach kernel driver on …
Youw Jul 14, 2026
4307e16
libusb: re-create usb_context after a register parks across a teardown
Youw Jul 14, 2026
c8edb1a
linux: harden the hotplug monitor join and recovery reconcile
Youw Jul 14, 2026
98d2354
mac: do not treat MACH_PORT_NULL as a device identity in hotplug matc…
Youw Jul 14, 2026
cc60780
mac: suppress global-error writes on the hotplug event thread
Youw Jul 15, 2026
77d6cae
linux/hidraw: drop hotplug monitor recovery; fix the thread-join life…
Youw Jul 15, 2026
64ef449
libusb: dedupe the hidapi_thread_create doc comment
Youw Jul 15, 2026
7827d8f
windows: keep the internal event context from writing the global error
Youw Jul 15, 2026
a1a0319
windows: detect the hotplug event context with a thread-local flag
Youw Jul 15, 2026
e202abf
linux: don't reap a new monitor generation started during the join wi…
Youw Jul 15, 2026
9c0b299
windows: use a portable thread-local for the hotplug-callback flag
Youw Jul 15, 2026
bfe82aa
linux: track monitor threads on a retired list so none is dropped
Youw Jul 15, 2026
72b00d7
linux/hid.c: pre-allocate the retired monitor node at thread creation
Youw Jul 15, 2026
ab3d6f4
Merge win (final) into hotplug-integration (CI validation vehicle)
Youw Jul 15, 2026
71e09cc
Merge mac (final) into hotplug-integration (CI validation vehicle)
Youw Jul 15, 2026
d12fedc
Merge usb (final) into hotplug-integration (CI validation vehicle)
Youw Jul 15, 2026
c433825
Merge lin (final) into hotplug-integration (CI validation vehicle)
Youw Jul 15, 2026
592bb78
Merge tests (final) into hotplug-integration (CI validation vehicle)
Youw Jul 15, 2026
0ddd060
ci: run the winapi hotplug tests in the win-vhid job, not just DeviceIO
Youw Jul 15, 2026
f91e127
tests: implement raw-gadget presence toggling for the hotplug test
Youw Jul 15, 2026
431bbbb
tests: toggle the vhidmini2 root devnode for the Windows hotplug test
Youw Jul 15, 2026
9815325
tests: locate the Windows vhid devnode by hardware id, not instance path
Youw Jul 15, 2026
7a0ac34
tests: re-enumerate the vhid subtree on replug so the HID child returns
Youw Jul 15, 2026
c9eabaa
tests: re-enumerate the function devnode itself, not its parent, on r…
Youw Jul 15, 2026
ffb03c1
tests: toggle the HID child PDO, not the UMDF function device, for ho…
Youw Jul 15, 2026
5cad314
tests: align the Windows vhid device identity with the hotplug test
Youw Jul 15, 2026
de7f521
tests: drop the win vhid debug diagnostics now that the test passes
Youw Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/win-vhid-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ jobs:
shell: pwsh
working-directory: build
run: |
ctest -C Release -R DeviceIO_winapi --output-on-failure
ctest -C Release -R "_winapi" --output-on-failure

- name: Cleanup virtual device
if: always()
Expand Down
1,114 changes: 939 additions & 175 deletions libusb/hid.c

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions libusb/hidapi_thread_pthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,14 @@ static void hidapi_thread_barrier_wait(hidapi_thread_state *state)
pthread_barrier_wait(&state->barrier);
}

static void hidapi_thread_create(hidapi_thread_state *state, void *(*func)(void*), void *func_arg)
{
pthread_create(&state->thread, NULL, func, func_arg);
/* Starts `func` on a new thread. Returns 0 on success, or a non-zero value when
the thread could not be started (in which case `state->thread` is left unset
and must not be joined). NOTE: HIDAPI checks this result, so an out-of-tree
thread model supplied through HIDAPI_THREAD_MODEL_INCLUDE must return `int` as
well - a model that still declares this function `void` no longer compiles. */
static int hidapi_thread_create(hidapi_thread_state *state, void *(*func)(void*), void *func_arg)
{
return pthread_create(&state->thread, NULL, func, func_arg);
}

static void hidapi_thread_join(hidapi_thread_state *state)
Expand Down
1,402 changes: 1,222 additions & 180 deletions linux/hid.c

Large diffs are not rendered by default.

1,280 changes: 1,100 additions & 180 deletions mac/hid.c

Large diffs are not rendered by default.

75 changes: 71 additions & 4 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,94 @@ function(hidapi_add_vdev_test name provider backend)
)
endfunction()

# Define a tier-1 hotplug-API test <name> built from test_hotplug_api.c only
# (NO virtual-device provider: it needs no device and no privileges, so it runs
# in the ordinary CI matrix), linked against the HIDAPI <backend>. It
# self-skips (77) when the backend reports hotplug as unsupported at runtime
# (e.g. a libusb without LIBUSB_CAP_HAS_HOTPLUG).
function(hidapi_add_hotplug_api_test name backend)
add_executable(${name} test_hotplug_api.c)
set_target_properties(${name} PROPERTIES
C_STANDARD 11
C_STANDARD_REQUIRED TRUE
)
target_link_libraries(${name} PRIVATE ${backend} Threads::Threads)
if(HIDAPI_ENABLE_ASAN AND NOT MSVC)
target_link_options(${name} PRIVATE -fsanitize=address)
endif()
add_test(NAME ${name} COMMAND ${name})
set_tests_properties(${name} PROPERTIES
SKIP_RETURN_CODE 77
TIMEOUT 120
)
endfunction()

# Define a tier-2 (device-backed) hotplug test <name> built from
# test_hotplug.c + <provider>. Self-skips when no virtual device can be
# created here, when the backend reports hotplug as unsupported, or when the
# provider cannot toggle device presence (test_virtual_device_unplug/_replug);
# currently only the uhid provider implements presence toggling.
# <event_timeout_ms> is the per-event wait budget inside the test;
# <ctest_timeout> bounds the whole run.
function(hidapi_add_hotplug_test name provider backend event_timeout_ms ctest_timeout)
add_executable(${name} test_hotplug.c ${provider})
set_target_properties(${name} PROPERTIES
C_STANDARD 11
C_STANDARD_REQUIRED TRUE
)
target_compile_definitions(${name} PRIVATE
TEST_HOTPLUG_EVENT_TIMEOUT_MS=${event_timeout_ms})
target_link_libraries(${name} PRIVATE ${backend} Threads::Threads)
if(HIDAPI_ENABLE_ASAN AND NOT MSVC)
target_link_options(${name} PRIVATE -fsanitize=address)
endif()
add_test(NAME ${name} COMMAND ${name})
set_tests_properties(${name} PROPERTIES
SKIP_RETURN_CODE 77
TIMEOUT ${ctest_timeout}
)
endfunction()

# --- Linux: hidraw backend via /dev/uhid -----------------------------------
if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_hidraw)
hidapi_add_vdev_test(DeviceIO_hidraw test_virtual_device_uhid.c hidapi_hidraw)
hidapi_add_hotplug_api_test(HotplugAPI_hidraw hidapi_hidraw)
hidapi_add_hotplug_test(Hotplug_hidraw test_virtual_device_uhid.c hidapi_hidraw 10000 120)
endif()

# --- Linux: libusb backend via /dev/raw-gadget (+ dummy_hcd) ----------------
# Built whenever the libusb backend is, so it keeps compiling; at runtime it
# self-skips unless the raw-gadget virtual device has been set up (CI job).
if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_libusb)
hidapi_add_vdev_test(DeviceIO_libusb test_virtual_device_rawgadget.c hidapi_libusb)
hidapi_add_hotplug_api_test(HotplugAPI_libusb hidapi_libusb)
# Self-skips until the rawgadget provider implements unplug/replug; the
# generous budgets anticipate the full (virtual) USB stack round trips.
hidapi_add_hotplug_test(Hotplug_libusb test_virtual_device_rawgadget.c hidapi_libusb 30000 300)
endif()

# --- Windows: winapi backend via a modified vhidmini2 UMDF driver -----------
if(WIN32 AND TARGET hidapi_winapi)
hidapi_add_vdev_test(DeviceIO_winapi test_virtual_device_win.c hidapi_winapi)
# HidD_GetPreparsedData / HidP_GetCaps used by the Windows provider.
target_link_libraries(DeviceIO_winapi PRIVATE hid)
hidapi_add_hotplug_api_test(HotplugAPI_winapi hidapi_winapi)
# Self-skips until the vhidmini2 provider implements unplug/replug.
hidapi_add_hotplug_test(Hotplug_winapi test_virtual_device_win.c hidapi_winapi 30000 300)
# hid: HidD_GetPreparsedData / HidP_GetCaps (device caps).
# cfgmgr32: CM_Locate_DevNodeA / CM_Disable_DevNode / CM_Enable_DevNode, used
# by the provider to toggle the root devnode's presence (unplug/replug).
# Set on the target (not via #pragma comment(lib), which MinGW ignores) so
# every winapi toolchain -- MSVC, clang-cl and MinGW -- links it.
target_link_libraries(DeviceIO_winapi PRIVATE hid cfgmgr32)
target_link_libraries(Hotplug_winapi PRIVATE hid cfgmgr32)
# Run from the directory holding the hidapi DLL so a shared build can find
# it at launch (there is no rpath on Windows).
set_tests_properties(DeviceIO_winapi PROPERTIES
set_tests_properties(DeviceIO_winapi HotplugAPI_winapi Hotplug_winapi PROPERTIES
WORKING_DIRECTORY "$<TARGET_FILE_DIR:hidapi_winapi>")
# With ASan (MSVC) the test exe needs the ASan runtime DLL, which lives next
# to the MSVC tools; add it to PATH (CMake >= 3.22).
if(HIDAPI_ENABLE_ASAN AND MSVC AND NOT CMAKE_VERSION VERSION_LESS "3.22")
get_filename_component(MSVC_BUILD_TOOLS_DIR "${CMAKE_LINKER}" DIRECTORY)
set_property(TEST DeviceIO_winapi PROPERTY
set_property(TEST DeviceIO_winapi HotplugAPI_winapi Hotplug_winapi PROPERTY
ENVIRONMENT_MODIFICATION "PATH=path_list_append:${MSVC_BUILD_TOOLS_DIR}")
endif()
endif()
Expand All @@ -83,6 +145,11 @@ endif()
# available (e.g. hosted CI runners); usable locally / on a self-hosted Mac.
if(APPLE AND TARGET hidapi_darwin)
hidapi_add_vdev_test(DeviceIO_darwin test_virtual_device_mac.c hidapi_darwin)
hidapi_add_hotplug_api_test(HotplugAPI_darwin hidapi_darwin)
# Self-skips until the IOHIDUserDevice provider implements unplug/replug.
hidapi_add_hotplug_test(Hotplug_darwin test_virtual_device_mac.c hidapi_darwin 30000 300)
target_link_libraries(DeviceIO_darwin PRIVATE
"-framework IOKit" "-framework CoreFoundation")
target_link_libraries(Hotplug_darwin PRIVATE
"-framework IOKit" "-framework CoreFoundation")
endif()
44 changes: 44 additions & 0 deletions src/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,47 @@ command bytes, expected payloads).
| Test | What it exercises |
|------|-------------------|
| `test_device_io.c` | open → write an output report → trigger+read input reports (Feature-report write, then input-report read-back) → close |
| `test_hotplug_api.c` | tier-1 hotplug API contract, no device needed: argument validation, handle properties, implicit init, `hid_exit()` teardown, register/deregister thread churn |
| `test_hotplug.c` | tier-2 hotplug scenarios against a virtual device whose presence is toggled: async delivery, exactly-once ENUMERATE pass, callback-return deregistration, pass-before-live ordering, payloads, filtering, dispatch order, deregistration post-condition, re-entrant registration |

## Hotplug tests

The hotplug tests come in two tiers:

* **Tier 1 — `HotplugAPI_<backend>`** (`test_hotplug_api.c`): everything in the
hotplug contract observable *without* a device event. Needs no virtual
device, no privileges, so it runs against **every** backend in the ordinary
per-push CI matrix. Self-skips (77) when the backend reports hotplug as
unsupported at runtime (e.g. a libusb without `LIBUSB_CAP_HAS_HOTPLUG`).
* **Tier 2 — `Hotplug_<backend>`** (`test_hotplug.c`): device-backed hotplug
scenarios. On top of a virtual device, the provider must be able to *toggle
the device's presence* (`test_virtual_device_unplug()` /
`test_virtual_device_replug()` in `test_virtual_device.h`). Currently only
the **uhid** provider implements toggling (a `UHID_DESTROY` /
`UHID_CREATE2` pair on the same open `/dev/uhid` fd), so `Hotplug_hidraw`
is the one tier-2 test that actually runs (in `builds.yml`'s ubuntu-cmake
job, like `DeviceIO_hidraw`); the other providers return
`TEST_VDEV_UNAVAILABLE` from the toggle calls and their `Hotplug_*` tests
self-skip everywhere until presence toggling is implemented for them.

| Test | Runs per-push in `builds.yml` | Notes |
|------|-------------------------------|-------|
| `HotplugAPI_hidraw` | yes (ubuntu-cmake) | |
| `HotplugAPI_libusb` | yes (ubuntu-cmake) | needs libusb hotplug support at runtime |
| `HotplugAPI_winapi` | yes (windows-cmake, MSVC/NMake/ClangCL/MinGW) | |
| `HotplugAPI_darwin` | yes (macos-cmake) | |
| `Hotplug_hidraw` | yes (ubuntu-cmake, via `uhid`) | the only tier-2 test that runs today |
| `Hotplug_libusb` | builds, self-skips | needs rawgadget unplug/replug (future) |
| `Hotplug_winapi` | builds, self-skips | needs driver-side presence toggling (future) |
| `Hotplug_darwin` | builds, self-skips | needs `IOHIDUserDevice` re-creation (future) |

The tier-2 test is written against strict synchronization rules (hotplug tests
are notoriously flaky otherwise): callbacks only deep-copy the event into a
log under a lock; every expectation is awaited with a deadline-based predicate
poll (never a bare sleep); the *absence* of an event is asserted behind an
**event barrier** — a later event that is provably ordered after the missing
one — never behind a time window; and a missed event within the (generous)
budget is treated as a bug, not retried.

## Providers

Expand Down Expand Up @@ -90,6 +131,9 @@ cmake -B build -S . -DHIDAPI_WITH_TESTS=ON
cmake --build build
sudo modprobe uhid
sudo ctest --test-dir build -R DeviceIO_hidraw --output-on-failure
sudo ctest --test-dir build -R Hotplug_hidraw --output-on-failure
# tier-1 hotplug API tests need no device and no root:
ctest --test-dir build -R HotplugAPI --output-on-failure
```

On Windows/macOS configure with `-DHIDAPI_WITH_TESTS=ON` and run `ctest`; the
Expand Down
Loading
Loading