Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 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
6753c0b
tests: run the device-backed hotplug scenarios on libusb and Windows
Youw Jul 15, 2026
aaf66c0
tests: fix hotplug/virtual-device review findings
Youw Aug 4, 2026
72169eb
mac: ignore hotplug devices with no backing io_service_t
Youw Aug 4, 2026
8b87914
libusb: destroy the main context under `mutex` in hid_exit()
Youw Aug 4, 2026
97760f9
linux: fix hotplug self-deadlock, quiet detection and degraded device…
Youw Aug 4, 2026
31a799f
windows: keep hotplug-only APIs out of the mandatory init path
Youw Aug 4, 2026
c54ecc6
tests: keep the rawgadget provider off fd 0 and report early ep0 exit
Youw Aug 4, 2026
18434e0
linux: identify the hotplug monitor thread by its generation id
Youw Aug 4, 2026
f1b7cc6
Merge commit '18434e0' into hotplug-tests-integration
Youw Aug 4, 2026
cc1a308
Merge commit '31a799f' into hotplug-tests-integration
Youw Aug 4, 2026
65ade92
Merge commit '72169eb' into hotplug-tests-integration
Youw Aug 4, 2026
34e85ab
Merge commit '8b87914' into hotplug-tests-integration
Youw Aug 4, 2026
79804fc
Merge commit 'c54ecc6' into hotplug-tests-integration
Youw Aug 4, 2026
0ba2720
Clarify hotplug documentation and NetBSD failure reporting
Youw Sep 8, 2026
179dbfb
Fix Linux hotplug lifecycle and error handling for PR #822
Youw Sep 8, 2026
e33a98f
Fix libusb hotplug review findings for PR #825
Youw Sep 8, 2026
bec001f
linux: use a named timespec for the hotplug poll pacing
Youw Sep 8, 2026
bb3cb69
hotplug docs: do not promise diagnostics from stubs or callbacks
Youw Sep 8, 2026
378f9df
Fix Windows hotplug review findings for PR #823
Youw Sep 8, 2026
5894384
Fix macOS hotplug startup and thread collection
Youw Sep 8, 2026
333a0da
Strengthen hotplug tests and virtual-device CI checks
Youw Sep 8, 2026
b077b35
Harden Linux hotplug joins and receive error handling
Youw Sep 8, 2026
1707c33
Fix second-round libusb hotplug findings for PR #825
Youw Sep 8, 2026
1c84663
Fix macOS hotplug destructor thread identification
Youw Sep 8, 2026
f94a68e
Preserve Windows recovered hotplug event ordering
Youw Sep 8, 2026
750e041
Preserve delayed libusb hotplug arrivals after reconciliation
Youw Sep 8, 2026
cd008b0
Fix round-two hotplug test and Windows provider findings
Youw Sep 8, 2026
d37cdab
Merge commit 'b077b35' into hotplug-tests-integration
Youw Sep 8, 2026
4407c38
Merge commit 'f94a68e' into hotplug-tests-integration
Youw Sep 8, 2026
0fa2786
Merge commit '750e041' into hotplug-tests-integration
Youw Sep 8, 2026
897ffa6
Merge commit 'cd008b0' into hotplug-tests-integration
Youw Sep 8, 2026
76f946a
Merge commit '1c84663' into hotplug-tests-integration
Youw Sep 8, 2026
a35f00a
Avoid locking for macOS hotplug thread identification
Youw Sep 8, 2026
0e941da
Merge commit 'a35f00a' into hotplug-tests-integration
Youw Sep 8, 2026
8976d38
Refine libusb hotplug removal recovery
Youw Sep 8, 2026
4db5fd4
Document Windows hotplug recovery semantics
Youw Sep 8, 2026
fcb2578
tests: fix MSVC warnings-as-errors and relax the T15 same-thread assu…
Youw Sep 8, 2026
1031012
Merge branch 'hotplug-tests' into hotplug-tests-integration
Youw Sep 8, 2026
74ef38c
Merge commit '4db5fd4' into hotplug-tests-integration
Youw Sep 8, 2026
e18c099
Merge commit '8976d38' into hotplug-tests-integration
Youw Sep 8, 2026
4fba0a2
Merge commit 'bb3cb69' into hotplug-tests-integration
Youw Sep 8, 2026
685a850
mac: refuse to start the hotplug thread without a published thread id
Youw Sep 8, 2026
9fdce41
Merge branch 'hotplug-async-enumerate-mac' into hotplug-tests-integra…
Youw Sep 8, 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
21 changes: 19 additions & 2 deletions .github/vmrun-libusb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,25 @@ modprobe dummy_hcd || true
modprobe raw_gadget || true
ls -l /dev/raw-gadget || true

ctest --test-dir build --output-on-failure
rc=$?
rc=0
for test in DeviceIO_libusb HotplugAPI_libusb Hotplug_libusb; do
listed=$(ctest --test-dir build -N -R "^${test}$" 2>&1)
listed_rc=$?
printf '%s\n' "$listed"
if [ "$listed_rc" -ne 0 ] || ! printf '%s\n' "$listed" | grep -q 'Total Tests: 1'; then
echo "Required CTest case '${test}' was not found."
rc=1
continue
fi

result=$(ASAN_OPTIONS=detect_leaks=0 ctest --test-dir build -R "^${test}$" --output-on-failure 2>&1)
result_rc=$?
printf '%s\n' "$result"
if [ "$result_rc" -ne 0 ] || ! printf '%s\n' "$result" | grep -Eq "^[[:space:]]*1/1 Test #[0-9]+: ${test} .* [P]assed[[:space:]]+[0-9]+([.][0-9]+)?[[:space:]]+sec[[:space:]]*$"; then
echo "Required CTest case '${test}' did not pass."
rc=1
fi
done

echo "=== diag ==="
lsmod | grep -E "raw_gadget|dummy_hcd|udc" || true
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ jobs:
- name: Build CMake Framework
working-directory: build/framework
run: make install
- name: Run virtual-device tests (IOHIDUserDevice self-skips on hosted CI)
- name: Run device-I/O and hotplug lifecycle tests
working-directory: build/shared
run: |
# The macOS virtual device needs the com.apple.developer.hid.virtual.device
# entitlement and interactive user consent, neither available on a hosted
# runner, so DeviceIO_darwin self-skips (CTest code 77). This still
# verifies the provider builds and the test runs/links.
ASAN_OPTIONS=detect_leaks=0 ctest --output-on-failure
# Verbose output also reports individual hotplug cases skipped when
# no real device is available; device-independent lifecycle checks run.
ASAN_OPTIONS=detect_leaks=0 ctest --verbose --output-on-failure
- name: Check artifacts
uses: andstor/file-existence-action@v2
with:
Expand Down
26 changes: 18 additions & 8 deletions .github/workflows/libusb-vhid-test.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
name: Linux libusb Virtual HID Device Test (manual)

# Runs the device-I/O test against the HIDAPI *libusb* backend using a real
# virtual USB HID device (USB Raw Gadget on top of dummy_hcd).
# Runs the device-I/O and hotplug tests against the HIDAPI *libusb* backend
# using a real virtual USB HID device (USB Raw Gadget on top of dummy_hcd).
#
# The hosted ubuntu-latest (azure) kernel is built without the USB gadget
# subsystem, so raw_gadget/dummy_hcd can't be loaded (or even built) there. We
# therefore run the test inside a lightweight VM (virtme-ng + QEMU) booting a
# *generic* Ubuntu kernel, whose linux-modules-extra ships dummy_hcd and
# raw_gadget. The VM shares the host filesystem, so it runs the binaries built
# *generic* Ubuntu kernel: linux-modules-extra supplies raw_gadget; Ubuntu does
# not package dummy_hcd, so this workflow builds it from matching upstream
# kernel source against that kernel's headers and installs it alongside. The VM
# shares the host filesystem, so it runs the binaries built
# on the host. The same approach works locally and on WSL2 (which also lacks
# those modules in its default kernel).
#
Expand Down Expand Up @@ -42,6 +44,8 @@ jobs:
find "/lib/modules/${KVER}" \( -name 'raw_gadget*' -o -name 'dummy_hcd*' \) || true

- name: Build dummy_hcd for the generic kernel (Ubuntu ships no package)
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -eux
KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1)
Expand All @@ -50,8 +54,14 @@ jobs:
# Ubuntu packages no dummy_hcd; build it from the upstream source that
# matches the generic kernel's major version (xairy's copy tracks newer
# kernels and won't compile against an older one).
curl -fsSL -o dummyhcd/dummy_hcd.c \
"https://raw.githubusercontent.com/torvalds/linux/v${KMAJ}/drivers/usb/gadget/udc/dummy_hcd.c"
# Fetch through the authenticated contents API: anonymous runner fetches
# from raw.githubusercontent.com are rate-limited (HTTP 429) often enough
# to fail the job, and the API path counts against the token's quota.
curl -fsSL --retry 6 --retry-delay 15 --retry-all-errors \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.raw+json" \
-o dummyhcd/dummy_hcd.c \
"https://api.github.com/repos/torvalds/linux/contents/drivers/usb/gadget/udc/dummy_hcd.c?ref=v${KMAJ}"
printf 'obj-m += dummy_hcd.o\n' > dummyhcd/Makefile
make -C "/lib/modules/${KVER}/build" M="${PWD}/dummyhcd" modules
sudo install -m 0644 "${PWD}/dummyhcd/dummy_hcd.ko" \
Expand All @@ -61,10 +71,10 @@ jobs:
- name: Build HIDAPI + tests (libusb backend)
run: |
cmake -B build -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DHIDAPI_WITH_LIBUSB=ON -DHIDAPI_WITH_HIDRAW=OFF -DHIDAPI_WITH_TESTS=ON
-DHIDAPI_WITH_LIBUSB=ON -DHIDAPI_WITH_HIDRAW=OFF -DHIDAPI_WITH_TESTS=ON -DHIDAPI_ENABLE_ASAN=ON
cmake --build build

- name: Run DeviceIO_libusb inside a VM (generic kernel + raw_gadget)
- name: Run the virtual-device tests inside a VM (generic kernel + raw_gadget)
run: |
set -eux
# The generic kernel just installed (has dummy_hcd + raw_gadget modules).
Expand Down
25 changes: 19 additions & 6 deletions .github/workflows/win-vhid-test.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Windows Virtual HID Device Test (manual)

# Builds and self-signs a modified vhidmini2 UMDF2 driver, installs it on a
# hosted runner, then runs the backend-agnostic device-I/O test against that
# real virtual HID device (winapi backend). It installs a driver, so it is not
# part of the per-push CI matrix; run it on demand from the Actions tab, or by
# hosted runner, then runs the backend-agnostic device-I/O and hotplug tests
# against that real virtual HID device (winapi backend). It installs a driver,
# so it is not part of the per-push CI matrix; run it on demand from the Actions tab, or by
# adding the 'ci-virtual-device' label to a pull request.
#
# The work is split across two runners because no single hosted image can do
Expand Down Expand Up @@ -138,14 +138,27 @@ jobs:
- name: Build HIDAPI + tests
shell: pwsh
run: |
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DHIDAPI_WITH_TESTS=ON
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DHIDAPI_WITH_TESTS=ON -DHIDAPI_ENABLE_ASAN=ON
cmake --build build --config Release

- name: Run device-I/O test against the virtual device
- name: Run device-I/O and hotplug tests against the virtual device
shell: pwsh
working-directory: build
run: |
ctest -C Release -R DeviceIO_winapi --output-on-failure
$env:ASAN_OPTIONS = "detect_leaks=0"
$tests = @("DeviceIO_winapi", "HotplugAPI_winapi", "Hotplug_winapi")
foreach ($test in $tests) {
$listed = & ctest -C Release -N -R "^$test$" 2>&1 | Out-String
if ($LASTEXITCODE -ne 0 -or $listed -notmatch "Total Tests: 1") {
throw "Required CTest case '$test' was not found."
}
$result = & ctest -C Release -R "^$test$" --output-on-failure 2>&1 | Out-String
$passed = "(?m)^\s*1/1 Test #\d+: " + [regex]::Escape($test) + ".*\sPassed\s+\d+(?:\.\d+)?\s+sec\s*$"
Write-Host $result
if ($LASTEXITCODE -ne 0 -or $result -notmatch $passed) {
throw "Required CTest case '$test' did not pass."
}
}

- name: Cleanup virtual device
if: always()
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,29 @@ Keyboards, mice, and some other devices which are blacklisted from having
hidraw nodes will not work. Fortunately, for nearly all the uses of hidraw,
this is not a problem.

Hotplug delivery is best-effort under kernel event-buffer overruns. An overrun
or unrecoverable udev monitor failure stops live events without notifying existing
callbacks; pending initial `ENUMERATE` passes still run. Once the failure is
recorded, new registrations fail with `-1`, so an application can detect it by
attempting a registration. Deregister all surviving callbacks before registering
again to create a fresh monitor.

The initial hotplug snapshot is keyed by `/dev/hidrawN`. If that node is reused
while the monitor is armed and the snapshot is taken, queued events from its
predecessor can produce `ARRIVED/LEFT/ARRIVED` for the successor connection.

#### __Linux/FreeBSD/libusb__ (`libusb/hid.c`):

This back-end uses libusb-1.0 to communicate directly to a USB device. This
back-end will of course not work with Bluetooth devices.

#### __Windows__ (`windows/hid.c`):

Hotplug callbacks require Windows 8 or later. Registration returns `-1` when
the required PnP notification API is unavailable. Compiler and runtime
requirements may impose a higher minimum Windows version.
Starting with HIDAPI 0.16, this back-end requires libusb 1.0.16 or newer.

### Test GUI

HIDAPI also comes with a Test GUI. The Test GUI is cross-platform and uses
Expand Down
8 changes: 4 additions & 4 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ case $host in

# HIDAPI/libusb libs
AC_CHECK_LIB([rt], [clock_gettime], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -lrt"], [hidapi_lib_error librt])
PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0])
PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0])
LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS"
CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS"
;;
Expand All @@ -87,7 +87,7 @@ case $host in
CFLAGS="$CFLAGS -I/usr/local/include"
LDFLAGS="$LDFLAGS -L/usr/local/lib"
LIBS="${LIBS}"
PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0])
PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0])
LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS"
CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS"
AC_CHECK_LIB([iconv], [iconv_open], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -liconv"], [hidapi_lib_error libiconv])
Expand All @@ -100,7 +100,7 @@ case $host in
os="kfreebsd"
threads="pthreads"

PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0])
PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0])
LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS"
CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS"
;;
Expand All @@ -112,7 +112,7 @@ case $host in
os="haiku"
threads="pthreads"

PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0])
PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0])
LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS"
CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS"
AC_CHECK_LIB([iconv], [libiconv_open], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -liconv"], [hidapi_lib_error libiconv])
Expand Down
Loading
Loading