diff --git a/.github/vmrun-libusb.sh b/.github/vmrun-libusb.sh index 49a49ee42..ccab019e5 100644 --- a/.github/vmrun-libusb.sh +++ b/.github/vmrun-libusb.sh @@ -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 diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 4c059aca6..df132f898 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -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: diff --git a/.github/workflows/libusb-vhid-test.yml b/.github/workflows/libusb-vhid-test.yml index bfe6d3128..e0f05a9c6 100644 --- a/.github/workflows/libusb-vhid-test.yml +++ b/.github/workflows/libusb-vhid-test.yml @@ -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). # @@ -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) @@ -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" \ @@ -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). diff --git a/.github/workflows/win-vhid-test.yml b/.github/workflows/win-vhid-test.yml index 3d814e758..819bfffc4 100644 --- a/.github/workflows/win-vhid-test.yml +++ b/.github/workflows/win-vhid-test.yml @@ -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 @@ -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() diff --git a/README.md b/README.md index cf5e6e02e..250527fa4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/configure.ac b/configure.ac index 1b2051052..14710cf5c 100644 --- a/configure.ac +++ b/configure.ac @@ -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" ;; @@ -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]) @@ -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" ;; @@ -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]) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index 5eb1f9b52..b76b9b802 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -198,10 +198,15 @@ extern "C" { This function initializes the HIDAPI library. Calling it is not strictly necessary, as it will be called automatically by - hid_enumerate() and any of the hid_open_*() functions if it is - needed. This function should be called at the beginning of - execution however, if there is a chance of HIDAPI handles - being opened by different threads simultaneously. + hid_enumerate(), any of the hid_open_*() functions and + hid_hotplug_register_callback() if it is needed (see the latter + for the macOS thread-binding caveat). This function should be + called explicitly at the beginning of execution (on macOS, from + the thread that will later call hid_exit()) whenever HIDAPI is + used from more than one thread, to avoid a racing implicit + initialization; hid_enumerate()/hid_open*() themselves must + still be serialized across threads - see the Multi-threading + Notes in the project wiki. @ingroup API @@ -217,6 +222,15 @@ extern "C" { HIDAPI. It should be called at the end of execution to avoid memory leaks. + Since version 0.16.0: stops the hotplug machinery, blocking until + any in-progress hotplug callback returns, and deregisters all + remaining hotplug callbacks (their handles become invalid). + Must not be called from within a hotplug callback, and must not + run concurrently with hid_hotplug_register_callback() or + hid_hotplug_deregister_callback() on another thread. On macOS it + must be called from the thread that initialized the library + (explicitly or implicitly, see hid_hotplug_register_callback()). + @ingroup API @returns @@ -321,6 +335,21 @@ extern "C" { disconnects and reconnects is a new connection and is reported again.) + These guarantees describe the hand-off between the initial + pass and live events and assume HIDAPI can observe and + describe the device. Delivery is best-effort under resource + exhaustion: a device whose description cannot be allocated + when it arrives, or an interface the backend cannot describe + because it cannot open it for its attributes (as with + hid_enumerate()), is not reported at all, and no "left" event + follows for it; each device entry for a connection is reported + at most once to the same callback. If the device-notification + transport fails irrecoverably, no further events are delivered to the + registered callbacks. On linux/hidraw (udev monitor socket + failure) and macOS (event thread failure), new registrations + then fail until every callback has been deregistered, after + which a new registration can restart the machinery. + The initial pass is delivered before any live events for this callback. In particular, a callback registered with this flag for both event types never observes a @@ -347,9 +376,14 @@ extern "C" { @par Execution context The callback is only ever invoked on HIDAPI's internal event - context, never on an application thread (including the - application's main thread). This includes the synthetic - "arrived" events requested with #HID_API_HOTPLUG_ENUMERATE: + context (an internal thread on most backends; on Windows, OS + notification callbacks and a threadpool work item, with callback + invocations serialized), never on an application thread + (including the application's main thread). Do not rely on thread + identity, thread-local state or COM apartment inside the callback. + + This execution context also applies to the synthetic "arrived" + events requested with #HID_API_HOTPLUG_ENUMERATE: they are delivered asynchronously on that same context and are never delivered from within the hid_hotplug_register_callback() call itself. (When a hotplug callback itself registers a new @@ -367,6 +401,12 @@ extern "C" { that is what makes them safe to call from inside the callback (see below). Keep the callback short. + Consequently the callback must never block waiting on a thread + that may be calling hid_hotplug_register_callback(), + hid_hotplug_deregister_callback() or hid_exit(), or the two will + deadlock; hand work off asynchronously (queue + wake, never + post-and-join). + When multiple callbacks are registered, each event is delivered to every matching callback sequentially, in the order the callbacks were registered. @@ -379,34 +419,41 @@ extern "C" { - hid_hotplug_register_callback() - hid_hotplug_deregister_callback() (including on its own handle) - - hid_error(dev) with a non-NULL device handle, provided no - other thread uses that same handle concurrently - - HIDAPI calls made from within the callback do not update the - global error string: the callback runs on HIDAPI's internal - event context, and internal contexts never write that string - (an application has no way to serialize against them, so writing - it there would be a use-after-free waiting to happen). Failures - are still reported through return values as usual, and - hid_error(dev) still works for a device handle - only - hid_error(NULL) is left untouched by calls made from the - callback. + + Per-device calls such as hid_error(dev) on a non-NULL handle + the callback owns follow the normal per-device rule (no + concurrent use of that handle from another thread). + + Calls to the hotplug API (hid_hotplug_register_callback() and + hid_hotplug_deregister_callback()) made from within the callback + do not update the global error string. Failures of these calls + are reported only through their -1 return values; + hid_error(NULL) does not provide a failure reason for them. Any other HIDAPI function follows HIDAPI's general thread-safety rule (see the Multi-threading Notes in the project wiki): it is the application's responsibility to serialize hid_init / hid_exit / hid_enumerate / hid_open* / hid_close / hid_error(NULL) across all - threads, including the hotplug callback thread (hid_exit() + threads, including HIDAPI's internal event context (hid_exit() additionally must never be called from within the callback - itself - see below). If your application + itself - see below). Functions in this group may update the + global error string, which is why they must be serialized + against hid_error(NULL); whether a call made from the internal + event context records a diagnostic at all is not portable (some + backends suppress every global-error write in that context), so + rely on return values there. If your application already calls those functions only from one thread, calling them - from the hotplug callback adds a second thread and is therefore + from the internal event context is therefore UNSAFE unless the application adds synchronisation itself. The recommended pattern is to copy the needed fields of @p device out of the callback and handle open/close on your own thread. Calling hid_exit() from within the callback has undefined behavior: - hid_exit() joins the hotplug thread, which would be joining itself. + hid_exit() tears down the hotplug machinery and normally waits + for the internal event context to drain (joining its thread or + waiting for OS notification callbacks). Calling it from within + a callback can wait on itself or invalidate state still in use + by the callback. @par The device parameter @@ -418,20 +465,32 @@ extern "C" { The @p device->next pointer is always NULL. Each callback invocation describes exactly one device; compound or composite - devices that expose multiple interfaces produce multiple callback - invocations (typically delivered in quick succession). + devices that expose multiple interfaces - or, on backends that + enumerate one entry per top-level usage (linux/hidraw, macOS), + multiple usages - produce one callback invocation per entry + hid_enumerate() would list for them (typically delivered in + quick succession). Such sibling entries may share the same + path and differ only in usage_page/usage. For #HID_API_HOTPLUG_EVENT_DEVICE_LEFT events @p device points to a copy captured when the device arrived (or was enumerated): all fields, including the strings, are valid and describe the device as it was while connected. A "left" event is delivered for any matching device that disconnects while the callback is - registered, including devices that were already connected + registered, subject to the best-effort delivery caveat under + #HID_API_HOTPLUG_ENUMERATE, including devices already connected before the registration (their arrival is reported to this callback only if #HID_API_HOTPLUG_ENUMERATE was used). When the callback has observed the device's arrival, the path field - matches the one reported then and may be used to correlate the - two events. + matches the one reported then; use path together with + usage_page/usage (and interface_number) to correlate a specific + sibling's two events. + + As with hid_enumerate(), string fields may be NULL when the + backend could not read them at arrival time (e.g. the libusb + backend on a device it cannot open yet, before udev permission + rules apply); re-enumerate from an application thread if they + are needed. @par Return value @@ -471,12 +530,17 @@ extern "C" { If @p vendor_id and @p product_id are both set to 0, then all HID devices will be notified. If HIDAPI is not initialized yet, this function initializes it - implicitly (as if by hid_init()). On some backends this binds - HIDAPI's device-monitoring facilities to the calling thread (for - example, the macOS backend schedules its run loop there). An - application that cares which thread owns those facilities should - call hid_init() explicitly from that thread first, rather than - relying on the implicit initialization performed here. + implicitly (as if by hid_init()). That implicit initialization + follows the same rules as an explicit hid_init() (see the + Multi-threading Notes): it must not run concurrently with + hid_init()/hid_exit()/hid_enumerate()/hid_open*()/hid_close()/ + hid_error(NULL) on another thread. On macOS the registering + thread then becomes the thread that must later call hid_exit() + and must stay alive until then: the library's IOHIDManager used + by hid_enumerate()/hid_open*() is scheduled on that thread's run + loop; hotplug events are still delivered on HIDAPI's internal + event context, never on that thread. Call hid_init() explicitly + from your HIDAPI thread before registering to avoid this. When #HID_API_HOTPLUG_ENUMERATE is set, the synthetic "arrived" events are delivered asynchronously on HIDAPI's internal event @@ -490,30 +554,41 @@ extern "C" { @par Thread safety - hid_hotplug_register_callback() and hid_hotplug_deregister_callback() - are thread-safe with respect to each other and to HIDAPI's - internal hotplug machinery. They may be called from any thread, - including from within a hotplug callback. This is a deliberate - exception to HIDAPI's general "not thread-safe" rule (see the + Once the library is initialized, hid_hotplug_register_callback() + and hid_hotplug_deregister_callback() are thread-safe with + respect to each other and to HIDAPI's internal hotplug machinery. + They may be called from any thread, including from within a + hotplug callback. This is a deliberate exception to HIDAPI's + general "not thread-safe" rule (see the Multi-threading Notes in the project wiki). - - The one caveat is the global error string: on failure these two - functions set it, like every other HIDAPI function that reports - an error via hid_error(NULL). They therefore have to be - serialized against hid_error(NULL) - which the application is - already required to serialize across all threads - even though - they need no serialization against each other. HIDAPI's own - internal threads never write the global error string, so an - application that serializes its own hid_error(NULL) calls - against its other HIDAPI calls is safe. + They must, however, be serialized against hid_init() and + hid_exit(): no hotplug API call may be in flight on any thread + while hid_exit() runs, and handles obtained before hid_exit() + must not be passed to hid_hotplug_deregister_callback() afterwards. + + The one caveat is the global error string. Called from an + application thread, these two functions write it like the + functions in the Multi-threading Notes group: they set it on + failure; a successful registration also resets it, as by + hid_init(). An application must therefore serialize them against + every call that touches that string - hid_init(), hid_exit(), + hid_enumerate(), hid_open*(), hid_close() and hid_error(NULL) - + on other threads, including calls made from a hotplug callback. + The two hotplug functions need no serialization against each + other, including when called from within a callback. In that + context they leave the global error string untouched and only + the -1 return is available on failure (see #hid_hotplug_callback_fn). The first successful call to hid_hotplug_register_callback() - starts HIDAPI's internal hotplug machinery (on most platforms an - internal thread), which runs until either (a) the last callback - is deregistered, or (b) hid_exit() is called. hid_exit() - deregisters any callbacks that are still registered and - invalidates their handles. hid_exit() must not be called from - within a hotplug callback (see #hid_hotplug_callback_fn). + starts HIDAPI's internal event context, which runs until + (a) the last callback is deregistered, (b) hid_exit() is called, + or (c) the backend's monitoring facility fails (see + #HID_API_HOTPLUG_ENUMERATE). hid_exit() deregisters any callbacks + that are still registered and invalidates their handles. + Calling hid_exit() from within a + hotplug callback has undefined behavior: teardown can wait on + the current callback or invalidate state it still uses + (see #hid_hotplug_callback_fn). @ingroup API @@ -532,7 +607,10 @@ extern "C" { @returns This function returns 0 on success or -1 on error. - Call hid_error(NULL) to get the failure reason. + Call hid_error(NULL) to get the failure reason (not applicable + to a call made from within a hotplug callback). On success + the global error string is reset, as by hid_init(), unless + this call is made from within a hotplug callback. Registration fails if @p callback is NULL, if @p events contains no valid #hid_hotplug_event bit, or if @p events or @p flags contain unknown bits. When more than one argument @@ -541,7 +619,20 @@ extern "C" { return and the zeroed @p callback_handle are guaranteed. @note On backends without hotplug support (e.g. NetBSD) - this function always returns -1. + this function always returns -1 and leaves the global + error string untouched. On Windows, hotplug + requires Windows 8 or later (CM_Register_Notification): on + older versions hid_init() succeeds but this function returns + -1 with an explanatory hid_error(NULL) message. On the libusb + backend it additionally requires a libusb that reports + LIBUSB_CAP_HAS_HOTPLUG at runtime; otherwise both this + function and hid_hotplug_deregister_callback() return -1. + + @note On macOS the first registration (and the first after the + last deregistration) opens the system's HID devices through + an IOHIDManager: it may fail with -1 if a device is seized + exclusively by another process or Input Monitoring access + is denied, and may trigger the Input Monitoring prompt. */ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle); @@ -549,11 +640,17 @@ extern "C" { Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) - Thread-safe. May be called from any thread, including from within - a hotplug callback (on its own handle or on another callback's - handle). Calling it on a handle that was already deregistered, - or on a handle that was never valid, is safe: it has no effect - and returns -1. + Thread-safe (subject to the initialization and global-error + caveats under "Thread safety" on #hid_hotplug_register_callback). + May be called from any thread, including from within a hotplug + callback (on its own handle or on another callback's handle). + Calling it on a handle that was already deregistered, or on a + handle that was never valid, is safe while the library remains + initialized (see #hid_hotplug_callback_handle): it does not + affect any registered callback and returns -1 (it may still + complete a pending internal wind-down before returning). + Handles obtained before hid_exit() must not be reused after a + subsequent hid_init(). When called from any thread other than HIDAPI's internal event context, this function does not return until an in-progress @@ -576,6 +673,9 @@ extern "C" { This function returns 0 when the callback was found and deregistered, or -1 on error (including when @p callback_handle is not a registered handle). + Call hid_error(NULL) to get the failure reason (not applicable + to a call made from within a hotplug callback). + On success the global error string is left unchanged. */ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle); diff --git a/libusb/CMakeLists.txt b/libusb/CMakeLists.txt index 62efd3c1e..a18252146 100644 --- a/libusb/CMakeLists.txt +++ b/libusb/CMakeLists.txt @@ -13,7 +13,7 @@ if(TARGET usb-1.0) target_link_libraries(hidapi_libusb PRIVATE usb-1.0) else() include(FindPkgConfig) - pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.9) + pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.16) target_link_libraries(hidapi_libusb PRIVATE PkgConfig::libusb) endif() diff --git a/libusb/hid.c b/libusb/hid.c index abf10bf8f..11a20194b 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -30,8 +30,10 @@ #include #include #include +#include #include #include +#include /* Unix */ #include @@ -159,12 +161,73 @@ static libusb_context *usb_context = NULL; static hidapi_error_ctx last_global_error; +/* Serializes mutations of the global error state: hid_hotplug_register_callback() + * and hid_hotplug_deregister_callback() are thread-safe by contract and their + * failure paths write last_global_error concurrently (an unserialized + * register_string_error() would double-free the stored string). */ +static pthread_mutex_t hid_global_error_mutex = PTHREAD_MUTEX_INITIALIZER; + +/* Identity of the hotplug callback thread - the one HIDAPI-owned thread that can + * reach the public API, as it is the thread the callbacks run on. + * + * The global error state belongs to the application: hid_error(NULL) frees and + * replaces the cached string, and an application cannot serialize its own calls + * against a thread it does not know about. Registering or deregistering a + * callback from within a callback is explicitly allowed (see hidapi.h), and both + * write the global error state on failure, for example when deregistering an + * already removed handle. Those writes are therefore DROPPED on this thread + * (see register_libusb_error()/register_string_error()): + * a call that fails there still reports the failure through its return value, + * only the error string of the process is left alone. + * + * Guarded by hid_global_error_mutex, which is a leaf lock (never held while + * another one is acquired), so this adds no lock-order edge. Only one callback + * thread exists at a time (see threads_running). */ +static pthread_t hid_callback_thread_id; +static unsigned char hid_callback_thread_id_valid; + +/* Called by the callback thread as its first and last action */ +static void hid_internal_callback_thread_enter(void) +{ + pthread_mutex_lock(&hid_global_error_mutex); + hid_callback_thread_id = pthread_self(); + hid_callback_thread_id_valid = 1; + pthread_mutex_unlock(&hid_global_error_mutex); +} + +static void hid_internal_callback_thread_leave(void) +{ + pthread_mutex_lock(&hid_global_error_mutex); + hid_callback_thread_id_valid = 0; + pthread_mutex_unlock(&hid_global_error_mutex); +} + +/* Called with hid_global_error_mutex held */ +static int hid_internal_on_callback_thread(void) +{ + return hid_callback_thread_id_valid && pthread_equal(pthread_self(), hid_callback_thread_id); +} + struct hid_hotplug_queue { + /* The device this message is about; NULL marks a request to flush + * the pending HID_API_HOTPLUG_ENUMERATE snapshots (a "replay marker") */ libusb_device* device; - int event; /* Arrived or removed */ + int event; /* Arrived or removed; unused for replay markers */ struct hid_hotplug_queue* next; }; +struct hid_hotplug_device { + /* NULL after a reconciled removal while a snapshot arrival is pending. */ + struct hid_device_info *info; + libusb_device *device; /* Referenced identity of this connection */ + /* A snapshot entry may still have an unprocessed live arrival. */ + unsigned char arrival_seen; + /* Nonzero once reconciliation finds it gone: hide it from snapshots and + * deliver its removal only to handles below this registration boundary. */ + hid_hotplug_callback_handle removed_before; + struct hid_hotplug_device *next; +}; + static struct hid_hotplug_context { /* A separate libusb context for hotplug events: helps avoid mutual blocking with read_thread's */ libusb_context * context; @@ -178,27 +241,60 @@ static struct hid_hotplug_context { /* A thread that fills the event queue */ hidapi_thread_state libusb_thread; - /* A separate thread which processes hidapi's internal event queue */ + /* A separate thread which processes hidapi's internal event queue. + * Its condition parks the callback thread while !shutdown_pending and + * shutdown waiters while shutdown_pending; flag transitions broadcast. */ hidapi_thread_state callback_thread; /* This mutex prevents changes to the callback list */ pthread_mutex_t mutex; /* Boolean flags */ + /* `mutex` (and the thread states) have been initialized. A one-way latch: + * written once under hid_hotplug_init_mutex and read under it as well; the + * objects it advertises are never destroyed, so once it is set, locking + * `mutex` is safe forever. */ unsigned char mutex_ready; unsigned char mutex_in_use; unsigned char cb_list_dirty; - + /* The event threads have been started and their join not yet claimed */ + unsigned char threads_running; + /* A thread has claimed the join of the event threads (threads_running is + * already cleared) but has not completed it yet. It releases `mutex` while + * joining, so in that window the event threads may still be running - and + * still draining messages into `devs`, which nobody else may free until the + * join is through (see hid_internal_hotplug_finish_shutdown). */ + unsigned char join_claimed; + /* Tells the event threads to wind down; set when the last callback is + * removed and cleared once they have been joined. Always written under BOTH + * `mutex` and callback_thread's mutex (see + * hid_internal_hotplug_set_shutdown_pending), so a reader may hold either. + * The join is performed synchronously on the initiating application thread; + * when the shutdown is initiated from the callback (event) thread itself, + * which cannot join itself, it is deferred to the next registration or + * hid_exit(). */ + unsigned char shutdown_pending; + /* Completed wind-downs; protected by callback_thread's mutex */ + unsigned long shutdown_generation; + + /* Pending messages for the callback thread; protected by callback_thread's mutex */ struct hid_hotplug_queue* queue; + /* A removal was dropped; protected by callback_thread's mutex. Cleared + * before reconciliation, restored on failure or another dropped removal. */ + unsigned char cache_stale; /* Linked list of the hotplug callbacks */ struct hid_hotplug_callback *hotplug_cbs; - /* Linked list of the device infos (mandatory when the device is disconnected). + /* Linked list of the devices and their infos (needed after disconnection). * Protected by `mutex`: all reads, writes and the final free during teardown - * are performed while holding it. The teardown free runs in - * hid_internal_hotplug_cleanup() after the hotplug threads have been joined. */ - struct hid_device_info *devs; + * are performed while holding it. Freed by + * hotplug_thread() after joining the callback thread, + * hid_internal_hotplug_cleanup()/hid_internal_hotplug_exit() when the threads + * never ran or were already joined (and no join is claimed), or + * hid_internal_hotplug_unwind_registration() on a failed first registration. + * Device references are released before their libusb context is destroyed. */ + struct hid_hotplug_device *devs; } hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ uint16_t get_usb_code_for_current_locale(void); @@ -387,18 +483,48 @@ static wchar_t *ctowcdup(const char *s, size_t slen) static void register_libusb_error(hidapi_error_ctx *err, int error, const char *error_context) { + int is_global = (err == &last_global_error); + + if (is_global) { + pthread_mutex_lock(&hid_global_error_mutex); + if (hid_internal_on_callback_thread()) { + /* Dropped: never write the global error state from HIDAPI's own + * thread (see hid_callback_thread_id) */ + pthread_mutex_unlock(&hid_global_error_mutex); + return; + } + } + err->error_code = error; err->error_context = error_context; + + if (is_global) + pthread_mutex_unlock(&hid_global_error_mutex); } static void register_string_error(hidapi_error_ctx *err, const char *error) { + int is_global = (err == &last_global_error); + + if (is_global) { + pthread_mutex_lock(&hid_global_error_mutex); + if (hid_internal_on_callback_thread()) { + /* Dropped: never write the global error state from HIDAPI's own + * thread (see hid_callback_thread_id) */ + pthread_mutex_unlock(&hid_global_error_mutex); + return; + } + } + free(err->last_error_str); err->last_error_str = ctowcdup(error, strlen(error)); err->error_code = err->last_error_code_cache = 1; err->error_context = err->last_error_context_cache = NULL; + + if (is_global) + pthread_mutex_unlock(&hid_global_error_mutex); } @@ -650,60 +776,232 @@ struct hid_hotplug_callback hid_hotplug_callback_fn callback; void* user_data; int events; + /* Deep-copied registration-time snapshot of the matching connected devices + * (HID_API_HOTPLUG_ENUMERATE): delivered asynchronously on the event thread + * as synthetic arrival events, always before any live event for this + * callback. Protected by hid_hotplug_context.mutex. */ + struct hid_device_info* replay; struct hid_hotplug_callback* next; hid_hotplug_callback_handle handle; }; -static void hid_internal_hotplug_remove_postponed() +static void hid_internal_hotplug_free_devices(struct hid_hotplug_device *devs) +{ + while (devs) { + struct hid_hotplug_device *next = devs->next; + hid_free_enumeration(devs->info); + libusb_unref_device(devs->device); + free(devs); + devs = next; + } +} + +/* Takes ownership of info on success only */ +static struct hid_hotplug_device *hid_internal_hotplug_create_device(libusb_device *device, struct hid_device_info *info) +{ + struct hid_hotplug_device *dev = (struct hid_hotplug_device *) calloc(1, sizeof(struct hid_hotplug_device)); + if (dev) { + dev->info = info; + dev->device = libusb_ref_device(device); + } + return dev; +} + +/* Sets shutdown_pending and wakes everyone who may be waiting for the change: + * the event threads, and any thread in hid_internal_hotplug_wait_shutdown(). + * Called with `mutex` held, so every write to shutdown_pending is made under + * BOTH `mutex` and the callback thread's mutex - a reader may therefore hold + * either one of them. */ +static void hid_internal_hotplug_set_shutdown_pending(unsigned char value) +{ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.shutdown_pending = value; + if (!value) { + hid_hotplug_context.shutdown_generation++; + } + hidapi_thread_cond_broadcast(&hid_hotplug_context.callback_thread); + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); +} + +static void hid_internal_hotplug_remove_postponed(void) { /* Unregister the callbacks whose removal was postponed */ - /* This function is always called inside a locked mutex */ - /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { + /* This function is always called with `mutex` held, which implies the + * machinery is initialized: locking it is the only way to get here */ + /* However, any actions are only allowed if the mutex is NOT in use */ + if (hid_hotplug_context.mutex_in_use) { return; } - - /* Traverse the list of callbacks and check if any were marked for removal */ - struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - while (*current) { - struct hid_hotplug_callback *callback = *current; - if (!callback->events) { - *current = (*current)->next; - free(callback); - continue; + + if (hid_hotplug_context.cb_list_dirty) { + /* Traverse the list of callbacks and check if any were marked for removal */ + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current) { + struct hid_hotplug_callback *callback = *current; + if (!callback->events) { + *current = callback->next; + /* An undelivered ENUMERATE snapshot dies with its callback */ + hid_free_enumeration(callback->replay); + free(callback); + continue; + } + current = &callback->next; } - current = &callback->next; + + /* Clear the flag so we don't start the cycle unless necessary */ + hid_hotplug_context.cb_list_dirty = 0; + } + + if (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.threads_running && !hid_hotplug_context.shutdown_pending) { + /* The last callback is gone: ask the event threads to wind down. The + * caller joins them (hid_internal_hotplug_cleanup_sync()), unless this + * runs on the event thread itself - it cannot join itself, so the join + * is then deferred to the next registration or hid_exit(). */ + hid_internal_hotplug_set_shutdown_pending(1); } - - /* Clear the flag so we don't start the cycle unless necessary */ - hid_hotplug_context.cb_list_dirty = 0; } -static void hid_internal_hotplug_cleanup() +static void hid_internal_hotplug_cleanup(void) { - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + /* Called with `mutex` held, which implies the machinery is initialized */ + if (hid_hotplug_context.mutex_in_use) { return; } - /* Before checking if the list is empty, clear any entries whose removal was postponed first */ + /* Before checking if the list is empty, clear any entries whose removal was + * postponed first; this also winds the event threads down (shutdown_pending) + * once the list becomes empty */ hid_internal_hotplug_remove_postponed(); if (hid_hotplug_context.hotplug_cbs != NULL) { return; } - /* Wait for both threads to stop */ + if (!hid_hotplug_context.threads_running && !hid_hotplug_context.join_claimed) { + /* The event threads either never ran or have already been joined: we + * have exclusive access to `devs` (the caller holds `mutex`). + * A merely CLAIMED join does not qualify: threads_running is already + * cleared, but the threads are still running and may be draining + * messages into `devs` - the pump frees it once the drainer is gone. */ + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + } + /* When the threads are still winding down, the pump frees `devs` and + * hid_internal_hotplug_finish_shutdown() joins it (releasing `mutex` + * while joining): joining here could deadlock, as the callback + * thread locks `mutex` to drain its queue while the caller of this + * function is holding it. */ +} + +/* Completes the wind-down of the event threads (a claimed join): joins them + * and clears/broadcasts shutdown_pending for any other + * thread waiting for the wind-down to finish. + * Called with `mutex` held (recursion level 1) and shutdown_pending set; + * temporarily releases `mutex` while joining so the callback thread can drain + * its queue (process_hotplug_event locks it); on return `mutex` is held again. + * Must not run on the event thread itself. */ +static void hid_internal_hotplug_finish_shutdown(void) +{ + /* Claim the join: other threads now see threads_running == 0 and wait for + * shutdown_pending to be cleared instead of joining a second time. + * join_claimed keeps them from mistaking the cleared threads_running for + * "the threads are gone" while `mutex` is released below - they are not, and + * `devs` stays theirs until the join is through. */ + hid_hotplug_context.threads_running = 0; + hid_hotplug_context.join_claimed = 1; + pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* The libusb thread joins the callback thread on its way out */ hidapi_thread_join(&hid_hotplug_context.libusb_thread); + pthread_mutex_lock(&hid_hotplug_context.mutex); + hid_hotplug_context.join_claimed = 0; - /* Both hotplug threads have exited: we now have exclusive access to `devs` - * (the caller holds `mutex` and no hotplug event can reach process_hotplug_event). */ - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; + /* Announce the completed wind-down to hid_internal_hotplug_wait_shutdown() */ + hid_internal_hotplug_set_shutdown_pending(0); +} + +/* Waits out a wind-down that another thread has already claimed (threads_running + * cleared while shutdown_pending is still set): a busy `unlock`/`lock` spin would + * be unfair and can livelock under a real-time scheduling policy, so we block on + * the callback thread's condition, which the joiner broadcasts once it is done. + * Called with `mutex` held (recursion level 1); releases and re-acquires it while + * waiting. Must not run on the event thread itself. */ +static void hid_internal_hotplug_wait_shutdown(void) +{ + unsigned long generation; + + if (!hid_hotplug_context.shutdown_pending) { + return; + } + + /* Acquire the mutex that guards shutdown_pending (and its condition) BEFORE + * releasing `mutex`, so the joiner cannot complete in between and leave us + * waiting for a broadcast that has already happened */ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + generation = hid_hotplug_context.shutdown_generation; + pthread_mutex_unlock(&hid_hotplug_context.mutex); + while (hid_hotplug_context.shutdown_pending && hid_hotplug_context.shutdown_generation == generation) { + hidapi_thread_cond_wait(&hid_hotplug_context.callback_thread); + } + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + pthread_mutex_lock(&hid_hotplug_context.mutex); } -static void hid_internal_hotplug_init() +/* Called with `mutex` held. With no callbacks left, the caller must be off + * the event thread at recursion level 1. + * Both join and wait release it: re-check which generation needs settling. */ +static void hid_internal_hotplug_settle_shutdown(void) { + while (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.shutdown_pending) { + if (hid_hotplug_context.threads_running) { + hid_internal_hotplug_finish_shutdown(); + } + else { + hid_internal_hotplug_wait_shutdown(); + } + } +} + +/* Runs the postponed-removal cleanup and, unless this is the event thread + * dispatching (mutex_in_use), completes the wind-down of the event threads + * synchronously once the last callback is gone: when this returns on an + * application thread, the machinery is fully stopped, no callback can be + * invoked anymore and the library may be safely unloaded. + * The event thread cannot join itself, so a shutdown initiated from within a + * callback stays deferred to the next registration or hid_exit(). + * Called with `mutex` held (recursion level 1). */ +static void hid_internal_hotplug_cleanup_sync(void) +{ + hid_internal_hotplug_cleanup(); + + if (hid_hotplug_context.mutex_in_use || hid_hotplug_context.hotplug_cbs != NULL) { + return; + } + + hid_internal_hotplug_settle_shutdown(); +} + +/* Serializes the one-time initialization of `mutex` and publishes the + * mutex_ready flag that advertises it: mutex_ready is read under this mutex, so + * no reader observes it without a happens-before relation to the initialization + * it advertises (a plain read would be a data race, and C11 atomics are not + * available on the C99 baseline). + * + * This mutex is a leaf: it is never held while another one is acquired. Holding + * it across the acquisition of `mutex` would deadlock, as the event thread takes + * them in the opposite order whenever a callback registers another callback + * (it already holds `mutex` and then needs this one). */ +static pthread_mutex_t hid_hotplug_init_mutex = PTHREAD_MUTEX_INITIALIZER; + +/* Initializes the hotplug machinery if needed, then locks `mutex`. + * Once initialized, `mutex` and the thread states live for the rest of the + * process: hid_internal_hotplug_exit() winds the machinery down but does NOT + * destroy them (mutex_ready is a one-way latch). Destroying `mutex` would race + * every thread that is about to lock it - it cannot be done safely without a + * lock, and taking one around it is what the lock-order note above rules out. */ +static void hid_internal_hotplug_init_and_lock(void) +{ + pthread_mutex_lock(&hid_hotplug_init_mutex); if (!hid_hotplug_context.mutex_ready) { hidapi_thread_state_init(&hid_hotplug_context.libusb_thread); hidapi_thread_state_init(&hid_hotplug_context.callback_thread); @@ -716,35 +1014,75 @@ static void hid_internal_hotplug_init() pthread_mutexattr_destroy(&attr); /* Set state to Ready */ - hid_hotplug_context.mutex_ready = 1; hid_hotplug_context.mutex_in_use = 0; hid_hotplug_context.cb_list_dirty = 0; + hid_hotplug_context.threads_running = 0; + hid_hotplug_context.join_claimed = 0; + hid_hotplug_context.shutdown_pending = 0; if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + + hid_hotplug_context.mutex_ready = 1; } + pthread_mutex_unlock(&hid_hotplug_init_mutex); + + pthread_mutex_lock(&hid_hotplug_context.mutex); } -static void hid_internal_hotplug_exit() +/* Locks `mutex` if the hotplug machinery has ever been initialized; returns -1 + * (without locking anything) if it has not */ +static int hid_internal_hotplug_lock(void) { - if (!hid_hotplug_context.mutex_ready) { - return; + unsigned char ready; + + pthread_mutex_lock(&hid_hotplug_init_mutex); + ready = hid_hotplug_context.mutex_ready; + pthread_mutex_unlock(&hid_hotplug_init_mutex); + + if (!ready) { + return -1; } pthread_mutex_lock(&hid_hotplug_context.mutex); + return 0; +} + +static void hid_internal_hotplug_exit(void) +{ + /* Initialize the machinery if it never was, instead of taking a lock-free + * shortcut for that case: the common path below then handles it naturally + * (empty callback list, no threads to wind down). The application must + * serialize hid_exit() against registration, as it does against hid_init(). */ + hid_internal_hotplug_init_and_lock(); + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - /* Remove all callbacks from the list */ + /* Remove all callbacks from the list (undelivered ENUMERATE snapshots die with them) */ while (*current) { struct hid_hotplug_callback* next = (*current)->next; + hid_free_enumeration((*current)->replay); free(*current); *current = next; } - hid_internal_hotplug_cleanup(); - pthread_mutex_unlock(&hid_hotplug_context.mutex); - hid_hotplug_context.mutex_ready = 0; - pthread_mutex_destroy(&hid_hotplug_context.mutex); + hid_hotplug_context.cb_list_dirty = 0; + + if (hid_hotplug_context.threads_running) { + /* Request the wind-down */ + hid_internal_hotplug_set_shutdown_pending(1); + } + hid_internal_hotplug_settle_shutdown(); + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; - hidapi_thread_state_destroy(&hid_hotplug_context.callback_thread); - hidapi_thread_state_destroy(&hid_hotplug_context.libusb_thread); + /* The event threads are gone. Destroy the main context under `mutex` too; + * libusb_exit() does not re-enter HIDAPI. */ + if (usb_context) { + libusb_exit(usb_context); + usb_context = NULL; + } + + /* `mutex` and the thread states are deliberately NOT destroyed: they are + * reused by the next registration (see hid_internal_hotplug_init_and_lock) */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); } int HID_API_EXPORT hid_init(void) @@ -772,15 +1110,16 @@ int HID_API_EXPORT hid_init(void) int HID_API_EXPORT hid_exit(void) { - if (usb_context) { - libusb_exit(usb_context); - usb_context = NULL; - hid_internal_hotplug_exit(); - } + /* Stop the hotplug machinery before destroying the main usb_context. + * The application must serialize this call against registration and must + * never make it from a hotplug callback (see hidapi.h). */ + hid_internal_hotplug_exit(); /* Free global error state */ + pthread_mutex_lock(&hid_global_error_mutex); free_hidapi_error(&last_global_error); memset(&last_global_error, 0, sizeof(last_global_error)); + pthread_mutex_unlock(&hid_global_error_mutex); return 0; } @@ -1067,7 +1406,7 @@ static int should_enumerate_interface(unsigned short vendor_id, const struct lib return 0; } -static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id) +static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id, int *oom) { struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; @@ -1089,8 +1428,14 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns } res = libusb_get_active_config_descriptor(dev, &conf_desc); - if (res < 0) - libusb_get_config_descriptor(dev, 0, &conf_desc); + if (res < 0 && res != LIBUSB_ERROR_NO_MEM) + res = libusb_get_config_descriptor(dev, 0, &conf_desc); + if (res == LIBUSB_ERROR_NO_MEM) { + if (oom) { + *oom = 1; + } + return NULL; + } if (conf_desc) { for (j = 0; j < conf_desc->bNumInterfaces; j++) { const struct libusb_interface *intf = &conf_desc->interface[j]; @@ -1116,6 +1461,13 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns #endif tmp = create_device_info_for_device(dev, handle, &desc, conf_desc->bConfigurationValue, intf_desc->bInterfaceNumber); + if (!tmp || !tmp->path) { + if (oom) { + *oom = 1; + } + hid_free_enumeration(tmp); + tmp = NULL; + } if (tmp) { #ifdef INVASIVE_GET_USAGE /* TODO: have a runtime check for this section. */ @@ -1164,27 +1516,57 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns return root; } -struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +/* Enumerates the connected HID devices. Unlike hid_enumerate(), it tells a + * genuine failure apart from an empty result: `*failed` is set to 1 only when + * the enumeration itself failed (and the global error describes why), and to 0 + * when the system simply has no matching device (NULL is returned in both + * cases). No error is registered for the empty case. Both public enumeration + * and the hotplug snapshot fail on allocation failure, rather than returning + * a partial list. With `cache` set, stores the infos and connection identities + * there instead of returning a public list. The caller initializes `ctx`. */ +static struct hid_device_info *hid_internal_enumerate(libusb_context *ctx, unsigned short vendor_id, unsigned short product_id, int *failed, struct hid_hotplug_device **cache) { libusb_device **devs; libusb_device *dev; ssize_t num_devs; int i = 0; + int oom = 0; + struct hid_hotplug_device **cache_tail = cache; struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; - if (hid_init() < 0) - /* register_global_error: global error is set by hid_init */ - return NULL; + *failed = 1; + if (cache) { + *cache = NULL; + } - num_devs = libusb_get_device_list(usb_context, &devs); + num_devs = libusb_get_device_list(ctx, &devs); if (num_devs < 0) { register_libusb_error(&last_global_error, num_devs, "libusb_get_device_list"); return NULL; } while ((dev = devs[i++]) != NULL) { - struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id); + struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id, &oom); + if (cache && tmp && !oom) { + *cache_tail = hid_internal_hotplug_create_device(dev, tmp); + if (*cache_tail) { + cache_tail = &(*cache_tail)->next; + continue; + } + oom = 1; + } + if (oom) { + hid_free_enumeration(tmp); + hid_free_enumeration(root); + if (cache) { + hid_internal_hotplug_free_devices(*cache); + *cache = NULL; + } + libusb_free_device_list(devs, 1); + register_string_error(&last_global_error, "Failed to allocate memory for the device enumeration"); + return NULL; + } if (cur_dev) { cur_dev->next = tmp; } @@ -1202,7 +1584,22 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, u libusb_free_device_list(devs, 1); - if (root == NULL) { + *failed = 0; + + return root; +} + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + int failed = 0; + struct hid_device_info *root; + if (hid_init() < 0) { + /* register_global_error: global error is set by hid_init */ + return NULL; + } + root = hid_internal_enumerate(usb_context, vendor_id, product_id, &failed, NULL); + + if (root == NULL && !failed) { if (vendor_id == 0 && product_id == 0) { register_string_error(&last_global_error, "No HID devices found in the system."); } else { @@ -1227,54 +1624,172 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } -static int match_libusb_to_info(libusb_device *device, struct hid_device_info* info) +/* Creates a standalone (next == NULL) deep copy of a single device info entry */ +static struct hid_device_info *hid_internal_copy_device_info(const struct hid_device_info *src) { - /* make a path from this libusb device, but leave the last 2 fields as 0 */ - char pseudo_path[64]; - get_path(&pseudo_path, device, 0, 0); - int len = strlen(pseudo_path) - sizeof("0.0"); - /* If the path on this HID device matches the template, aside from the last 2 fields, */ - /* we assume the HID device is located on this libusb device */ - return !strncmp(info->path, pseudo_path, len); + struct hid_device_info *copy = (struct hid_device_info *) calloc(1, sizeof(struct hid_device_info)); + if (copy == NULL) { + return NULL; + } + + *copy = *src; + copy->next = NULL; + copy->path = src->path ? strdup(src->path) : NULL; + copy->serial_number = src->serial_number ? wcsdup(src->serial_number) : NULL; + copy->manufacturer_string = src->manufacturer_string ? wcsdup(src->manufacturer_string) : NULL; + copy->product_string = src->product_string ? wcsdup(src->product_string) : NULL; + + if ((src->path && !copy->path) + || (src->serial_number && !copy->serial_number) + || (src->manufacturer_string && !copy->manufacturer_string) + || (src->product_string && !copy->product_string)) { + hid_free_enumeration(copy); + return NULL; + } + + return copy; +} + +/* Delivers the registration-time ENUMERATE snapshot of a single callback as + * synthetic arrival events. Called on the event thread only, with `mutex` held + * and mutex_in_use set. A non-zero return from the callback stops the rest of + * the pass and deregisters the callback (the removal itself is postponed). */ +static void hid_internal_flush_replay(struct hid_hotplug_callback *callback) +{ + while (callback->replay != NULL && callback->events) { + struct hid_device_info *info = callback->replay; + int result; + callback->replay = info->next; + info->next = NULL; + result = callback->callback(callback->handle, info, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, callback->user_data); + hid_free_enumeration(info); + if (result) { + callback->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } + } + + if (callback->replay != NULL) { + /* The callback was deregistered mid-pass: the undelivered rest of the + * snapshot must never be delivered */ + hid_free_enumeration(callback->replay); + callback->replay = NULL; + } } -static void hid_internal_invoke_callbacks(struct hid_device_info* info, hid_hotplug_event event) +/* Delivers the pending ENUMERATE snapshots of all registered callbacks + * (a queued replay marker requests this when there is no live event traffic) */ +static void hid_internal_flush_replays(void) { pthread_mutex_lock(&hid_hotplug_context.mutex); hid_hotplug_context.mutex_in_use = 1; - struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - while (*current) { - struct hid_hotplug_callback *callback = *current; + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; callback != NULL; callback = callback->next) { + if (callback->replay != NULL && callback->events) { + hid_internal_flush_replay(callback); + } + } + + hid_hotplug_context.mutex_in_use = 0; + hid_internal_hotplug_remove_postponed(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); +} + +/* Delivers a single device event to the matching callbacks up to (and + * including) `last`, the dispatch boundary computed once per hotplug message by + * process_hotplug_event(). Called on the event thread only, with `mutex` held + * and mutex_in_use set (which keeps `last` alive: removals are postponed). */ +static void hid_internal_invoke_callbacks(struct hid_device_info* info, hid_hotplug_event event, struct hid_hotplug_callback *last) +{ + struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; + while (callback != NULL) { + /* The ENUMERATE snapshot is always delivered before any live event for the callback */ + if (callback->replay != NULL && callback->events) { + hid_internal_flush_replay(callback); + } if ((callback->events & event) && hid_internal_match_device_id(info->vendor_id, info->product_id, callback->vendor_id, callback->product_id)) { int result = callback->callback(callback->handle, info, event, callback->user_data); /* If the result is non-zero, we mark the callback for removal and proceed */ if (result) { - (*current)->events = 0; + callback->events = 0; hid_hotplug_context.cb_list_dirty = 1; - continue; } } - current = &callback->next; + if (callback == last) { + break; + } + callback = callback->next; + } +} + +/* Record an arrival already represented in the cache, including a retired + * snapshot identity retained only to suppress this delayed arrival. */ +static int hid_internal_hotplug_is_known_device(libusb_device *device) +{ + for (struct hid_hotplug_device **current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { + struct hid_hotplug_device *dev = *current; + if (device == dev->device) { + dev->arrival_seen = 1; + if (dev->info == NULL) { + *current = dev->next; + dev->next = NULL; + hid_internal_hotplug_free_devices(dev); + } + return 1; + } } - hid_hotplug_context.mutex_in_use = 0; - hid_internal_hotplug_remove_postponed(); - pthread_mutex_unlock(&hid_hotplug_context.mutex); + return 0; } -static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void * user_data) +static int hid_internal_hotplug_cache_stale(void) { - (void)ctx; - (void)user_data; + int stale; + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + stale = hid_hotplug_context.cache_stale; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + return stale; +} - /* Make sure we HOLD the device until we are done with it - otherwise libusb would delete it the moment we exit this function */ - libusb_ref_device(device); +/* Called with `mutex` held. Mark departed entries without freeing infos that + * an active callback may still be using; the callback thread dispatches their + * removals after the current message. */ +static int hid_internal_hotplug_reconcile(void) +{ + libusb_device **devices; + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 0; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + ssize_t count = libusb_get_device_list(hid_hotplug_context.context, &devices); + if (count < 0) { + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 1; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + return (int) count; + } + + for (struct hid_hotplug_device *dev = hid_hotplug_context.devs; dev != NULL; dev = dev->next) { + ssize_t i; + for (i = 0; i < count; i++) { + if (devices[i] == dev->device) { + break; + } + } + if (i == count && !dev->removed_before) { + dev->removed_before = hid_hotplug_context.next_handle; + } + } + libusb_free_device_list(devices, 1); + return 0; +} +/* Appends a message for the callback thread to the queue and wakes it up. + * A NULL device marks a request to flush the pending ENUMERATE snapshots. */ +static int hid_internal_enqueue_hotplug_message(libusb_device *device, int event) +{ struct hid_hotplug_queue* msg = (struct hid_hotplug_queue*) calloc(1, sizeof(struct hid_hotplug_queue)); if (NULL == msg) { - libusb_unref_device(device); - return 0; + return -1; } msg->device = device; @@ -1300,73 +1815,215 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic return 0; } +static int LIBUSB_CALL hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void * user_data) +{ + (void)ctx; + (void)user_data; + + /* Make sure we HOLD the device until we are done with it - otherwise libusb would delete it the moment we exit this function */ + libusb_ref_device(device); + + if (hid_internal_enqueue_hotplug_message(device, event) != 0) { + /* A dropped arrival cannot be retried. A dropped removal requires cache + * reconciliation before another snapshot or an unknown live arrival. */ + if (event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 1; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + } + libusb_unref_device(device); + } + + return 0; +} + static void process_hotplug_event(struct hid_hotplug_queue* msg) { + if (msg->device == NULL) { + /* A replay marker: deliver the pending ENUMERATE snapshots promptly + * even when there is no live event traffic */ + hid_internal_flush_replays(); + return; + } + /* Lock the mutex to avoid race conditions with hid_hotplug_register_callback(), - * which may iterate devs during HID_API_HOTPLUG_ENUMERATE while holding this mutex. - * The mutex is recursive, so hid_internal_invoke_callbacks() can safely re-acquire it. */ + * which iterates devs during HID_API_HOTPLUG_ENUMERATE while holding this mutex. + * The mutex is recursive, so a callback may safely re-enter the API. */ pthread_mutex_lock(&hid_hotplug_context.mutex); + /* Mark the list of callbacks as in use for the WHOLE message: a callback + * deregistered from within a callback (its own or another's) is only marked + * and gets removed by hid_internal_hotplug_remove_postponed() below, which + * is what keeps the `last` boundary below alive across the invocations. */ + hid_hotplug_context.mutex_in_use = 1; + + /* Compute the dispatch boundary ONCE for the whole message, before any + * callback runs: a callback registered from within a callback (i.e. on this + * thread, while this message is being dispatched) must not observe this + * event - it receives an already-arrived device through its own + * registration-time HID_API_HOTPLUG_ENUMERATE snapshot instead. Recomputing + * the boundary per interface would deliver the remaining interfaces of a + * multi-interface device to such a callback a second time. */ + struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; + while (last != NULL && last->next != NULL) { + last = last->next; + } + if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) { - struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0); - struct hid_device_info* info_cur = info; - while (info_cur) { - /* For each device, call all matching callbacks */ - /* TODO: possibly make the `next` field NULL to match the behavior on other systems */ - hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); - info_cur = info_cur->next; - } - - /* Append all we got to the end of the device list */ - if (info) { - if (hid_hotplug_context.devs != NULL) { - struct hid_device_info* last = hid_hotplug_context.devs; - while (last->next != NULL) { - last = last->next; + /* The device may already be in the cache: the libusb listener is armed + * before the initial enumeration, so a device that connects in between + * is reported both by the snapshot and as a live arrival. Match only + * connection identity: an older queued arrival must not evict a newer + * connection already captured at the same port by the snapshot. Suppress + * retired snapshot identities, but deliver an unseen queued arrival even + * if the device has already disconnected. */ + if (!hid_internal_hotplug_is_known_device(msg->device)) { + if (hid_internal_hotplug_cache_stale()) { + hid_internal_hotplug_reconcile(); + } + struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0, NULL); + struct hid_hotplug_device *dev = info ? hid_internal_hotplug_create_device(msg->device, info) : NULL; + + if (dev) { + dev->arrival_seen = 1; + /* Append everything we got to the end of the device list BEFORE + * invoking any callback: a callback registered from within a + * callback takes its HID_API_HOTPLUG_ENUMERATE snapshot from + * `devs`, and this device - which it is excluded from receiving + * as a live event (see `last` above) - must be in it. */ + struct hid_hotplug_device **tail = &hid_hotplug_context.devs; + while (*tail != NULL) { + tail = &(*tail)->next; + } + *tail = dev; + + for (struct hid_device_info* info_cur = info; info_cur != NULL; info_cur = info_cur->next) { + /* Each invocation describes exactly one device: `device->next` + * is NULL by contract. A shallow copy is passed rather than + * unlinking the entry, as `devs` must stay whole: a callback + * may walk it (through a nested registration) while we are + * dispatching. */ + struct hid_device_info single = *info_cur; + single.next = NULL; + hid_internal_invoke_callbacks(&single, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, last); } - last->next = info; } else { - hid_hotplug_context.devs = info; + hid_free_enumeration(info); } } } else if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { - for (struct hid_device_info **current = &hid_hotplug_context.devs; *current;) { - struct hid_device_info* info = *current; - if (match_libusb_to_info(msg->device, *current)) { - /* If the libusb device that's left matches this HID device, we detach it from the list */ - *current = (*current)->next; - info->next = NULL; - hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); - /* Free every removed device (and its internal allocations) */ - hid_free_enumeration(info); + struct hid_hotplug_device *removed = NULL; + struct hid_hotplug_device **removed_tail = &removed; + + /* Detach EVERY interface of the departed device from `devs` BEFORE + * invoking any callback: a callback registered from within this dispatch + * takes its HID_API_HOTPLUG_ENUMERATE snapshot from `devs`, and the + * interfaces that have not been dispatched yet must not be in it - the + * device is physically gone, so it would receive a synthetic arrival for + * them and never a matching removal (this event is excluded from it by + * the `last` boundary above). */ + for (struct hid_hotplug_device **current = &hid_hotplug_context.devs; *current;) { + struct hid_hotplug_device *dev = *current; + if (msg->device == dev->device) { + if (dev->removed_before) { + /* Registrations whose snapshots excluded this connection must + * not receive its delayed removal either. */ + last = NULL; + for (struct hid_hotplug_callback *cb = hid_hotplug_context.hotplug_cbs; + cb != NULL && cb->handle < dev->removed_before; cb = cb->next) { + last = cb; + } + } + /* Detach only this connection, never its same-port replacement */ + *current = dev->next; + dev->next = NULL; + *removed_tail = dev; + removed_tail = &dev->next; + } else { + current = &dev->next; + } + } + + while (removed != NULL) { + struct hid_hotplug_device *dev = removed; + removed = dev->next; + dev->next = NULL; + for (struct hid_device_info *info = dev->info; info != NULL; info = info->next) { + /* Each invocation describes exactly one device */ + struct hid_device_info single = *info; + single.next = NULL; + if (last != NULL) { + hid_internal_invoke_callbacks(&single, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, last); + } + } + if (dev->removed_before && !dev->arrival_seen && dev->info != NULL) { + /* Only snapshot entries can still have an unprocessed arrival. + * Retain their identity until it is drained, a queued LEFT + * confirms no arrival is pending, or the context exits. */ + hid_free_enumeration(dev->info); + dev->info = NULL; + dev->next = hid_hotplug_context.devs; + hid_hotplug_context.devs = dev; } else { - current = &info->next; + hid_internal_hotplug_free_devices(dev); } } } + hid_hotplug_context.mutex_in_use = 0; + /* Remove the callbacks whose removal was postponed during the dispatch; this + * also winds the event threads down once the last one is gone */ + hid_internal_hotplug_remove_postponed(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); /* Release the libusb device - we are done with it */ libusb_unref_device(msg->device); - /* Cleanup note: this function is called inside a thread that the clenup function would be waiting to finish */ - /* Any callbacks that await removal are removed in hid_internal_invoke_callbacks */ + /* Cleanup note: this function is called inside a thread that the cleanup function would be waiting to finish */ + /* Any callbacks that await removal are removed above */ /* No further cleaning is needed */ } +/* Called on the callback thread between messages, never during a callback. */ +static void hid_internal_hotplug_dispatch_removed(void) +{ + pthread_mutex_lock(&hid_hotplug_context.mutex); + while (1) { + struct hid_hotplug_device *dev = hid_hotplug_context.devs; + while (dev != NULL && (!dev->removed_before || dev->info == NULL)) { + dev = dev->next; + } + if (dev == NULL) { + break; + } + struct hid_hotplug_queue msg; + msg.device = libusb_ref_device(dev->device); + msg.event = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT; + msg.next = NULL; + process_hotplug_event(&msg); + } + pthread_mutex_unlock(&hid_hotplug_context.mutex); +} + static void* callback_thread(void* user_data) { (void) user_data; + /* Publish this thread's identity before any callback can run on it: the + * global error state must not be written from here (see + * hid_callback_thread_id) */ + hid_internal_callback_thread_enter(); + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); - /* We stop the thread if by the moment there are no events left in the queue there are no callbacks left */ + /* We stop the thread once the shutdown is requested (the last callback is + * removed) and the queue has been drained */ while (1) { /* Wait for events to arrive or shutdown signal */ - while (!hid_hotplug_context.queue && hid_hotplug_context.hotplug_cbs) { + while (!hid_hotplug_context.queue && !hid_hotplug_context.shutdown_pending) { hidapi_thread_cond_wait(&hid_hotplug_context.callback_thread); } @@ -1379,24 +2036,29 @@ static void* callback_thread(void* user_data) hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); process_hotplug_event(cur_event); free(cur_event); + hid_internal_hotplug_dispatch_removed(); hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); } - if (!hid_hotplug_context.hotplug_cbs) { + if (hid_hotplug_context.shutdown_pending) { break; } } hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + hid_internal_callback_thread_leave(); + return NULL; } +/* The libusb event thread. The callback thread is started by the registration + * (which can report a failure to start it), not from here, and is joined below. */ static void* hotplug_thread(void* user_data) { (void) user_data; - - hidapi_thread_create(&hid_hotplug_context.callback_thread, callback_thread, NULL); + int error_logged = 0; + const struct timespec retry_delay = { 0, 5000000 }; /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ /* This timeout only affects how much time it takes to stop the thread */ @@ -1404,44 +2066,126 @@ static void* hotplug_thread(void* user_data) tv.tv_sec = 0; tv.tv_usec = 5000; - while (hid_hotplug_context.hotplug_cbs) { + while (1) { + unsigned char shutdown; + /* The shutdown flag is set under callback_thread's mutex: read it under + * the same one to synchronize with the writer */ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + shutdown = hid_hotplug_context.shutdown_pending; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + if (shutdown) { + break; + } + /* This will allow libusb to call the callbacks, which will fill up the queue */ - libusb_handle_events_timeout_completed(hid_hotplug_context.context, &tv, NULL); + int res = libusb_handle_events_timeout_completed(hid_hotplug_context.context, &tv, NULL); + if (res < 0 && res != LIBUSB_ERROR_TIMEOUT && res != LIBUSB_ERROR_INTERRUPTED) { + if (!error_logged) { + LOG("Hotplug event handling failed: (%d) %s\n", res, libusb_error_name(res)); + error_logged = 1; + } + nanosleep(&retry_delay, NULL); + } } - /* Disarm the libusb listener */ + /* Disarm the libusb listener: no new messages can be enqueued after this */ libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); + + hidapi_thread_join(&hid_hotplug_context.callback_thread); + + /* Free anything still in the queue (the callback thread may exit before the + * last messages are enqueued); the devices must be unreferenced before their + * libusb context is destroyed. Nothing else can touch the queue anymore. */ + while (hid_hotplug_context.queue) { + struct hid_hotplug_queue *msg = hid_hotplug_context.queue; + hid_hotplug_context.queue = msg->next; + if (msg->device) { + libusb_unref_device(msg->device); + } + free(msg); + } + + /* Self-removal may defer the pump's join indefinitely. Release its context + * now, after all queued and cached device references have been released. */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; libusb_exit(hid_hotplug_context.context); + hid_hotplug_context.context = NULL; + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + return NULL; +} + +/* Rolls a failed registration back to the state the hotplug machinery was in + * before it: frees the callback that was never added to the list and, when it + * would have been the first one, tears the freshly created libusb context and + * device cache down again. Called with `mutex` held. For a first registration, + * no event thread is running (the caller must have joined the callback thread + * if it managed to start it); otherwise the earlier generation is untouched + * and only the never-added callback is freed. + * The caller registers the error itself, as the roll-back may overwrite it. */ +static void hid_internal_hotplug_unwind_registration(struct hid_hotplug_callback *hotplug_cb, int is_first_callback) +{ + hid_free_enumeration(hotplug_cb->replay); + free(hotplug_cb); + + if (!is_first_callback) { + return; + } - /* hotplug_cbs is already NULL here (the loop above exited because of that). - * Signal callback_thread under the mutex so it can observe the NULL hotplug_cbs - * and exit cleanly, rather than waiting indefinitely in cond_wait. */ + /* Drop anything enqueued by synchronous libusb I/O during the snapshot, + * or by the replay marker. Neither event thread is running now. */ hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); - hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + while (hid_hotplug_context.queue) { + struct hid_hotplug_queue *msg = hid_hotplug_context.queue; + hid_hotplug_context.queue = msg->next; + if (msg->device) { + libusb_unref_device(msg->device); + } + free(msg); + } hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); - hidapi_thread_join(&hid_hotplug_context.callback_thread); - - return NULL; + libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + libusb_exit(hid_hotplug_context.context); + hid_hotplug_context.context = NULL; } int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle) { + if (callback_handle != NULL) { + *callback_handle = 0; + } + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + register_string_error(&last_global_error, "Hotplug is not supported by this version of libusb"); return -1; } /* Check params */ if (events == 0 - || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT)) - || (flags & ~(HID_API_HOTPLUG_ENUMERATE)) - || callback == NULL) { + || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT))) { + register_string_error(&last_global_error, "Invalid hotplug events mask"); + return -1; + } + + if (flags & ~(HID_API_HOTPLUG_ENUMERATE)) { + register_string_error(&last_global_error, "Invalid hotplug flags"); + return -1; + } + + if (callback == NULL) { + register_string_error(&last_global_error, "Hotplug callback function is NULL"); return -1; } struct hid_hotplug_callback* hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); if (hotplug_cb == NULL) { + register_string_error(&last_global_error, "Failed to allocate memory for a hotplug callback"); return -1; } @@ -1452,84 +2196,208 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven hotplug_cb->events = events; hotplug_cb->user_data = user_data; hotplug_cb->callback = callback; - - /* Ensure we are ready to actually use the mutex */ - hid_internal_hotplug_init(); - - /* Lock the mutex to avoid race itions */ - pthread_mutex_lock(&hid_hotplug_context.mutex); - - hotplug_cb->handle = hid_hotplug_context.next_handle++; - - /* handle the unlikely case of handle overflow */ - if (hid_hotplug_context.next_handle < 0) - { - hid_hotplug_context.next_handle = 1; + hotplug_cb->replay = NULL; + + /* Ensure we are ready to actually use the mutex, and lock it to avoid race conditions */ + hid_internal_hotplug_init_and_lock(); + + /* If a previous generation of the event threads is still winding down (the + * last callback was removed from the event thread itself, so its join had to + * be deferred), finish it before starting a new one */ + hid_internal_hotplug_settle_shutdown(); + + /* Registration implicitly initializes HIDAPI (as if by hid_init()); done + * under the mutex so concurrent registrations do not race in it. The + * application must serialize hid_exit() against registration. */ + if (!usb_context && hid_init() < 0) { + /* register_global_error: global error is already set by hid_init */ + free(hotplug_cb); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; } - /* Return allocated handle */ - if (callback_handle != NULL) { - *callback_handle = hotplug_cb->handle; - } - /* Append a new callback to the end */ - if (hid_hotplug_context.hotplug_cbs != NULL) { - struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; - while (last->next != NULL) { - last = last->next; - } - last->next = hotplug_cb; + /* Handles are never reused, as a stale handle must not silently address a + * live callback. Refuse to register rather than to overflow (undefined) or + * to wrap around into the handles still in use. */ + if (hid_hotplug_context.next_handle == INT_MAX) { + register_string_error(&last_global_error, "No hotplug callback handles left"); + free(hotplug_cb); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; } - else { - /* Fill already connected devices so we can use this info in disconnection notification */ - if (libusb_init(&hid_hotplug_context.context)) { + + int is_first_callback = (hid_hotplug_context.hotplug_cbs == NULL); + + if (is_first_callback) { + int res = libusb_init(&hid_hotplug_context.context); + if (res) { + register_libusb_error(&last_global_error, res, "hotplug/libusb_init"); free(hotplug_cb); pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } - - hid_hotplug_context.devs = hid_enumerate(0, 0); - hid_hotplug_context.hotplug_cbs = hotplug_cb; - - /* Arm a global callback to receive ALL notifications for HID class devices */ - if (libusb_hotplug_register_callback(hid_hotplug_context.context, + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 0; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + + /* Arm a global callback for ALL USB devices (HID is an interface class; + * hid_enumerate_from_libusb() filters supported interfaces on the callback + * thread) BEFORE taking the snapshot from this same context: libusb does + * not report the devices that are already connected when the listener is + * armed (LIBUSB_HOTPLUG_ENUMERATE is deliberately not used, the snapshot + * takes that role), so a device connecting the other way around - after + * the snapshot but before the listener - would be in neither, and its + * removal would later go unreported as well. The reverse order can only + * report a device twice, which the arrival path deduplicates against + * `devs` (see hid_internal_hotplug_is_known_device). */ + res = libusb_hotplug_register_callback(hid_hotplug_context.context, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, 0, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, &hid_libusb_hotplug_callback, NULL, - &hid_hotplug_context.callback_handle)) { - /* Major malfunction, failed to register a callback. - * No hotplug thread was started, so we must unwind `devs` ourselves - * (hid_internal_hotplug_cleanup() would try to join a non-existent thread). */ + &hid_hotplug_context.callback_handle); + if (res) { + /* Major malfunction, failed to register a callback */ + register_libusb_error(&last_global_error, res, "libusb_hotplug_register_callback"); libusb_exit(hid_hotplug_context.context); - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; + hid_hotplug_context.context = NULL; free(hotplug_cb); - hid_hotplug_context.hotplug_cbs = NULL; pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } - /* Initialization succeeded! We run the threads now */ - hidapi_thread_create(&hid_hotplug_context.libusb_thread, hotplug_thread, NULL); + /* Fill already connected devices so we can use this info in disconnection + * notification. An empty system is not a failure of the registration, but + * a failed enumeration is: silently caching an empty list would make every + * already-connected device invisible to this and all later callbacks. */ + int enumeration_failed = 0; + hid_internal_enumerate(hid_hotplug_context.context, 0, 0, &enumeration_failed, &hid_hotplug_context.devs); + if (enumeration_failed) { + /* register_global_error: global error is already set by hid_internal_enumerate */ + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + } + + if (hid_internal_hotplug_cache_stale()) { + int res = hid_internal_hotplug_reconcile(); + if (res < 0) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_libusb_error(&last_global_error, res, "hotplug/libusb_get_device_list"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } } - /* Mark the mutex as IN USE, to prevent callback removal from inside a callback */ - unsigned char old_state = hid_hotplug_context.mutex_in_use; - hid_hotplug_context.mutex_in_use = 1; - if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { - struct hid_device_info* device = hid_hotplug_context.devs; - /* Notify about already connected devices, if asked so */ - while (device != NULL) { - if (hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { - (*hotplug_cb->callback)(hotplug_cb->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, hotplug_cb->user_data); + /* Take a registration-time snapshot of the matching connected devices: + * it is replayed asynchronously on the event thread as synthetic arrival + * events, never from within this call (see hid_internal_flush_replay). + * All or nothing: a partially copied snapshot would silently hide a + * connected device from the callback forever. */ + struct hid_device_info *replay_tail = NULL; + for (struct hid_hotplug_device *dev = hid_hotplug_context.devs; dev != NULL; dev = dev->next) { + if (dev->removed_before) { + continue; } + for (struct hid_device_info *device = dev->info; device != NULL; device = device->next) { + struct hid_device_info *copy; + if (!hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { + continue; + } + copy = hid_internal_copy_device_info(device); + if (copy == NULL) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_string_error(&last_global_error, "Failed to allocate memory for the hotplug device snapshot"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + if (replay_tail != NULL) { + replay_tail->next = copy; + } + else { + hotplug_cb->replay = copy; + } + replay_tail = copy; + } + } + } - device = device->next; + int removals_pending = 0; + for (struct hid_hotplug_device *dev = hid_hotplug_context.devs; dev != NULL; dev = dev->next) { + if (dev->removed_before && dev->info != NULL) { + removals_pending = 1; + break; } } + if (hotplug_cb->replay != NULL || removals_pending || hid_internal_hotplug_cache_stale()) { + /* Wake the callback thread up so the snapshot is delivered promptly even + * with no live event traffic, and reconciled removals are dispatched. + * Enqueued (and, for the first callback, + * before the threads are even started) while holding the mutex the + * delivery needs, so the callback is always in the list by the time the + * marker is acted upon. Without the marker the snapshot would be stuck + * until the next live event, which may never come. */ + if (hid_internal_enqueue_hotplug_message(NULL, 0) != 0) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_string_error(&last_global_error, "Failed to allocate memory for a hotplug message"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + } + + if (is_first_callback) { + /* Initialization succeeded! We run the threads now. The callback thread + * is started here rather than from the libusb thread so that a failure to + * start it can be reported. Neither thread can deliver anything before we + * release the mutex. */ + if (hidapi_thread_create(&hid_hotplug_context.callback_thread, callback_thread, NULL) != 0) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_string_error(&last_global_error, "Failed to start the hotplug callback thread"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } - hid_hotplug_context.mutex_in_use = old_state; + if (hidapi_thread_create(&hid_hotplug_context.libusb_thread, hotplug_thread, NULL) != 0) { + /* Stop the callback thread we have just started. The mutex is + * released while joining, as the thread locks it to process the + * replay marker (a no-op: the callback is not in the list yet). */ + hid_internal_hotplug_set_shutdown_pending(1); - hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + hidapi_thread_join(&hid_hotplug_context.callback_thread); + pthread_mutex_lock(&hid_hotplug_context.mutex); + + hid_internal_hotplug_set_shutdown_pending(0); + + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_string_error(&last_global_error, "Failed to start the hotplug event thread"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + + hid_hotplug_context.threads_running = 1; + } + + /* Commit the registration: from here on nothing can fail */ + hotplug_cb->handle = hid_hotplug_context.next_handle++; + + /* Append the new callback to the end */ + if (hid_hotplug_context.hotplug_cbs != NULL) { + struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; + while (last->next != NULL) { + last = last->next; + } + last->next = hotplug_cb; + } + else { + hid_hotplug_context.hotplug_cbs = hotplug_cb; + } + + /* Return the allocated handle: it is guaranteed to be written before any + * events can be delivered, as they are dispatched under the same mutex */ + if (callback_handle != NULL) { + *callback_handle = hotplug_cb->handle; + } pthread_mutex_unlock(&hid_hotplug_context.mutex); @@ -1538,14 +2406,22 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) || !hid_hotplug_context.mutex_ready || callback_handle <= 0) { + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + register_string_error(&last_global_error, "Hotplug is not supported by this version of libusb"); return -1; } - pthread_mutex_lock(&hid_hotplug_context.mutex); + if (callback_handle <= 0) { + register_string_error(&last_global_error, "Invalid or unknown hotplug callback handle"); + return -1; + } - if (hid_hotplug_context.hotplug_cbs == NULL) { - pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* Fails only if the machinery was never initialized in this process. + * mutex_ready is a one-way latch: after hid_exit() the never-destroyed mutex + * is locked normally and the empty list below reports the stale handle. + * On success `mutex` is locked. */ + if (hid_internal_hotplug_lock() < 0) { + register_string_error(&last_global_error, "Invalid or unknown hotplug callback handle"); return -1; } @@ -1553,13 +2429,21 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_call /* Remove this notification */ for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { - if ((*current)->handle == callback_handle) { + /* A callback whose removal is already postponed (events == 0) is gone as + * far as the caller is concerned: deregistering it a second time must + * fail, not silently succeed */ + if ((*current)->handle == callback_handle && (*current)->events != 0) { /* Check if we were already in a locked state, as we are NOT allowed to remove any callbacks if we are */ if (hid_hotplug_context.mutex_in_use) { + /* Postpone the removal; the callback receives no events + * (including undelivered synthetic ones) from now on */ (*current)->events = 0; + hid_free_enumeration((*current)->replay); + (*current)->replay = NULL; hid_hotplug_context.cb_list_dirty = 1; } else { struct hid_hotplug_callback *next = (*current)->next; + hid_free_enumeration((*current)->replay); free(*current); *current = next; } @@ -1568,10 +2452,17 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_call } } - hid_internal_hotplug_cleanup(); + /* Deregistering the last callback stops the machinery: unless we are the + * event thread (which cannot join itself), do it synchronously, so that no + * callback can be running anymore by the time we return */ + hid_internal_hotplug_cleanup_sync(); pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (result < 0) { + register_string_error(&last_global_error, "Invalid or unknown hotplug callback handle"); + } + return result; } @@ -1827,6 +2718,24 @@ static void init_xboxone(libusb_device_handle *device_handle, unsigned short idV } } +/* Reattaches the kernel driver detached during a partial initialization, if any. + * Shared by every failure path in hidapi_initialize_device() so they cannot drift + * apart and leave the device with its kernel driver detached (unusable until + * replug). A no-op unless DETACH_KERNEL_DRIVER support actually detached it. */ +static void hidapi_reattach_kernel_driver(hid_device *dev, int interface_num) +{ +#ifdef DETACH_KERNEL_DRIVER + if (dev->is_driver_detached) { + int res = libusb_attach_kernel_driver(dev->device_handle, interface_num); + if (res < 0) + LOG("Failed to reattach the driver to kernel: (%d) %s\n", res, libusb_error_name(res)); + } +#else + (void)dev; + (void)interface_num; +#endif +} + static int hidapi_initialize_device(hid_device *dev, const struct libusb_interface_descriptor *intf_desc, const struct libusb_config_descriptor *conf_desc) { int i =0; @@ -1854,13 +2763,8 @@ static int hidapi_initialize_device(hid_device *dev, const struct libusb_interfa if (res < 0) { LOG("can't claim interface %d: (%d) %s\n", intf_desc->bInterfaceNumber, res, libusb_error_name(res)); -#ifdef DETACH_KERNEL_DRIVER - if (dev->is_driver_detached) { - res = libusb_attach_kernel_driver(dev->device_handle, intf_desc->bInterfaceNumber); - if (res < 0) - LOG("Failed to reattach the driver to kernel: (%d) %s\n", res, libusb_error_name(res)); - } -#endif + /* The interface was never claimed; just undo the kernel-driver detach. */ + hidapi_reattach_kernel_driver(dev, intf_desc->bInterfaceNumber); return 0; } @@ -1921,7 +2825,18 @@ static int hidapi_initialize_device(hid_device *dev, const struct libusb_interfa } } - hidapi_thread_create(&dev->thread_state, read_thread, dev); + if (hidapi_thread_create(&dev->thread_state, read_thread, dev) != 0) { + /* Without the read thread nothing would ever release the barrier below: + * fail the open instead of blocking in it forever. The caller registers + * the error and destroys the device. Unwind the interface claim and the + * kernel-driver detach we already performed - libusb_close() alone would + * drop the claim but leave the kernel driver detached, i.e. the device + * unusable by the kernel until it is replugged. */ + LOG("hidapi_initialize_device: couldn't start the read thread\n"); + libusb_release_interface(dev->device_handle, intf_desc->bInterfaceNumber); + hidapi_reattach_kernel_driver(dev, intf_desc->bInterfaceNumber); + return 0; + } /* Wait here for the read thread to be initialized. */ hidapi_thread_barrier_wait(&dev->thread_state); @@ -2588,19 +3503,13 @@ int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char return res; } -HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +/* Formats - and caches - the error string of one error context. The global + * context is only ever passed with hid_global_error_mutex held. */ +static const wchar_t *hid_internal_error(hidapi_error_ctx *err) { const char *name, *description, *context; char *buffer; int len; - hidapi_error_ctx *err; - - if (!dev) { - err = &last_global_error; - } - else { - err = &dev->error; - } if (err->error_code == LIBUSB_SUCCESS) { return L"Success"; @@ -2660,6 +3569,27 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) } +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + if (!dev) { + /* The global error state is shared by every application thread: this + * function frees and replaces its cached string, so without the lock two + * threads - one here, one in a failing API call - would double-free it. + * HIDAPI's own threads never write it (see hid_callback_thread_id); they + * only take this leaf lock briefly to check their identity, so holding + * it here cannot deadlock anything internal. */ + const wchar_t *res; + + pthread_mutex_lock(&hid_global_error_mutex); + res = hid_internal_error(&last_global_error); + pthread_mutex_unlock(&hid_global_error_mutex); + + return res; + } + + return hid_internal_error(&dev->error); +} + HID_API_EXPORT int HID_API_CALL hid_libusb_error(hid_device *dev) { if (!dev) { diff --git a/libusb/hidapi_libusb.h b/libusb/hidapi_libusb.h index 6e0ded4f7..a2892d790 100644 --- a/libusb/hidapi_libusb.h +++ b/libusb/hidapi_libusb.h @@ -56,6 +56,9 @@ extern "C" { If the error occurred is not immediately caused by a libusb function call, the returned value is 1. @ref hid_error would still contain a valid and meaningful error message. + With a NULL device, this function follows the same application + serialization rules as hid_error(NULL), including against hotplug registration/deregistration. + @ingroup API @param dev A device handle returned from hid_open(), or NULL to get the last non-device-specific error diff --git a/libusb/hidapi_thread_pthread.h b/libusb/hidapi_thread_pthread.h index 0abe733e5..2ab363eaa 100644 --- a/libusb/hidapi_thread_pthread.h +++ b/libusb/hidapi_thread_pthread.h @@ -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) diff --git a/linux/hid.c b/linux/hid.c index 4d9fa38b2..8ba2c9691 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -26,6 +26,8 @@ #include #include #include +#include +#include /* Unix */ #include #include @@ -135,14 +137,26 @@ static wchar_t *utf8_to_wchar_t(const char *utf8) } +/* Serializes concurrent writers of the error strings: hotplug registration and + * deregistration may fail on several application threads at once, including + * during hid_exit(). Internal hotplug processing never writes the global string + * (see quiet); application calls such as hid_enumerate() from a callback remain + * subject to the application's serialization. Reading via hid_error() + * remains subject to the documented thread-safety rules. */ +static pthread_mutex_t error_str_mutex = PTHREAD_MUTEX_INITIALIZER; + /* Makes a copy of the given error message (and decoded according to the * currently locale) into the wide string pointer pointed by error_str. * The last stored error string is freed. * Use register_error_str(NULL) to free the error message completely. */ static void register_error_str(wchar_t **error_str, const char *msg) { + wchar_t *new_str = utf8_to_wchar_t(msg); + + pthread_mutex_lock(&error_str_mutex); free(*error_str); - *error_str = utf8_to_wchar_t(msg); + *error_str = new_str; + pthread_mutex_unlock(&error_str_mutex); } /* Semilar to register_error_str, but allows passing a format string with va_list args into this function. */ @@ -192,13 +206,6 @@ static void register_device_error_format(hid_device *dev, const char *format, .. va_end(args); } -/* Get an attribute value from a udev_device and return it as a whar_t - string. The returned string must be freed with free() when done.*/ -static wchar_t *copy_udev_string(struct udev_device *dev, const char *udev_name) -{ - return utf8_to_wchar_t(udev_device_get_sysattr_value(dev, udev_name)); -} - /* * Gets the size of the HID item at the given position * Returns 1 if successful, 0 if an invalid key @@ -419,15 +426,19 @@ static int get_next_hid_usage(const __u8 *report_descriptor, __u32 size, struct /* * Retrieves the hidraw report descriptor from a file. * When using this form, /device/report_descriptor, elevated privileges are not required. + * quiet: don't touch the global error string - for the callers on HIDAPI's + * internal monitor thread, which never writes it (see hidapi.h). */ -static int get_hid_report_descriptor(const char *rpt_path, struct hidraw_report_descriptor *rpt_desc) +static int get_hid_report_descriptor(const char *rpt_path, struct hidraw_report_descriptor *rpt_desc, int quiet) { int rpt_handle; ssize_t res; rpt_handle = open(rpt_path, O_RDONLY | O_CLOEXEC); if (rpt_handle < 0) { - register_global_error_format("open failed (%s): %s", rpt_path, strerror(errno)); + if (!quiet) { + register_global_error_format("open failed (%s): %s", rpt_path, strerror(errno)); + } return -1; } @@ -440,7 +451,9 @@ static int get_hid_report_descriptor(const char *rpt_path, struct hidraw_report_ memset(rpt_desc, 0x0, sizeof(*rpt_desc)); res = read(rpt_handle, rpt_desc->value, HID_MAX_DESCRIPTOR_SIZE); if (res < 0) { - register_global_error_format("read failed (%s): %s", rpt_path, strerror(errno)); + if (!quiet) { + register_global_error_format("read failed (%s): %s", rpt_path, strerror(errno)); + } } rpt_desc->size = (__u32) res; @@ -448,25 +461,26 @@ static int get_hid_report_descriptor(const char *rpt_path, struct hidraw_report_ return (int) res; } -/* return size of the descriptor, or -1 on failure */ -static int get_hid_report_descriptor_from_sysfs(const char *sysfs_path, struct hidraw_report_descriptor *rpt_desc) +/* return size of the descriptor, -1 if unavailable, or -2 on allocation failure + (quiet: see get_hid_report_descriptor) */ +static int get_hid_report_descriptor_from_sysfs(const char *sysfs_path, struct hidraw_report_descriptor *rpt_desc, int quiet) { int res = -1; /* Construct /device/report_descriptor */ size_t rpt_path_len = strlen(sysfs_path) + 25 + 1; char* rpt_path = (char*) calloc(1, rpt_path_len); if (!rpt_path) - return -1; + return -2; snprintf(rpt_path, rpt_path_len, "%s/device/report_descriptor", sysfs_path); - res = get_hid_report_descriptor(rpt_path, rpt_desc); + res = get_hid_report_descriptor(rpt_path, rpt_desc, quiet); free(rpt_path); return res; } /* return non-zero if successfully parsed */ -static int parse_hid_vid_pid_from_uevent(const char *uevent, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id) +static int parse_hid_vid_pid_from_uevent(const char *uevent, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id, int quiet) { char tmp[1024]; size_t uevent_len = strlen(uevent); @@ -506,19 +520,23 @@ static int parse_hid_vid_pid_from_uevent(const char *uevent, unsigned *bus_type, line = strtok_r(NULL, "\n", &saveptr); } - register_global_error("Couldn't find/parse HID_ID"); + if (!quiet) { + register_global_error("Couldn't find/parse HID_ID"); + } return 0; } /* return non-zero if successfully parsed */ -static int parse_hid_vid_pid_from_uevent_path(const char *uevent_path, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id) +static int parse_hid_vid_pid_from_uevent_path(const char *uevent_path, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id, int quiet) { int handle; ssize_t res; handle = open(uevent_path, O_RDONLY | O_CLOEXEC); if (handle < 0) { - register_global_error_format("open failed (%s): %s", uevent_path, strerror(errno)); + if (!quiet) { + register_global_error_format("open failed (%s): %s", uevent_path, strerror(errno)); + } return 0; } @@ -527,24 +545,32 @@ static int parse_hid_vid_pid_from_uevent_path(const char *uevent_path, unsigned close(handle); if (res < 0) { - register_global_error_format("read failed (%s): %s", uevent_path, strerror(errno)); + if (!quiet) { + register_global_error_format("read failed (%s): %s", uevent_path, strerror(errno)); + } return 0; } buf[res] = '\0'; - return parse_hid_vid_pid_from_uevent(buf, bus_type, vendor_id, product_id); + return parse_hid_vid_pid_from_uevent(buf, bus_type, vendor_id, product_id, quiet); } /* return non-zero if successfully read/parsed */ -static int parse_hid_vid_pid_from_sysfs(const char *sysfs_path, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id) +static int parse_hid_vid_pid_from_sysfs(const char *sysfs_path, unsigned *bus_type, unsigned short *vendor_id, unsigned short *product_id, int quiet) { int res = 0; /* Construct /device/uevent */ size_t uevent_path_len = strlen(sysfs_path) + 14 + 1; char* uevent_path = (char*) calloc(1, uevent_path_len); + if (!uevent_path) { + if (!quiet) { + register_global_error("Couldn't allocate the HID uevent path"); + } + return 0; + } snprintf(uevent_path, uevent_path_len, "%s/device/uevent", sysfs_path); - res = parse_hid_vid_pid_from_uevent_path(uevent_path, bus_type, vendor_id, product_id); + res = parse_hid_vid_pid_from_uevent_path(uevent_path, bus_type, vendor_id, product_id, quiet); free(uevent_path); return res; @@ -641,7 +667,18 @@ static int parse_uevent_info(const char *uevent, unsigned *bus_type, } -static struct hid_device_info * create_device_info_for_device(struct udev_device *raw_dev) +/* quiet: don't touch the global error string - for the callers on HIDAPI's + internal monitor thread, which never writes it (see hidapi.h) */ +/* Build the hid_device_info chain (one entry per usage) for a single udev device + node. Returns NULL both for benign exclusions (no HID parent, an unparseable + uevent, an unhandled bus type - exactly what hid_enumerate() would also skip) + and for genuine resource failures. When `failure` is non-NULL it is set to 1 + ONLY in the latter case, so a caller that needs an all-or-nothing enumeration + (the initial hotplug-registration snapshot, which must fail rather than arm the + callbacks against an incomplete device set) can tell a device that is + legitimately absent from one that merely failed to materialize. It is never + cleared here, so callers accumulate it across a whole enumeration. */ +static struct hid_device_info * create_device_info_for_device(struct udev_device *raw_dev, int quiet, int *failure) { struct hid_device_info *root = NULL; struct hid_device_info *cur_dev = NULL; @@ -658,6 +695,9 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device char *product_name_utf8 = NULL; unsigned bus_type; int result; + /* USB-parent strings come from optional sysfs attributes and are checked + against their sources in that branch; other branches require both copies. */ + int strings_from_uevent = 1; struct hidraw_report_descriptor report_desc; sysfs_path = udev_device_get_syspath(raw_dev); @@ -685,6 +725,12 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device /* parse_uevent_info() failed for at least one field. */ goto end; } + if (!serial_number_utf8 || !product_name_utf8) { + if (failure) { + *failure = 1; + } + goto end; + } /* Filter out unhandled devices right away */ switch (bus_type) { @@ -701,8 +747,14 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device /* Create the record. */ root = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); - if (!root) + if (!root) { + /* A genuine resource failure, unlike the benign NULL returns above: + flag it for callers that require an all-or-nothing enumeration. */ + if (failure) { + *failure = 1; + } goto end; + } cur_dev = root; @@ -746,8 +798,17 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device break; } - cur_dev->manufacturer_string = copy_udev_string(usb_dev, "manufacturer"); - cur_dev->product_string = copy_udev_string(usb_dev, "product"); + str = udev_device_get_sysattr_value(usb_dev, "manufacturer"); + cur_dev->manufacturer_string = utf8_to_wchar_t(str); + if (str && !cur_dev->manufacturer_string && failure) { + *failure = 1; + } + str = udev_device_get_sysattr_value(usb_dev, "product"); + cur_dev->product_string = utf8_to_wchar_t(str); + if (str && !cur_dev->product_string && failure) { + *failure = 1; + } + strings_from_uevent = 0; cur_dev->bus_type = HID_API_BUS_USB; @@ -803,14 +864,35 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device break; } + /* A string copy that failed is a genuine resource failure too, exactly like + the calloc() above - flag it, or an all-or-nothing caller would accept a + silently degraded record (see the partial-copy check in + hid_internal_copy_device_info). An entry with a NULL path is especially + harmful in the hotplug cache: it can never be matched by its removal, nor + recognized as a duplicate. utf8_to_wchar_t() returns NULL only on an + allocation failure - an unconvertible string yields an empty one - so this + never fails a merely odd device. */ + if ((dev_path && !root->path) + || (serial_number_utf8 && !root->serial_number) + || (strings_from_uevent + && (!root->manufacturer_string + || (product_name_utf8 && !root->product_string)))) { + if (failure) { + *failure = 1; + } + } + /* Usage Page and Usage */ if (sysfs_path) { - result = get_hid_report_descriptor_from_sysfs(sysfs_path, &report_desc); + result = get_hid_report_descriptor_from_sysfs(sysfs_path, &report_desc, quiet); } else { result = -1; } + if (result == -2 && failure) { + *failure = 1; + } if (result >= 0) { unsigned short page = 0, usage = 0; @@ -835,8 +917,14 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device struct hid_device_info *tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); struct hid_device_info *prev_dev = cur_dev; - if (!tmp) + if (!tmp) { + /* Out of memory mid-device: the returned chain would be + incomplete, so flag it for all-or-nothing callers. */ + if (failure) { + *failure = 1; + } break; + } cur_dev->next = tmp; cur_dev = tmp; @@ -852,6 +940,19 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device cur_dev->usage_page = page; cur_dev->usage = usage; cur_dev->bus_type = prev_dev->bus_type; + + /* Same all-or-nothing rule as above: every string here is copied + from a source known to be non-NULL, so a NULL copy is an + allocation failure that left a degraded entry in the chain */ + if ((dev_path && !cur_dev->path) + || (prev_dev->serial_number && !cur_dev->serial_number) + || (prev_dev->manufacturer_string && !cur_dev->manufacturer_string) + || (prev_dev->product_string && !cur_dev->product_string)) { + if (failure) { + *failure = 1; + } + break; + } } } @@ -889,7 +990,7 @@ static struct hid_device_info * create_device_info_for_hid_device(hid_device *de /* Open a udev device from the dev_t. 'c' means character device. */ udev_dev = udev_device_new_from_devnum(udev, 'c', s.st_rdev); if (udev_dev) { - root = create_device_info_for_device(udev_dev); + root = create_device_info_for_device(udev_dev, 1, NULL); } if (!root) { @@ -914,6 +1015,56 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) return HID_API_VERSION_STR; } +/* Lifecycle of the udev monitor thread; every transition happens under the + hotplug mutex */ +enum hid_hotplug_thread_state { + /* No monitor thread exists; nothing to join */ + HID_HOTPLUG_THREAD_NONE, + /* The monitor thread is running */ + HID_HOTPLUG_THREAD_RUNNING, + /* The monitor thread has returned - it published this state as its LAST + write to shared state under the mutex, immediately before unlocking and + returning - but its pthread_t has not been joined yet. The thread is + JOINABLE, not detached: hid_exit() (and the next register/deregister) + reap it (see hid_internal_hotplug_reap_thread), so once hid_exit() returns + no monitor-thread instruction can still be executing inside the library. A + detached thread could not guarantee that - after it published its exit and + hid_exit() returned, it could still be running its own epilogue + (unlock/return) in the library's text when the application calls + dlclose(), i.e. resume in unmapped code. Reaping first RETIRES the finished + generation (moves its pthread_t onto hid_hotplug_context.retired, keyed by a + stable id token) and then joins it with the mutex released, so a pthread_t + is never inspected after pthread_join() invalidated it, and a generation + superseded by a re-entrant registration is joined rather than dropped. */ + HID_HOTPLUG_THREAD_FINISHED +}; + +/* A monitor thread that has been CREATED (pthread_create succeeded) and not yet + JOINED. The current generation lives in hid_hotplug_context.thread / + .thread_id / .thread_state; once it publishes FINISHED it is moved onto + hid_hotplug_context.retired (see hid_internal_hotplug_reap_thread) and joined + from there. A generation that is superseded while still FINISHED-but-unjoined + - a thread-specific-data destructor running on it re-enters HIDAPI and starts a + new generation - is therefore never dropped: its pthread_t stays on this list + until joined. Threads are identified by their `id` token and by list + membership, NEVER by comparing a pthread_t after it was joined (that handle is + invalid). hid_exit() joins EVERY entry on this list (and the current thread) + before it returns. */ +struct hid_hotplug_monitor_thread { + pthread_t thread; + /* Stable identity token, assigned at creation (hid_hotplug_context.thread_id + at the time). Survives the join; used for identity/diagnostics without ever + touching the joined pthread_t. */ + unsigned long id; + /* A reaper has set this and dropped the mutex to pthread_join() this entry: + any other reaper skips it (a second join of the same pthread_t is undefined + behavior), and hid_exit() waits on thread_cond for the join to finish. */ + unsigned char being_joined; + /* A failed join is never retried; retain the node instead. */ + unsigned char join_failed; + struct hid_hotplug_monitor_thread *next; +}; + static struct hid_hotplug_context { /* UDEV context that handles the monitor */ struct udev* udev_ctx; @@ -921,18 +1072,54 @@ static struct hid_hotplug_context { /* UDEV monitor that receives events */ struct udev_monitor* mon; - /* File descriptor for the UDEV monitor that allows to check for new events with select() */ + /* File descriptor for the UDEV monitor that allows to check for new events with poll() */ int monitor_fd; - /* Thread for the UDEV monitor */ + /* Thread for the UDEV monitor (the current generation) */ pthread_t thread; + /* Stable identity token of the current generation's thread (0 = none). + Assigned from next_thread_id at each pthread_create so a generation can be + identified without ever comparing a joined pthread_t. */ + unsigned long thread_id; + + /* Source of non-zero thread_id tokens; survives hid_exit, skips 0 on wrap */ + unsigned long next_thread_id; + + enum hid_hotplug_thread_state thread_state; + + /* Pre-allocated node owned while RUNNING or FINISHED; see + hid_internal_hotplug_retire_current(). Freed after joining, or if creation + fails. */ + struct hid_hotplug_monitor_thread *thread_node; + + /* Monitor threads created but not yet joined that are no longer the current + generation (superseded/orphaned). Their pthread_t is moved here instead of + being dropped; every entry is joined before hid_exit() returns. */ + struct hid_hotplug_monitor_thread *retired; + + /* Recursive for callbacks. mutex_in_use turns nested cleanup/exit away, so + their unlock/lock pairs and pthread_cond_wait run at depth exactly one: + those operations release only one level of a recursive mutex. */ pthread_mutex_t mutex; + /* Broadcast whenever a retired monitor thread is joined and removed from the + list. hid_exit() must join every generation before returning; when the only + entries left are being joined by another (pre-exit) reaper, it waits here + for that join to complete instead of double-joining. Process-lifetime, like + the mutex: never destroyed. */ + pthread_cond_t thread_cond; + /* Boolean flags */ + /* The one-time initialization of the mutex succeeded (written once, under + pthread_once); the mutex lives for the rest of the process */ unsigned char mutex_ready; unsigned char mutex_in_use; unsigned char cb_list_dirty; + /* hid_exit() is tearing the hotplug machinery down */ + unsigned char exiting; + /* The udev monitor socket died; no more events (see hotplug_thread) */ + unsigned char monitor_dead; /* HIDAPI unique callback handle counter */ hid_hotplug_callback_handle next_handle; @@ -942,7 +1129,7 @@ static struct hid_hotplug_context { /* Linked list of the device infos (mandatory when the device is disconnected) */ struct hid_device_info *devs; -} hid_hotplug_context; /* zero-initialized (static storage); next_handle/monitor_fd set on first init */ +} hid_hotplug_context; /* zero-initialized; next_handle/monitor_fd/next_thread_id set on first init */ struct hid_hotplug_callback { hid_hotplug_callback_handle handle; @@ -952,16 +1139,22 @@ struct hid_hotplug_callback { void *user_data; hid_hotplug_callback_fn callback; + /* Registration-time snapshot of the matching connected devices, + delivered asynchronously by the monitor thread as the + HID_API_HOTPLUG_ENUMERATE initial pass, before any live events + for this callback */ + struct hid_device_info *replay; + /* Pointer to the next notification */ struct hid_hotplug_callback *next; }; -static void hid_internal_hotplug_remove_postponed() +static void hid_internal_hotplug_remove_postponed(void) { /* Unregister the callbacks whose removal was postponed */ /* This function is always called inside a locked mutex */ /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { + if (hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { return; } @@ -971,6 +1164,8 @@ static void hid_internal_hotplug_remove_postponed() struct hid_hotplug_callback *callback = *current; if (!callback->events) { *current = (*current)->next; + /* A deregistered callback never fires again: drop its undelivered snapshot */ + hid_free_enumeration(callback->replay); free(callback); continue; } @@ -981,73 +1176,393 @@ static void hid_internal_hotplug_remove_postponed() hid_hotplug_context.cb_list_dirty = 0; } -static void hid_internal_hotplug_cleanup() +/* Releases a (possibly partially initialized) monitoring context. + Called with the mutex held, only when the monitor thread cannot touch it + anymore: before it is started, after it has been joined, or by the monitor + thread itself right before it announces its own exit. Idempotent. */ +static void hid_internal_hotplug_release_monitor(void) { - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + if (hid_hotplug_context.mon) { + udev_monitor_unref(hid_hotplug_context.mon); + hid_hotplug_context.mon = NULL; + } + if (hid_hotplug_context.udev_ctx) { + udev_unref(hid_hotplug_context.udev_ctx); + hid_hotplug_context.udev_ctx = NULL; + } + hid_hotplug_context.monitor_fd = -1; +} + +/* Moves the current generation onto the retired list once it has published + FINISHED, so its pthread_t is tracked (never dropped) and the current slot is + free for a new generation. The generation is identified by its stable id token, + not by its pthread_t. Called with the mutex held. This only splices the + generation's OWN pre-allocated node (hid_hotplug_context.thread_node, reserved + at pthread_create time) onto the list - no allocation - so it CANNOT fail and + never drops or overwrites a live pthread_t. */ +static void hid_internal_hotplug_retire_current(void) +{ + struct hid_hotplug_monitor_thread *node; + + if (hid_hotplug_context.thread_state != HID_HOTPLUG_THREAD_FINISHED) { return; } - /* Before checking if the list is empty, clear any entries whose removal was postponed first */ - hid_internal_hotplug_remove_postponed(); + node = hid_hotplug_context.thread_node; + node->thread = hid_hotplug_context.thread; + node->id = hid_hotplug_context.thread_id; + node->being_joined = 0; + node->join_failed = 0; + node->next = hid_hotplug_context.retired; + hid_hotplug_context.retired = node; - if (hid_hotplug_context.hotplug_cbs != NULL) { + /* The current slot no longer names a live-or-finished thread. */ + hid_hotplug_context.thread_node = NULL; + hid_hotplug_context.thread_state = HID_HOTPLUG_THREAD_NONE; + hid_hotplug_context.thread_id = 0; +} + +/* The stable id token of the generation this thread IS, or 0 on any other + thread. Written by hotplug_thread() as its first action and never cleared, so + it identifies the thread for the whole of its life - including the + thread-specific-data destructor phase, which runs AFTER the thread published + FINISHED and returned. That phase is exactly where a pthread_t comparison + stops being usable: a reaper may already have claimed the entry + (being_joined) and be sitting in pthread_join(), which returns only once + those destructors are done - so being_joined does NOT mean the thread has + terminated, and its pthread_t must not be touched either way. The id tokens + start at 1 and skip 0 on wrap, so 0 is never a valid generation; thread-locals + are zero-initialized, and a recycled OS thread gets a fresh (zero) one. */ +static __thread unsigned long hid_hotplug_thread_self_id; + +/* Non-zero when the caller runs on one of HIDAPI's monitor threads, whatever + the state of that thread's generation - running, finished, retired or being + joined. Needs no lock: it only reads this thread's own thread-local. + + mutex_in_use alone does not catch every such call: it only covers a call made + from within a dispatch. A thread-specific-data destructor armed by a user + callback runs on the monitor thread after the dispatch unwound - and after the + thread published FINISHED - and may re-enter the library from there. */ +static int hid_internal_on_monitor_thread(void) +{ + return hid_hotplug_thread_self_id != 0; +} + +/* Reaps monitor threads: first RETIRES the current generation if it has finished + (hid_internal_hotplug_retire_current), then joins every retired thread it is + allowed to join, each with the mutex RELEASED. Called with the mutex held + exactly once; because the mutex is dropped for the joins, callers must + re-validate any cached state after this returns. + + pthread_join() must NOT run under the mutex: a finished monitor thread has + published FINISHED and released everything it owned, but a thread-specific-data + destructor armed by a user callback still runs on it afterwards - and such a + destructor re-entering HIDAPI would block on the hotplug mutex forever if the + joiner held it, deadlocking the join. So each entry is claimed (being_joined), + the mutex is dropped, the thread is joined, the mutex is re-acquired, and the + entry is unlinked and freed with a thread_cond broadcast. + + Both hazards are handled WITHOUT ever inspecting a joined pthread_t: + - Monitor threads never join, even from thread-specific-data destructors: + two such destructors could otherwise join each other. They only retire; + joins are left to an application-thread reaper / hid_exit(). + - Concurrent reapers: an entry already being joined (being_joined) is skipped, + so the same pthread_t is never joined twice; its claimant unlinks it when its + join completes. Identity is by list membership and the id token - a pthread_t + is only ever passed to pthread_join() while still unjoined. */ +static void hid_internal_hotplug_reap_thread(void) +{ + hid_internal_hotplug_retire_current(); + if (hid_internal_on_monitor_thread()) { + return; + } + + for (;;) { + struct hid_hotplug_monitor_thread **link; + struct hid_hotplug_monitor_thread *node = NULL; + int join_error; + + for (link = &hid_hotplug_context.retired; *link != NULL; link = &(*link)->next) { + if (!(*link)->being_joined && !(*link)->join_failed) { + node = *link; + break; + } + } + if (node == NULL) { + /* Nothing left that we may join: the list is empty, or the only + entries are already being joined by another reaper or failed a join. */ + return; + } + + /* Claim the join, drop the mutex for it, then re-acquire and unlink. */ + node->being_joined = 1; + pthread_mutex_unlock(&hid_hotplug_context.mutex); + join_error = pthread_join(node->thread, NULL); + pthread_mutex_lock(&hid_hotplug_context.mutex); + if (join_error != 0) { + node->being_joined = 0; + node->join_failed = 1; + pthread_cond_broadcast(&hid_hotplug_context.thread_cond); + return; + } + + for (link = &hid_hotplug_context.retired; *link != NULL; link = &(*link)->next) { + if (*link == node) { + *link = node->next; + break; + } + } + free(node); + /* Wake hid_exit() (or any reaper) waiting for this join to complete. */ + pthread_cond_broadcast(&hid_hotplug_context.thread_cond); + } +} + +/* Winds the monitor thread down once the last callback is gone and reaps it. The + thread releases the monitoring context itself, then publishes FINISHED; this + retires it and joins the retired threads it can (see + hid_internal_hotplug_reap_thread). Called with the mutex held; during a + dispatch (mutex_in_use) it returns immediately. The unlock/lock pairs below + therefore run at recursion depth one, dropping the mutex while waiting or + joining, so the caller must re-validate cached state after this returns. + + This does NOT guarantee every retired generation is joined before it returns: + monitor threads only retire, and an entry another reaper is joining is left + on the list. Application-thread register/deregister calls join the finished + generations they claim (waiting for their thread-specific-data destructors), + but never wait for a join owned by another reaper. hid_exit() is the backstop + that joins EVERY generation before it returns. */ +static void hid_internal_hotplug_cleanup(void) +{ + if (hid_hotplug_context.mutex_in_use) { return; } - pthread_join(hid_hotplug_context.thread, NULL); + for (;;) { + /* Before checking if the list is empty, clear any entries whose removal was postponed first */ + hid_internal_hotplug_remove_postponed(); + + if (hid_hotplug_context.hotplug_cbs != NULL) { + /* Still serving callbacks - the thread must keep running */ + return; + } + + if (hid_hotplug_context.thread_state == HID_HOTPLUG_THREAD_RUNNING) { + /* The thread has not yet noticed the empty callback list. Drop the + mutex so it can make progress towards publishing FINISHED, then + re-evaluate. */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); + poll(NULL, 0, 1); + pthread_mutex_lock(&hid_hotplug_context.mutex); + continue; + } + + /* NONE or FINISHED: retire the finished generation and join what we may. + The join drops the mutex, so another registration may start a new + generation meanwhile. Re-evaluate until the slot is NONE or callbacks + exist again; only hid_exit(), via exiting, excludes new generations. */ + hid_internal_hotplug_reap_thread(); + if (hid_hotplug_context.thread_state == HID_HOTPLUG_THREAD_NONE) { + return; + } + } } -static void hid_internal_hotplug_init() +static pthread_once_t hid_hotplug_init_once = PTHREAD_ONCE_INIT; + +/* The one-time initialization of the hotplug mutex, run by pthread_once(). + On failure mutex_ready stays 0 and the hotplug API remains unavailable. */ +static void hid_internal_hotplug_init_once(void) { - if (!hid_hotplug_context.mutex_ready) { - /* Initialize the mutex as recursive */ - pthread_mutexattr_t attr; - pthread_mutexattr_init(&attr); - pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); - pthread_mutex_init(&hid_hotplug_context.mutex, &attr); + pthread_mutexattr_t attr; + + if (pthread_mutexattr_init(&attr) != 0) { + return; + } + /* The mutex must be recursive: a callback runs with it held and is + allowed to call hid_hotplug_register_callback() / + hid_hotplug_deregister_callback() */ + if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) != 0) { pthread_mutexattr_destroy(&attr); + return; + } + if (pthread_mutex_init(&hid_hotplug_context.mutex, &attr) != 0) { + pthread_mutexattr_destroy(&attr); + return; + } + pthread_mutexattr_destroy(&attr); - /* Set state to Ready */ - hid_hotplug_context.mutex_ready = 1; - hid_hotplug_context.mutex_in_use = 0; - hid_hotplug_context.cb_list_dirty = 0; - hid_hotplug_context.monitor_fd = -1; - if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) - hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + /* Process-lifetime, like the mutex (never destroyed): lets hid_exit() wait for + a retired thread another reaper is joining, so no pthread_t is joined twice. */ + if (pthread_cond_init(&hid_hotplug_context.thread_cond, NULL) != 0) { + pthread_mutex_destroy(&hid_hotplug_context.mutex); + return; } + + hid_hotplug_context.monitor_fd = -1; + /* The handles are monotonic and never reused (see hidapi.h); the counter + survives hid_exit */ + hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + /* Non-zero monitor-thread generation tokens; survive hid_exit. + Start at 1 so that 0 unambiguously means "no current generation". */ + hid_hotplug_context.next_thread_id = 1; + + /* Publish the mutex as usable, last */ + hid_hotplug_context.mutex_ready = 1; +} + +/* Ensures the hotplug mutex exists. Returns 0 when the hotplug machinery is + usable, -1 when it could not be initialized (locking an uninitialized mutex + is undefined behavior, so the caller must fail). + The mutex is process-lifetime: it is deliberately never destroyed. + Destroying it in hid_exit() would race the concurrent (and allowed) + hid_hotplug_register_callback()/hid_hotplug_deregister_callback() calls + that are about to lock it - they can only re-check the state AFTER locking, + so the mutex itself must stay valid; teardown is gated by the `exiting` + flag INSIDE the mutex instead. There is deliberately no bootstrap lock + around the initialization either: any lock ordered outside the hotplug + mutex would deadlock against a registration made from within a callback + (which already holds the hotplug mutex); pthread_once() provides both the + one-time guarantee and the memory synchronization for reading mutex_ready. */ +static int hid_internal_hotplug_init(void) +{ + pthread_once(&hid_hotplug_init_once, hid_internal_hotplug_init_once); + + return hid_hotplug_context.mutex_ready ? 0 : -1; } -static void hid_internal_hotplug_exit() +static void hid_internal_hotplug_exit(void) { - if (!hid_hotplug_context.mutex_ready) { + if (hid_internal_hotplug_init() != 0) { + /* The hotplug mutex could not be created: nothing can ever have been + registered, and there is nothing to tear down */ return; } pthread_mutex_lock(&hid_hotplug_context.mutex); - struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - /* Remove all callbacks from the list */ - while (*current) { - struct hid_hotplug_callback* next = (*current)->next; - free(*current); - *current = next; + + if (hid_hotplug_context.exiting) { + /* Another hid_exit() is already tearing the machinery down */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return; } - hid_internal_hotplug_cleanup(); + + if (hid_hotplug_context.mutex_in_use) { + /* hid_exit() from within a hotplug callback has undefined behavior + (see hidapi.h); degrade gracefully instead of corrupting the + dispatch in flight below this frame: mark every callback for + removal and let the monitor thread wind itself down (releasing the + monitoring context) once the dispatch unwinds */ + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; callback; callback = callback->next) { + if (callback->events) { + callback->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } + } + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return; + } + + /* Close the hotplug API for the duration of the teardown: a concurrent + hid_hotplug_register_callback()/hid_hotplug_deregister_callback() + (allowed by the thread-safety contract) fails/no-ops instead of + re-arming the machinery while it is being torn down - + hid_internal_hotplug_cleanup() below temporarily drops the mutex + while reaping the monitor thread */ + hid_hotplug_context.exiting = 1; + + for (;;) { + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + /* Remove all callbacks from the list, dropping any undelivered snapshots */ + while (*current) { + struct hid_hotplug_callback* next = (*current)->next; + hid_free_enumeration((*current)->replay); + free(*current); + *current = next; + } + hid_hotplug_context.cb_list_dirty = 0; + + /* Wind the current generation down and reap what can be reaped + (temporarily dropping the mutex). `exiting` keeps anything from + re-arming the machinery or starting a new generation while the mutex is + dropped, so from here the retired list only ever shrinks. */ + hid_internal_hotplug_cleanup(); + + /* Join every generation possible before hid_exit() returns. `exiting` + keeps new ones from appearing. cleanup may leave entries being joined + by another (pre-exit) reaper, unattempted after a failed join, or marked + join_failed. Wait for another reaper's thread_cond broadcast or retry + cleanup for unattempted entries. Failed joins are never retried: once + only those entries remain, leave them in place and re-open the API + below instead of spinning. + + A monitor-thread destructor must neither join nor wait for a join: + its own completion may be needed by another reaper. Leave all entries + for an application-thread register/deregister that finds the callback + list empty, or hid_exit(), to join. This is the same degradation as + hid_exit() from within a callback; identity uses the thread-local token, + never a possibly claimed pthread_t. */ + if (hid_internal_on_monitor_thread()) { + break; + } + if (hid_hotplug_context.retired != NULL) { + int others = 0; + int joinable = 0; + + for (struct hid_hotplug_monitor_thread *entry = hid_hotplug_context.retired; entry != NULL; entry = entry->next) { + if (entry->being_joined) { + others = 1; + break; + } + if (!entry->join_failed) { + joinable = 1; + } + } + + if (others) { + pthread_cond_wait(&hid_hotplug_context.thread_cond, &hid_hotplug_context.mutex); + } else if (!joinable) { + break; + } + continue; + } + + if (hid_hotplug_context.hotplug_cbs == NULL + && hid_hotplug_context.thread_state == HID_HOTPLUG_THREAD_NONE) { + /* The retired list is empty: every generation has been joined. */ + break; + } + } + + /* Re-open the hotplug API: the library may be initialized/used again */ + hid_hotplug_context.exiting = 0; + pthread_mutex_unlock(&hid_hotplug_context.mutex); - hid_hotplug_context.mutex_ready = 0; - pthread_mutex_destroy(&hid_hotplug_context.mutex); } +/* Serializes the implicit initialization: the hotplug API allows concurrent + hid_hotplug_register_callback() calls, each of which implicitly initializes + the library - directly, and once more through the initial enumeration - and + setlocale() is not thread-safe against itself. */ +static pthread_mutex_t hid_init_mutex = PTHREAD_MUTEX_INITIALIZER; + int HID_API_EXPORT hid_init(void) { const char *locale; /* indicate no error */ - register_global_error(NULL); + if (!hid_internal_on_monitor_thread()) { + register_global_error(NULL); + } + pthread_mutex_lock(&hid_init_mutex); /* Set the locale if it's not set. */ locale = setlocale(LC_CTYPE, NULL); if (!locale) setlocale(LC_CTYPE, ""); + pthread_mutex_unlock(&hid_init_mutex); return 0; } @@ -1056,7 +1571,9 @@ int HID_API_EXPORT hid_init(void) int HID_API_EXPORT hid_exit(void) { /* Free global error message */ - register_global_error(NULL); + if (!hid_internal_on_monitor_thread()) { + register_global_error(NULL); + } hid_internal_hotplug_exit(); @@ -1068,7 +1585,16 @@ static int hid_internal_match_device_id(unsigned short vendor_id, unsigned short return (expected_vendor_id == 0x0 || vendor_id == expected_vendor_id) && (expected_product_id == 0x0 || product_id == expected_product_id); } -struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +/* Same as hid_enumerate, but distinguishes a genuine failure from an empty + system: *failure (when non-NULL) is set to 1 only when the enumeration itself + failed; the hotplug caller supplies the failure message. An empty result is + not a failure. Used by the initial hotplug-registration snapshot, which must fail + rather than arm the callbacks against an incomplete device set. + quiet: don't touch the global error string - for a caller running on HIDAPI's + monitor thread, which never writes it (see hidapi.h). The library is already + initialized in that case, so the implicit hid_init() (which resets the string) + is skipped as well. */ +static struct hid_device_info *hid_internal_enumerate(unsigned short vendor_id, unsigned short product_id, int *failure, int quiet) { struct udev *udev; struct udev_enumerate *enumerate; @@ -1077,20 +1603,61 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; - hid_init(); - /* register_global_error: global error is reset by hid_init */ + if (failure) { + *failure = 0; + } + + if (!quiet) { + hid_init(); + /* register_global_error: global error is reset by hid_init */ + } /* Create the udev object */ udev = udev_new(); if (!udev) { - register_global_error("Couldn't create udev context"); + if (!quiet) { + register_global_error("Couldn't create udev context"); + } + if (failure) { + *failure = 1; + } return NULL; } /* Create a list of the devices in the 'hidraw' subsystem. */ enumerate = udev_enumerate_new(udev); - udev_enumerate_add_match_subsystem(enumerate, "hidraw"); - udev_enumerate_scan_devices(enumerate); + if (!enumerate) { + udev_unref(udev); + if (!quiet) { + register_global_error("Couldn't create udev enumeration"); + } + if (failure) { + *failure = 1; + } + return NULL; + } + if (udev_enumerate_add_match_subsystem(enumerate, "hidraw") < 0) { + udev_enumerate_unref(enumerate); + udev_unref(udev); + if (!quiet) { + register_global_error("Couldn't add the hidraw subsystem match to the udev enumeration"); + } + if (failure) { + *failure = 1; + } + return NULL; + } + if (udev_enumerate_scan_devices(enumerate) < 0) { + udev_enumerate_unref(enumerate); + udev_unref(udev); + if (!quiet) { + register_global_error("Couldn't scan the udev devices"); + } + if (failure) { + *failure = 1; + } + return NULL; + } devices = udev_enumerate_get_list_entry(enumerate); /* For each item, see if it matches the vid/pid, and if so create a udev_device record for it */ @@ -1109,7 +1676,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, continue; if (vendor_id != 0 || product_id != 0) { - if (!parse_hid_vid_pid_from_sysfs(sysfs_path, &bus_type, &dev_vid, &dev_pid)) + if (!parse_hid_vid_pid_from_sysfs(sysfs_path, &bus_type, &dev_vid, &dev_pid, quiet)) continue; if (vendor_id != 0 && vendor_id != dev_vid) @@ -1119,10 +1686,18 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, } raw_dev = udev_device_new_from_syspath(udev, sysfs_path); - if (!raw_dev) + if (!raw_dev) { + /* Conservatively fail the initial snapshot if a listed path cannot + be read, even for a racing unplug; registration reports the failure + and the caller may retry. hid_enumerate() passes failure == NULL + and keeps its best-effort behavior. */ + if (failure) { + *failure = 1; + } continue; + } - tmp = create_device_info_for_device(raw_dev); + tmp = create_device_info_for_device(raw_dev, quiet, failure); if (tmp) { if (cur_dev) { cur_dev->next = tmp; @@ -1144,7 +1719,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, udev_enumerate_unref(enumerate); udev_unref(udev); - if (root == NULL) { + if (root == NULL && !quiet && !(failure && *failure)) { if (vendor_id == 0 && product_id == 0) { register_global_error("No HID devices found in the system."); } else { @@ -1155,6 +1730,11 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, return root; } +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + return hid_internal_enumerate(vendor_id, product_id, NULL, 0); +} + void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) { struct hid_device_info *d = devs; @@ -1169,25 +1749,110 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } -static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event) +/* Deep copy of a single hid_device_info entry; the next pointer of the copy is always NULL */ +static struct hid_device_info *hid_internal_copy_device_info(const struct hid_device_info *src) +{ + struct hid_device_info *dst = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (dst == NULL) { + return NULL; + } + + dst->path = src->path? strdup(src->path): NULL; + dst->vendor_id = src->vendor_id; + dst->product_id = src->product_id; + dst->serial_number = src->serial_number? wcsdup(src->serial_number): NULL; + dst->release_number = src->release_number; + dst->manufacturer_string = src->manufacturer_string? wcsdup(src->manufacturer_string): NULL; + dst->product_string = src->product_string? wcsdup(src->product_string): NULL; + dst->usage_page = src->usage_page; + dst->usage = src->usage; + dst->interface_number = src->interface_number; + dst->next = NULL; + dst->bus_type = src->bus_type; + + /* A partial copy must never reach a callback */ + if ((src->path && !dst->path) + || (src->serial_number && !dst->serial_number) + || (src->manufacturer_string && !dst->manufacturer_string) + || (src->product_string && !dst->product_string)) { + hid_free_enumeration(dst); + return NULL; + } + + return dst; +} + +/* Deliver the registration-time snapshot taken by hid_hotplug_register_callback() + with HID_API_HOTPLUG_ENUMERATE: the initial pass of synthetic "arrived" events. + Only ever runs on the monitor thread, with the mutex held. */ +static void hid_internal_hotplug_replay(struct hid_hotplug_callback *callback) +{ + unsigned char old_state = hid_hotplug_context.mutex_in_use; + hid_hotplug_context.mutex_in_use = 1; + + while (callback->replay) { + /* Detach one entry at a time, so a deregistration from within the callback + (or from another thread, once we return) never sees a dangling list */ + struct hid_device_info *device = callback->replay; + callback->replay = device->next; + device->next = NULL; + if ((*callback->callback)(callback->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, callback->user_data)) { + /* A non-zero return deregisters the callback and stops the remainder of the pass */ + callback->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } + hid_free_enumeration(device); + if (!callback->events) { + /* Deregistered (by return value or from within the callback): drop the undelivered entries */ + hid_free_enumeration(callback->replay); + callback->replay = NULL; + } + } + + hid_hotplug_context.mutex_in_use = old_state; +} + +/* Deliver the pending initial passes of all registered callbacks. + Only ever runs on the monitor thread, with the mutex held (and not in use). */ +static void hid_internal_hotplug_process_replays(void) +{ + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; callback; callback = callback->next) { + if (callback->events && callback->replay) { + hid_internal_hotplug_replay(callback); + } + } + + hid_internal_hotplug_remove_postponed(); +} + +/* Deliver one live event to the matching callbacks whose handle does not + exceed dispatch_bound. The bound freezes the dispatch to the callbacks + registered before the event started: a callback registered from within + a callback observes the device through its registration snapshot instead + of the in-flight event, keeping the arrivals exactly-once (the handles + are monotonic and the list is kept in registration order). */ +static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event, hid_hotplug_callback_handle dispatch_bound) { pthread_mutex_lock(&hid_hotplug_context.mutex); hid_hotplug_context.mutex_in_use = 1; - struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - while (*current) { - struct hid_hotplug_callback *callback = *current; + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; + callback != NULL && callback->handle <= dispatch_bound; + callback = callback->next) { + /* Flush the callback's pending initial pass first, so it never observes + a live event before the synthetic events of HID_API_HOTPLUG_ENUMERATE */ + if (callback->events && callback->replay) { + hid_internal_hotplug_replay(callback); + } if ((callback->events & event) && hid_internal_match_device_id(info->vendor_id, info->product_id, callback->vendor_id, callback->product_id)) { int result = callback->callback(callback->handle, info, event, callback->user_data); /* If the result is non-zero, we mark the callback for removal and proceed */ if (result) { - (*current)->events = 0; + callback->events = 0; hid_hotplug_context.cb_list_dirty = 1; - continue; } } - current = &callback->next; } hid_hotplug_context.mutex_in_use = 0; @@ -1195,105 +1860,361 @@ static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotp pthread_mutex_unlock(&hid_hotplug_context.mutex); } -static int match_udev_to_info(struct udev_device* raw_dev, struct hid_device_info *info) +static struct hid_device_info *hid_internal_find_device_in_list(struct hid_device_info *list, const char *path) { - const char *path = udev_device_get_devnode(raw_dev); - if (!strcmp(path, info->path)) { - return 1; + if (path == NULL) { + return NULL; } - return 0; + for (struct hid_device_info *device = list; device; device = device->next) { + if (device->path && !strcmp(device->path, path)) { + return device; + } + } + return NULL; } -static void* hotplug_thread(void* user_data) +static struct hid_device_info *hid_internal_find_device_by_path(const char *path) { - (void) user_data; + return hid_internal_find_device_in_list(hid_hotplug_context.devs, path); +} - /* Note: the cleanup sequence is always executed with the mutex locked, so we shoud never lock the mutex without checking if we need to stop */ +/* Handle a device arrival: update the connected-device cache and dispatch the + callbacks. Takes ownership of the whole device chain (one entry per usage). + Only ever runs on the monitor thread, with the mutex held. */ +static void hid_internal_hotplug_process_arrival(struct hid_device_info *info) +{ + hid_hotplug_callback_handle dispatch_bound; + struct hid_device_info *copies = NULL; + struct hid_device_info **copies_tail = &copies; - while (hid_hotplug_context.monitor_fd > 0) { - fd_set fds; - struct timeval tv; - int ret; + if (info == NULL) { + return; + } + if (info->path == NULL) { + hid_free_enumeration(info); + return; + } - /* On every iteration, check if we still have any callbacks left and leave if none are left */ - /* NOTE: the check is performed UNLOCKED and the value CAN change in the background */ - if (!hid_hotplug_context.hotplug_cbs) { - break; + /* The device may already be known: a device arriving between arming the + udev monitor and taking the initial enumeration at first registration + is both in the cache and queued as an event on the monitor socket. + Skip the queued duplicate arrival. This devnode-only key cannot distinguish + a queued removal of a predecessor that reused the same /dev/hidrawN while + the snapshot already contains its successor: that window can still produce + ARRIVED/LEFT/ARRIVED for the successor. */ + if (hid_internal_find_device_by_path(info->path) != NULL) { + hid_free_enumeration(info); + return; + } + + /* Copy every usage entry up front, for the one-entry-per-invocation + dispatch below (the callback must always see device->next == NULL, and + a copy keeps the cache walkable for the snapshots of callbacks + registered from within a callback). On failure the whole arrival is + dropped BEFORE anything is observable: dispatching entries of the live + cache chain instead would temporarily truncate the cache, and a + callback registered during such a dispatch would miss the truncated + entries in its snapshot - yet receive their "left" events later. */ + for (struct hid_device_info *info_cur = info; info_cur; info_cur = info_cur->next) { + *copies_tail = hid_internal_copy_device_info(info_cur); + if (*copies_tail == NULL) { + /* Out of memory: this device is never reported at all */ + hid_free_enumeration(copies); + hid_free_enumeration(info); + return; } + copies_tail = &(*copies_tail)->next; + } - FD_ZERO(&fds); - FD_SET(hid_hotplug_context.monitor_fd, &fds); - /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ - /* This timeout only affects how much time it takes to stop the thread */ - tv.tv_sec = 0; - tv.tv_usec = 5000; + /* Append to the cache BEFORE dispatching, so an ENUMERATE snapshot taken + by a callback registered from within a callback captures the whole + device rather than the in-flight event */ + if (hid_hotplug_context.devs != NULL) { + struct hid_device_info *last = hid_hotplug_context.devs; + while (last->next != NULL) { + last = last->next; + } + last->next = info; + } else { + hid_hotplug_context.devs = info; + } - ret = select(hid_hotplug_context.monitor_fd+1, &fds, NULL, NULL, &tv); + /* Freeze the dispatch to the callbacks registered up to this point */ + dispatch_bound = hid_hotplug_context.next_handle - 1; - /* An extra check, just in case within those 5msec the thread was told to stop */ - if (!hid_hotplug_context.hotplug_cbs) { - break; + while (copies != NULL) { + struct hid_device_info *copy = copies; + copies = copy->next; + /* One usage entry per invocation */ + copy->next = NULL; + hid_internal_invoke_callbacks(copy, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, dispatch_bound); + hid_free_enumeration(copy); + } +} + +/* Handle a device removal: detach the matching entries from the + connected-device cache and dispatch the callbacks. + Only ever runs on the monitor thread, with the mutex held. */ +static void hid_internal_hotplug_process_removal(const char *devnode) +{ + /* Freeze the dispatch to the callbacks registered up to this point + (see hid_internal_hotplug_process_arrival) */ + hid_hotplug_callback_handle dispatch_bound = hid_hotplug_context.next_handle - 1; + struct hid_device_info *removed = NULL; + struct hid_device_info **removed_tail = &removed; + + /* Detach every usage entry of the device from the cache BEFORE dispatching + any of them: a callback registered from within this dispatch is excluded + from it by the bound, so it must not be able to capture a still-cached + usage entry of the leaving device in its ENUMERATE snapshot either - + that would be an "arrived" without a matching "left" */ + for (struct hid_device_info **current = &hid_hotplug_context.devs; *current;) { + struct hid_device_info *info = *current; + if (info->path && !strcmp(devnode, info->path)) { + *current = info->next; + info->next = NULL; + *removed_tail = info; + removed_tail = &info->next; + } else { + current = &info->next; } + } - /* Check if our file descriptor has received data. */ - if (ret > 0 && FD_ISSET(hid_hotplug_context.monitor_fd, &fds)) { - - /* Make the call to receive the device. - select() ensured that this will not block. */ - struct udev_device *raw_dev = udev_monitor_receive_device(hid_hotplug_context.mon); - if (raw_dev) { - pthread_mutex_lock(&hid_hotplug_context.mutex); - const char* action = udev_device_get_action(raw_dev); - if (!strcmp(action, "add")) { - // We create a list of all usages on this UDEV device - struct hid_device_info *info = create_device_info_for_device(raw_dev); - struct hid_device_info *info_cur = info; - while (info_cur) { - /* For each device, call all matching callbacks */ - /* TODO: possibly make the `next` field NULL to match the behavior on other systems */ - hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); - info_cur = info_cur->next; - } + while (removed != NULL) { + struct hid_device_info *info = removed; + removed = info->next; + /* One usage entry per invocation: the callback always sees next == NULL */ + info->next = NULL; + hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, dispatch_bound); + /* Free every removed device */ + hid_free_enumeration(info); + } +} + +/* Dispatch one udev monitor event. + Only ever runs on the monitor thread, with the mutex held. */ +static void hid_internal_hotplug_process_event(struct udev_device *raw_dev) +{ + const char *action = udev_device_get_action(raw_dev); + if (action == NULL) { + return; + } + + if (!strcmp(action, "add")) { + /* We create a list of all usages on this UDEV device. + A device whose information cannot be read/parsed stays out of the + cache and is invisible to the callbacks - exactly as it would be + invisible to hid_enumerate(); there is nothing meaningful to + deliver instead. quiet: this runs on the monitor thread, which + never writes the global error string (see hidapi.h). */ + int arrival_failure = 0; + struct hid_device_info *info = create_device_info_for_device(raw_dev, 1, &arrival_failure); + if (arrival_failure) { + /* Drop incomplete or degraded chains on resource failure. + process_arrival also rejects devices with no devnode, since those + cannot be matched by removal or recognized as duplicates. */ + hid_free_enumeration(info); + info = NULL; + } + hid_internal_hotplug_process_arrival(info); + } else if (!strcmp(action, "remove")) { + const char *devnode = udev_device_get_devnode(raw_dev); + char devnode_buf[32]; + if (devnode == NULL) { + /* A remove event does not always carry the device node. The + cache is keyed by the node path, which for a hidraw device is + always "/dev/": reconstruct it, or the stale cache + entry would suppress - as a duplicate - the arrival of the + next device that reuses the same node */ + const char *sysname = udev_device_get_sysname(raw_dev); + if (sysname != NULL) { + int len = snprintf(devnode_buf, sizeof(devnode_buf), "/dev/%s", sysname); + if (len > 0 && (size_t)len < sizeof(devnode_buf)) { + devnode = devnode_buf; + } + } + } + if (devnode != NULL) { + hid_internal_hotplug_process_removal(devnode); + } + } +} + +/* Consecutive poll() reports of an error condition on the udev monitor + socket - with a drain attempt in between each - after which the socket is + considered dead. ENOBUFS stops live delivery immediately because event + history was lost; EBADF, ENOTSOCK, ECONNRESET and ENOTCONN indicate a broken + transport. Other receive errors, such as per-message parse failures, end + the drain without killing the monitor. */ +#define HID_HOTPLUG_SOCKET_ERROR_POLL_LIMIT 100 + +static void* hotplug_thread(void* user_data) +{ + int monitor_fd; + int socket_error_polls = 0; + int socket_dead = 0; + struct pollfd fds; + int ret = 0; + int poll_error = 0; + + (void) user_data; + + /* The startup lock waits for thread_id, published after pthread_create(). + The creating registration releases it after publishing RUNNING, and no + reaper can be joining this generation yet. monitor_fd is already ordered + by pthread_create() and stays immutable until this thread winds down. */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + /* Learn our own generation id, for the join-safe self-identification the + library needs once this thread's pthread_t may have been claimed for a + join (see hid_hotplug_thread_self_id). The value read here is ours: the + creating registration holds the mutex continuously from pthread_create() + until after it published this id, and a newer generation cannot be + created before this one publishes FINISHED below. */ + hid_hotplug_thread_self_id = hid_hotplug_context.thread_id; + monitor_fd = hid_hotplug_context.monitor_fd; + pthread_mutex_unlock(&hid_hotplug_context.mutex); - /* Append all we got to the end of the device list */ - if (info) { - if (hid_hotplug_context.devs != NULL) { - struct hid_device_info *last = hid_hotplug_context.devs; - while (last->next != NULL) { - last = last->next; - } - last->next = info; - } else { - hid_hotplug_context.devs = info; + /* From here on the main loop takes the mutex with trylock only: it must never + block on the mutex, so that it always makes progress towards its exit check + while hid_internal_hotplug_cleanup() waits, without holding the mutex, + for it to announce its own exit. All shared state, including the loop + decisions, is only ever accessed with the mutex held. */ + + for (;;) { + int stop = 0; + + if (pthread_mutex_trylock(&hid_hotplug_context.mutex) != 0) { + /* Contended: back off shortly and retry. Polling the monitor fd + here would spin, as it stays readable until the queued events + are drained (which needs the mutex). */ + poll(NULL, 0, 1); + continue; + } + + /* Classify the previous poll result and publish death under the same + lock, so a registration cannot attach between those two actions. */ + if (ret < 0) { + if (poll_error != EINTR && ++socket_error_polls >= HID_HOTPLUG_SOCKET_ERROR_POLL_LIMIT) { + socket_dead = 1; + } + } else if (ret > 0 && !(fds.revents & POLLIN)) { + if (fds.revents & (POLLHUP | POLLNVAL)) { + socket_dead = 1; + } else if (++socket_error_polls >= HID_HOTPLUG_SOCKET_ERROR_POLL_LIMIT) { + socket_dead = 1; + } + } else { + socket_error_polls = 0; + } + + if (hid_hotplug_context.hotplug_cbs == NULL) { + /* The last callback is gone: release the monitoring context (the + device cache, the udev monitor and its context) right away - + when the last callback removed itself from within a callback, + no further hotplug call is guaranteed to come and reap it - + then publish the exit under the mutex and stop touching any + shared state. A monitoring context created after this point + belongs to a new thread; the release is idempotent, so a + claimant repeating it is a no-op. */ + hid_internal_hotplug_release_monitor(); + hid_hotplug_context.monitor_dead = 0; + /* Publish FINISHED as the last write under the mutex (the thread + touches no shared state below this point, and everything it owned + has just been released), then unlock and return. The pthread_t + stays JOINABLE: hid_exit() - and the next register/deregister - + reap it (see hid_internal_hotplug_reap_thread), so no + monitor-thread instruction is still running inside the library + once hid_exit() returns. When the last callback deregistered + itself from within a callback and the application then never calls + HIDAPI again, this one pthread_t lingers unjoined until hid_exit() + reaps it - a bounded, single-thread leak that is strictly better + than a detached thread resuming in unmapped code after dlclose(). */ + hid_hotplug_context.thread_state = HID_HOTPLUG_THREAD_FINISHED; + stop = 1; + } else { + if (socket_dead && !hid_hotplug_context.monitor_dead) { + /* The udev monitor socket died unrecoverably (see the poll() + handling below). Stop delivering events cleanly: losing the + event transport is NOT evidence that the devices left, so do + NOT fabricate removals and do NOT re-enumerate or diff. The + device cache is left exactly as it is and the application's + open handles are untouched (those devices are still physically + connected). Mark the machinery dead so new registrations are + refused (hid_hotplug_register_callback) - that refusal is the + observable failure; the global error string is NOT written + from this thread (see hidapi.h). + + After an unrecoverable monitor failure, no further live hotplug + event is delivered for this machinery generation; devices + already reported remain in the cache and open handles are + unaffected. The thread keeps idling (still flushing any pending + initial ENUMERATE passes) until its callbacks are deregistered, + then winds down and releases everything (the dead monitor + included). */ + hid_hotplug_context.monitor_dead = 1; + } + + /* Deliver the pending initial passes of HID_API_HOTPLUG_ENUMERATE + before any queued live events */ + hid_internal_hotplug_process_replays(); + + if (!socket_dead) { + /* Drain and dispatch the events queued on the (non-blocking) + udev monitor socket. EINTR retries; EAGAIN/EWOULDBLOCK end the + drain. Only the transport errors listed above stop live delivery; + per-message failures have consumed the datagram, so end this + drain and let poll() resume on any remaining data. */ + for (;;) { + errno = 0; + struct udev_device *raw_dev = udev_monitor_receive_device(hid_hotplug_context.mon); + if (raw_dev == NULL) { + if (errno == EINTR) { + continue; } - } - } else if (!strcmp(action, "remove")) { - for (struct hid_device_info **current = &hid_hotplug_context.devs; *current;) { - struct hid_device_info* info = *current; - if (match_udev_to_info(raw_dev, *current)) { - /* If the libusb device that's left matches this HID device, we detach it from the list */ - *current = (*current)->next; - info->next = NULL; - hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); - /* Free every removed device */ - hid_free_enumeration(info); - } else { - current = &info->next; + if (errno == ENOBUFS || errno == EBADF || errno == ENOTSOCK + || errno == ECONNRESET || errno == ENOTCONN) { + socket_dead = 1; + hid_hotplug_context.monitor_dead = 1; } + break; } + hid_internal_hotplug_process_event(raw_dev); + udev_device_unref(raw_dev); } - udev_device_unref(raw_dev); - pthread_mutex_unlock(&hid_hotplug_context.mutex); } } - } - /* Cleanup connected device list */ - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; - /* Disarm the udev monitor */ - udev_monitor_unref(hid_hotplug_context.mon); - udev_unref(hid_hotplug_context.udev_ctx); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + if (stop) { + break; + } + + if (socket_dead) { + /* The dead socket is no longer polled; release_monitor frees it + when the last callback is gone. Pace replay flushes and exit checks. */ + struct timespec pace = { 0, 5000000 }; + ret = 0; + nanosleep(&pace, NULL); + continue; + } + if (socket_error_polls && !(ret < 0 && poll_error == EINTR)) { + /* Pace persistent poll failures independently of poll() itself. */ + struct timespec pace = { 0, 5000000 }; + nanosleep(&pace, NULL); + } + + /* Wait for udev events; the timeout paces the mutex retries and caps + the latency of the initial HID_API_HOTPLUG_ENUMERATE passes. + 5 msec seems reasonable; don't set too low to avoid high CPU usage. */ + fds.fd = monitor_fd; + fds.events = POLLIN; + fds.revents = 0; + ret = poll(&fds, 1, 5); + if (ret < 0) { + poll_error = errno; + } + } return NULL; } @@ -1301,18 +2222,88 @@ static void* hotplug_thread(void* user_data) int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle) { struct hid_hotplug_callback* hotplug_cb; + int quiet; + + /* No events can be delivered before the out parameter is written */ + if (callback_handle != NULL) { + *callback_handle = 0; + } + + /* Ensure we are ready to actually use the mutex (the mutex is + process-lifetime and never destroyed - see hid_internal_hotplug_init). + This can only fail before any callback was ever registered - i.e. never + on the monitor thread - so the error string is safe to write here */ + if (hid_internal_hotplug_init() != 0) { + register_global_error("Couldn't initialize the hotplug mutex"); + return -1; + } + + /* Lock the mutex to avoid race conditions */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + /* A registration made from within a callback runs on HIDAPI's internal + monitor thread, which holds this (recursive) mutex for the whole + dispatch - hence mutex_in_use. Such a call must leave the global error + string alone: HIDAPI's internal threads never write it (see hidapi.h), + or an application that reads it with hid_error(NULL) on another thread + would race a write it cannot serialize against. This is why even the + parameter checks below run under the mutex. + mutex_in_use only covers a call made from within a dispatch: a + thread-specific-data destructor armed by a user callback re-enters here on + the monitor thread after the dispatch unwound (and after the thread + published FINISHED), so the thread identity is checked as well. */ + quiet = hid_hotplug_context.mutex_in_use || hid_internal_on_monitor_thread(); /* Check params */ + if (callback == NULL) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("Hotplug callback function is NULL"); + } + return -1; + } if (events == 0 - || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT)) - || (flags & ~(HID_API_HOTPLUG_ENUMERATE)) - || callback == NULL) { + || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT))) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("Hotplug events mask contains no valid events or unknown bits"); + } + return -1; + } + if (flags & ~(HID_API_HOTPLUG_ENUMERATE)) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("Hotplug flags mask contains unknown bits"); + } + return -1; + } + + if (hid_hotplug_context.exiting) { + /* hid_exit() is tearing the machinery down: it deregisters every + callback itself, so there is nothing to register into */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("hid_exit() is in progress"); + } return -1; } + if (!quiet) { + /* Implicit hid_init: unlike the other API entry points, concurrent + registrations are allowed - hid_init() serializes itself. + A nested registration skips it: the library is initialized by then, + and hid_init() resets the global error string (see above) */ + hid_init(); + /* register_global_error: global error is reset by hid_init */ + } + hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); if (hotplug_cb == NULL) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("Failed to allocate a hotplug callback record"); + } return -1; } @@ -1323,26 +2314,57 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven hotplug_cb->events = events; hotplug_cb->user_data = user_data; hotplug_cb->callback = callback; + hotplug_cb->replay = NULL; - /* Ensure we are ready to actually use the mutex */ - hid_internal_hotplug_init(); - - /* Lock the mutex to avoid race conditions */ - pthread_mutex_lock(&hid_hotplug_context.mutex); + /* Reap the monitor thread first in case it is exiting (or has exited) + after the removal of its last callback, releasing the previous + monitoring context with it (may temporarily drop the mutex) */ + hid_internal_hotplug_cleanup(); - hotplug_cb->handle = hid_hotplug_context.next_handle++; + /* hid_exit() may have started while the mutex was dropped above: fail + instead of re-arming the machinery it is tearing down */ + if (hid_hotplug_context.exiting) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (!quiet) { + register_global_error("hid_exit() is in progress"); + } + return -1; + } - /* handle the unlikely case of handle overflow */ - if (hid_hotplug_context.next_handle < 0) - { - hid_hotplug_context.next_handle = 1; + if (hid_hotplug_context.monitor_dead) { + /* The udev monitor socket died (see hotplug_thread): this machinery + can deliver no further events, so refuse to attach to it. It winds + down once the surviving callbacks are deregistered; a registration + after that rebuilds it. */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (!quiet) { + register_global_error("The hotplug monitor is not operational (the udev monitor socket failed)"); + } + return -1; } - /* Return allocated handle */ - if (callback_handle != NULL) { - *callback_handle = hotplug_cb->handle; + /* The handles are monotonic and never reused: fail instead of overflowing */ + if (hid_hotplug_context.next_handle == INT_MAX) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (!quiet) { + register_global_error("Hotplug callback handles exhausted"); + } + return -1; } + /* Allocate the handle only after hid_internal_hotplug_cleanup() above: + it may temporarily drop the mutex, and the dispatch bounds rely on the + callback list being in (monotonic) handle order - a handle allocated + before the drop could get linked in after a younger one. The mutex is + then held continuously from this allocation up to (at least) the + unwinding point of every failure path below, so those paths can safely + return the handle to the counter (a failed registration must not + consume handles). */ + hotplug_cb->handle = hid_hotplug_context.next_handle++; + /* Append a new callback to the end */ if (hid_hotplug_context.hotplug_cbs != NULL) { struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; @@ -1352,64 +2374,247 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven last->next = hotplug_cb; } else { - // Prepare a UDEV context to run monitoring on - hid_hotplug_context.udev_ctx = udev_new(); - if (!hid_hotplug_context.udev_ctx) - { + int enumerate_failure = 0; + const char *monitor_error = NULL; + + /* cleanup settles an empty callback list to NONE. Never overwrite a + generation if that invariant changes. */ + if (hid_hotplug_context.thread_state != HID_HOTPLUG_THREAD_NONE) { + hid_hotplug_context.next_handle--; pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (!quiet) { + register_global_error("The previous hotplug monitor is still active"); + } return -1; } - hid_hotplug_context.mon = udev_monitor_new_from_netlink(hid_hotplug_context.udev_ctx, "udev"); - udev_monitor_filter_add_match_subsystem_devtype(hid_hotplug_context.mon, "hidraw", NULL); - udev_monitor_enable_receiving(hid_hotplug_context.mon); - hid_hotplug_context.monitor_fd = udev_monitor_get_fd(hid_hotplug_context.mon); + /* This branch is never taken from within a dispatch: the callback list + is empty here, while a dispatch always has at least the callback it + is dispatching to in the list (a deregistration from within a + callback only tombstones the record). It CAN still run on the monitor + thread - a thread-specific-data destructor re-registering after the + last callback is gone - so the global error writes below, the + hid_internal_enumerate() ones included, are all made conditional on + quiet: HIDAPI's internal threads never write that string */ + + /* Prepare a UDEV context to run monitoring on */ + hid_hotplug_context.udev_ctx = udev_new(); + if (!hid_hotplug_context.udev_ctx) { + monitor_error = "Couldn't create udev context"; + } + + if (!monitor_error) { + hid_hotplug_context.mon = udev_monitor_new_from_netlink(hid_hotplug_context.udev_ctx, "udev"); + if (!hid_hotplug_context.mon) { + monitor_error = "Couldn't create udev monitor"; + } + } + if (!monitor_error && udev_monitor_filter_add_match_subsystem_devtype(hid_hotplug_context.mon, "hidraw", NULL) < 0) { + monitor_error = "Couldn't add the hidraw filter to the udev monitor"; + } + if (!monitor_error && udev_monitor_enable_receiving(hid_hotplug_context.mon) < 0) { + monitor_error = "Couldn't enable receiving on the udev monitor"; + } + if (!monitor_error) { + /* 0 is a valid file descriptor: only negative values are errors */ + hid_hotplug_context.monitor_fd = udev_monitor_get_fd(hid_hotplug_context.mon); + if (hid_hotplug_context.monitor_fd < 0) { + monitor_error = "Couldn't get the udev monitor file descriptor"; + } + } + + if (!monitor_error) { + /* After monitoring is all set up, enumerate all devices: a failure + here would leave pre-connected devices without their "left" + events later, so it fails the registration (unlike an empty + system, which is not an error) */ + hid_hotplug_context.devs = hid_internal_enumerate(0, 0, &enumerate_failure, quiet); + if (!enumerate_failure && !quiet) { + register_global_error(NULL); + } + } - /* After monitoring is all set up, enumerate all devices */ - hid_hotplug_context.devs = hid_enumerate(0, 0); + if (monitor_error || enumerate_failure) { + /* The handle never became visible: return it to the counter */ + hid_hotplug_context.next_handle--; + hid_internal_hotplug_release_monitor(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (monitor_error && !quiet) { + register_global_error(monitor_error); + } else if (enumerate_failure && !quiet) { + register_global_error("Couldn't take the initial device snapshot for the hotplug registration"); + } + return -1; + } /* Don't forget to actually register the callback */ hid_hotplug_context.hotplug_cbs = hotplug_cb; - /* Start the thread that will be doing the event scanning */ - pthread_create(&hid_hotplug_context.thread, NULL, &hotplug_thread, NULL); + /* Start the event-scanning thread. cleanup's loop exit condition and the + defensive check above guarantee the current slot is NONE here. + The thread is JOINABLE: after releasing its context it publishes + FINISHED, and an application-thread register/deregister that finds the + callback list empty, or hid_exit(), joins it. */ + /* Reserve its node before creation; see hid_internal_hotplug_retire_current. */ + struct hid_hotplug_monitor_thread *thread_node = + (struct hid_hotplug_monitor_thread *)calloc(1, sizeof(*thread_node)); + if (thread_node == NULL) { + hid_hotplug_context.hotplug_cbs = NULL; + /* The handle never became visible: return it to the counter */ + hid_hotplug_context.next_handle--; + hid_internal_hotplug_release_monitor(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (!quiet) { + register_global_error("Couldn't allocate the hotplug monitor thread record"); + } + return -1; + } + + /* Always a no-op after cleanup settles the slot; retained as a guard + against a future cleanup change leaving a FINISHED predecessor. */ + hid_internal_hotplug_retire_current(); + + int thread_error = pthread_create(&hid_hotplug_context.thread, NULL, &hotplug_thread, NULL); + + if (thread_error) { + free(thread_node); + hid_hotplug_context.hotplug_cbs = NULL; + /* The handle never became visible: return it to the counter */ + hid_hotplug_context.next_handle--; + hid_internal_hotplug_release_monitor(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (!quiet) { + register_global_error("Couldn't create the hotplug monitor thread"); + } + return -1; + } + /* The new generation now owns its pre-allocated retired-list node. Stamp + it with a fresh stable id token, then publish it RUNNING (all under the + mutex, before the thread can do anything). */ + hid_hotplug_context.thread_node = thread_node; + hid_hotplug_context.thread_id = hid_hotplug_context.next_thread_id; + if (++hid_hotplug_context.next_thread_id == 0) { + hid_hotplug_context.next_thread_id = 1; + } + hid_hotplug_context.thread_state = HID_HOTPLUG_THREAD_RUNNING; } - /* Mark the mutex as IN USE, to prevent callback removal from inside a callback */ - unsigned char old_state = hid_hotplug_context.mutex_in_use; - hid_hotplug_context.mutex_in_use = 1; - if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { - struct hid_device_info* device = hid_hotplug_context.devs; - /* Notify about already connected devices, if asked so */ - while (device != NULL) { - if (hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { - (*hotplug_cb->callback)(hotplug_cb->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, hotplug_cb->user_data); + /* Take a snapshot of the matching connected devices: the monitor thread + delivers it asynchronously as the initial pass of synthetic "arrived" + events, before any live events for this callback and never from + within this call */ + int snapshot_failure = 0; + struct hid_device_info **replay_tail = &hotplug_cb->replay; + for (struct hid_device_info *device = hid_hotplug_context.devs; device != NULL; device = device->next) { + if (!hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { + continue; + } + *replay_tail = hid_internal_copy_device_info(device); + if (*replay_tail == NULL) { + snapshot_failure = 1; + break; } + replay_tail = &(*replay_tail)->next; + } - device = device->next; + if (snapshot_failure) { + /* The initial pass is all-or-nothing (each device connection is + reported exactly once - never "neither"): unwind the whole + registration. No event can have been delivered yet: the mutex + was held since the callback was linked in. */ + for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { + if (*current == hotplug_cb) { + *current = hotplug_cb->next; + break; + } + } + hid_free_enumeration(hotplug_cb->replay); + /* The handle never became visible: return it to the counter */ + hid_hotplug_context.next_handle--; + /* Reap the monitor thread in case this was the only callback */ + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + if (!quiet) { + register_global_error("Couldn't allocate the device snapshot for the hotplug enumerate pass"); + } + return -1; } } - hid_hotplug_context.mutex_in_use = old_state; - - hid_internal_hotplug_cleanup(); + /* Return the allocated handle: written before the mutex is released, i.e. + before any event can be delivered to the callback */ + if (callback_handle != NULL) { + *callback_handle = hotplug_cb->handle; + } pthread_mutex_unlock(&hid_hotplug_context.mutex); - + return 0; } int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - if (!hid_hotplug_context.mutex_ready || callback_handle <= 0) { + int quiet; + + if (hid_internal_hotplug_init() != 0) { + /* The hotplug mutex could not be created: nothing can ever have been + registered (and this can never run on the monitor thread, so the + error string is safe to write here) */ + register_global_error("No hotplug callbacks are registered"); return -1; } pthread_mutex_lock(&hid_hotplug_context.mutex); + /* A deregistration made from within a callback - or from a thread-specific- + data destructor running on the monitor thread once the dispatch unwound - + runs on HIDAPI's internal monitor thread, which never writes the global + error string, the parameter check included (see + hid_hotplug_register_callback) */ + quiet = hid_hotplug_context.mutex_in_use || hid_internal_on_monitor_thread(); + + if (callback_handle <= 0) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("Invalid hotplug callback handle"); + } + return -1; + } + + if (hid_hotplug_context.exiting) { + /* hid_exit() is tearing the machinery down: it deregisters every + callback and invalidates every handle itself */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("No hotplug callbacks are registered"); + } + return -1; + } + + /* Reap a monitor thread that has wound down after the removal of its last + callback but has not been joined yet (may temporarily drop the mutex) */ + hid_internal_hotplug_cleanup(); + + /* hid_exit() may have started while the mutex was dropped above */ + if (hid_hotplug_context.exiting) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("No hotplug callbacks are registered"); + } + return -1; + } + if (hid_hotplug_context.hotplug_cbs == NULL) { pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (!quiet) { + register_global_error("No hotplug callbacks are registered"); + } return -1; } @@ -1418,12 +2623,19 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_call /* Remove this notification */ for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { if ((*current)->handle == callback_handle) { + /* A record deregistered from within a callback (awaiting its + postponed removal) is already gone for the caller: not found */ + if (!(*current)->events) { + break; + } /* Check if we were already in a locked state, as we are NOT allowed to remove any callbacks if we are */ if (hid_hotplug_context.mutex_in_use) { (*current)->events = 0; hid_hotplug_context.cb_list_dirty = 1; } else { struct hid_hotplug_callback *next = (*current)->next; + /* A deregistered callback never fires again: drop its undelivered snapshot */ + hid_free_enumeration((*current)->replay); free(*current); *current = next; } @@ -1436,6 +2648,12 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_call pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (result < 0 && !quiet) { + register_global_error("Hotplug callback handle not found"); + } else if (!quiet) { + register_global_error(NULL); + } + return result; } diff --git a/mac/hid.c b/mac/hid.c index 70951da72..1c717027d 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -66,10 +67,10 @@ static int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrie return -1; } - if (pthread_mutex_init(&barrier->mutex, 0) < 0) { + if (pthread_mutex_init(&barrier->mutex, 0) != 0) { return -1; } - if (pthread_cond_init(&barrier->cond, 0) < 0) { + if (pthread_cond_init(&barrier->cond, 0) != 0) { pthread_mutex_destroy(&barrier->mutex); return -1; } @@ -261,6 +262,16 @@ static void register_error_str_vformat(wchar_t **error_str, const char *format, register_error_str(error_str, msg); } +/* True when the calling thread is HIDAPI's internal hotplug event thread; used + to suppress writes to the global error string made from that thread (see the + definition after the hotplug context for the full rationale). Takes no lock. */ +static int hid_internal_on_event_thread(void); + +/* Serializes the mutations of the global error string: the hotplug API is + thread-safe and its failure paths (and the implicit hid_init()) may write + the global error from multiple threads concurrently. */ +static pthread_mutex_t global_error_mutex = PTHREAD_MUTEX_INITIALIZER; + /* Set the last global error to be reported by hid_error(NULL). * The given error message will be copied (and decoded according to the * currently locale, so do not pass in string constants). @@ -268,15 +279,30 @@ static void register_error_str_vformat(wchar_t **error_str, const char *format, * Use register_global_error(NULL) to indicate "no error". */ static void register_global_error(const char *msg) { + /* Honor the cross-backend contract (see hidapi.h): a global-error write + attempted on the internal hotplug event thread - e.g. from a + hid_hotplug_(de)register_callback() call re-entered from within a user + callback - must not touch the global error string. Per-device errors go + through register_error_str() with a different target and are unaffected; + only this process-global string is suppressed. */ + if (hid_internal_on_event_thread()) + return; + pthread_mutex_lock(&global_error_mutex); register_error_str(&last_global_error_str, msg); + pthread_mutex_unlock(&global_error_mutex); } /* Similar to register_global_error, but allows passing a format string into this function. */ static void register_global_error_format(const char *format, ...) { va_list args; + /* See register_global_error(): suppressed on the internal event thread. */ + if (hid_internal_on_event_thread()) + return; va_start(args, format); + pthread_mutex_lock(&global_error_mutex); register_error_str_vformat(&last_global_error_str, format, args); + pthread_mutex_unlock(&global_error_mutex); va_end(args); } @@ -438,7 +464,8 @@ static wchar_t *dup_wcs(const wchar_t *s) { size_t len = wcslen(s); wchar_t *ret = (wchar_t*) malloc((len+1)*sizeof(wchar_t)); - wcscpy(ret, s); + if (ret) + wcscpy(ret, s); return ret; } @@ -476,6 +503,11 @@ struct hid_hotplug_callback { void *user_data; hid_hotplug_callback_fn callback; + /* Snapshot of the matching devices connected at registration time, + to be delivered ("replayed") as synthetic HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED + events on the event thread (HID_API_HOTPLUG_ENUMERATE); NULL once delivered */ + struct hid_device_info *replay; + /* Pointer to the next notification */ struct hid_hotplug_callback *next; }; @@ -487,40 +519,146 @@ struct hid_device_info_ex io_service_t service; }; +/* --- Hotplug locking: the one global lock order --- + + Two locks are involved in the hotplug machinery: + + (1) hid_hotplug_context.mutex - recursive; guards the shared hotplug + context, with the exceptions below: the callback list, the device cache + and lifecycle flags (thread_state, thread_needs_join, join_in_progress, + exiting, ...) as + well as the CoreFoundation references of the event thread. It is held + for the whole duration of every callback invocation, and it is + re-entrant so that a callback may call hid_hotplug_register_callback() + or hid_hotplug_deregister_callback() from the event thread itself - + which the API documentation guarantees cannot deadlock. + + (2) global_error_mutex - a leaf lock; guards the global error string. + Nothing is ever acquired while it is held. + + The startup barrier's internal lock (inside pthread_barrier_wait()) is a leaf + as well. + + GLOBAL LOCK ORDER: + hid_hotplug_context.mutex -> { global_error_mutex, startup_barrier } + + The hotplug mutex is always the OUTERMOST lock; no code holding a leaf lock + ever tries to acquire it, so no cycle can exist. In particular there is + deliberately NO bootstrap/startup mutex: a lock ordered *outside* the hotplug + mutex is fundamentally incompatible with registering from inside a callback + (which is entered with the hotplug mutex already held), so the one-time + initialization uses pthread_once(), and the hid_exit() teardown is guarded + from *inside* the hotplug mutex by the `exiting` flag. + + pthread_join() is only ever called with the hotplug mutex released, and + pthread_cond_wait() only with exactly one recursion level held (see + hid_internal_hotplug_collect_thread()). + + The thread-local event-thread marker is only a fast-path hint. The OS thread + ID is published atomically, so identity checks never need the hotplug mutex. + startup_phase is owned by the event thread; startup_ok/startup_error are + published through the startup barrier. + Before that barrier, the registering thread holds the mutex and is parked + there, so the event thread has exclusive access to the context and + MUST NOT take the mutex there (that would deadlock against the parked + registrant). The barrier is the release/acquire edge that publishes what the + thread has set up. Its CoreFoundation objects also remain event-thread-owned + until the epilogue; see the manager release at the end of hotplug_thread(). */ + static struct hid_hotplug_context { /* MacOS specific notification handles */ IOHIDManagerRef manager; /* Thread and RunLoop for the manager to work in */ pthread_t thread; + uint64_t thread_id; /* Atomic OS thread ID; retained until a successful join */ CFRunLoopRef run_loop; CFRunLoopSourceRef source; + CFRunLoopSourceRef replay_source; /* Delivers the initial HID_API_HOTPLUG_ENUMERATE pass of new registrations */ CFStringRef run_loop_mode; pthread_barrier_t startup_barrier; /* Ensures correct startup sequence */ - int thread_state; /* 0 = starting (events ignored), 1 = running (events processed), 2 = shutting down */ - + + /* Lifecycle of the event thread: 0 = starting, 1 = running, 2 = stopping or + stopped. Only ever read and written under the mutex - the event thread + itself never writes it before the startup barrier (the registering thread + publishes the startup result, see startup_ok) */ + int thread_state; + /* HIDAPI unique callback handle counter */ hid_hotplug_callback_handle next_handle; pthread_mutex_t mutex; + pthread_cond_t join_done; /* Broadcast once the stopped event thread has been collected */ /* Boolean flags */ - unsigned char mutex_ready; + unsigned char mutex_ready; /* The mutex and the condition variable are usable (written once, under pthread_once) */ unsigned char mutex_in_use; unsigned char cb_list_dirty; + unsigned char thread_needs_join; /* Event thread was started and has not been joined yet, even after its epilogue */ + unsigned char join_in_progress; /* A thread is currently joining the event thread (with the mutex released) */ + unsigned char exiting; /* hid_exit() is tearing the hotplug machinery down */ + + /* Set while the event thread has not passed its startup barrier yet. + Read and written ONLY on the event thread (the registering thread sets it + before pthread_create(), which is a synchronization point), so it needs no + lock - and it must not: during that phase the mutex is held by the parked + registrant */ + unsigned char startup_phase; + + /* Written by the event thread before the startup barrier, read by the + registering thread after it (the barrier is the synchronization edge) */ + unsigned char startup_ok; + const char *startup_error; /* Static reason string, published with startup_ok */ /* Linked list of the hotplug callbacks */ struct hid_hotplug_callback *hotplug_cbs; /* Linked list of the device infos (mandatory when the device is disconnected) */ struct hid_device_info *devs; -} hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ +} hid_hotplug_context; /* zero-initialized (static storage) */ + +/* The hotplug mutex and condition variable are created exactly once and are + never destroyed: they live for the lifetime of the process, so that no thread + can ever lock a mutex that hid_exit() destroyed underneath it */ +static pthread_once_t hid_hotplug_init_once = PTHREAD_ONCE_INIT; + +/* Fast-path hint. Darwin may clear this TLS before application thread-specific + destructors, so a zero marker requires checking the published OS thread ID. */ +static __thread unsigned char hid_hotplug_event_thread; + +/* HIDAPI's public API contract (see hidapi.h) is that HIDAPI calls made from + within a hotplug callback do not update the global error string: the callback + runs on this internal event thread, and an application cannot serialize a + hid_error(NULL) read against a write from that thread - that would be a + use-after-free of last_global_error_str. A callback may re-enter the public + hid_hotplug_register_callback()/hid_hotplug_deregister_callback(), whose + failure paths report global errors; those writes are suppressed via this + check in register_global_error()[_format](). + Returns non-zero when the caller is the hotplug event thread, including during + its thread-specific destructors. The fallback reads the OS thread ID with + acquire/release atomics, independently of the hotplug mutex: an application + may hold its own serialization mutex while a callback waits for it. The ID + remains published until a successful join, and does not depend on pthread_t + storage that the join may already have freed. Compiler atomics also support + building this file as C++. */ +static int hid_internal_on_event_thread(void) +{ + uint64_t event_thread_id, thread_id; + + if (hid_hotplug_event_thread) + return 1; + + event_thread_id = __atomic_load_n(&hid_hotplug_context.thread_id, __ATOMIC_ACQUIRE); + return event_thread_id != 0 && pthread_threadid_np(NULL, &thread_id) == 0 + && thread_id == event_thread_id; +} static void hid_internal_hotplug_remove_postponed(void) { /* Unregister the callbacks whose removal was postponed */ /* This function is always called inside a locked mutex */ /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ + /* mutex_ready is defensive: callers must hold the initialized hotplug mutex. */ if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { return; } @@ -531,6 +669,7 @@ static void hid_internal_hotplug_remove_postponed(void) struct hid_hotplug_callback *callback = *current; if (!callback->events) { *current = (*current)->next; + hid_free_enumeration(callback->replay); free(callback); continue; } @@ -541,8 +680,87 @@ static void hid_internal_hotplug_remove_postponed(void) hid_hotplug_context.cb_list_dirty = 0; } +/* Releases the startup barrier and run loop references in the event thread's + epilogue, with the hotplug mutex held. Join ownership remains with the + collector until pthread_join() succeeds, including thread-specific destructors. + Both participants have left the startup barrier by then: the registering + thread holds the mutex across the barrier and releases it only afterwards, so + acquiring the mutex proves it is out. */ +static void hid_internal_hotplug_release_thread(void) +{ + pthread_barrier_destroy(&hid_hotplug_context.startup_barrier); + + /* Only the epilogue releases these references, after their last event-thread + use. Cleanup reads them under this same mutex; NULL records their release, + not completion of the still-joinable thread. */ + if (hid_hotplug_context.source) { + CFRelease(hid_hotplug_context.source); + hid_hotplug_context.source = NULL; + } + if (hid_hotplug_context.replay_source) { + CFRelease(hid_hotplug_context.replay_source); + hid_hotplug_context.replay_source = NULL; + } + hid_hotplug_context.run_loop = NULL; +} + +/* Collects (joins) the event thread once it has been told to stop, and clears + its join ownership. Serializes concurrent joiners and waits + out a join running on another thread. + Must be called with the hotplug mutex NOT held by the calling thread, except + from the event thread itself, where it is a guaranteed no-op (the + hid_internal_on_event_thread() check below) - that keeps pthread_cond_wait() from + ever being reached with the recursive mutex locked more than once. + Returns the pthread_join() error, if any, without releasing the thread record. */ +static int hid_internal_hotplug_collect_thread(void) +{ + int result = 0; + + pthread_mutex_lock(&hid_hotplug_context.mutex); + + while (hid_hotplug_context.thread_needs_join + && hid_hotplug_context.hotplug_cbs == NULL + && hid_hotplug_context.thread_state == 2 + && !hid_internal_on_event_thread()) { + if (hid_hotplug_context.join_in_progress) { + /* Another thread is already joining: wait for it to finish. + A condition variable (and not a spin) is essential: the joiner is + blocked in pthread_join() waiting for the event thread, which may + still need this very mutex to finish an in-flight dispatch. */ + pthread_cond_wait(&hid_hotplug_context.join_done, &hid_hotplug_context.mutex); + continue; + } + + hid_hotplug_context.join_in_progress = 1; + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + /* Join with the mutex released: the exiting thread may still need the + mutex to finish an in-flight callback dispatch (issue #794 and the + matching cross-thread deadlock). No new event thread can be started + while thread_needs_join is set, so the thread handle is stable. */ + result = pthread_join(hid_hotplug_context.thread, NULL); + + pthread_mutex_lock(&hid_hotplug_context.mutex); + hid_hotplug_context.join_in_progress = 0; + if (result == 0) { + __atomic_store_n(&hid_hotplug_context.thread_id, 0, __ATOMIC_RELEASE); + hid_hotplug_context.thread_needs_join = 0; + } + + /* Wake the threads waiting for this join to complete */ + pthread_cond_broadcast(&hid_hotplug_context.join_done); + if (result != 0) + break; + } + + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return result; +} + +/* Must be called with the hotplug mutex held */ static void hid_internal_hotplug_cleanup(void) { + /* mutex_ready is defensive: callers must hold the initialized hotplug mutex. */ if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { return; } @@ -558,55 +776,145 @@ static void hid_internal_hotplug_cleanup(void) hid_free_enumeration(hid_hotplug_context.devs); hid_hotplug_context.devs = NULL; - /* Cause hotplug_thread() to stop. */ - hid_hotplug_context.thread_state = 2; + if (!hid_hotplug_context.thread_needs_join) { + /* The event thread is not running */ + return; + } - /* Wake up the run thread's event loop so that the thread can exit. */ - CFRunLoopSourceSignal(hid_hotplug_context.source); - CFRunLoopWakeUp(hid_hotplug_context.run_loop); + if (hid_hotplug_context.thread_state != 2) { + /* Cause hotplug_thread() to stop. */ + hid_hotplug_context.thread_state = 2; - /* Wait for read_thread() to end. */ - pthread_join(hid_hotplug_context.thread, NULL); + /* Wake up the run thread's event loop so that the thread can exit. + Both references are still alive: the epilogue releases them under + this mutex, after thread_state has reached 2. */ + if (hid_hotplug_context.source != NULL && hid_hotplug_context.run_loop != NULL) { + CFRunLoopSourceSignal(hid_hotplug_context.source); + CFRunLoopWakeUp(hid_hotplug_context.run_loop); + } + } + + /* The join is never performed here: this function runs with the mutex + held, and the exiting thread may still need the mutex to finish an + in-flight dispatch (it may even be the current thread - issue #794). + The stopped thread is collected by hid_internal_hotplug_collect_thread() + from the public entry points, with the mutex released. */ } -static void hid_internal_hotplug_init(void) +/* The one-time hotplug initialization, run by pthread_once(). On failure + mutex_ready is left at 0 and the hotplug API stays unavailable. */ +static void hid_internal_hotplug_init_once(void) { - if (!hid_hotplug_context.mutex_ready) { - /* Initialize the mutex as recursive */ - pthread_mutexattr_t attr; - pthread_mutexattr_init(&attr); - pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); - pthread_mutex_init(&hid_hotplug_context.mutex, &attr); + pthread_mutexattr_t attr; + + if (pthread_mutexattr_init(&attr) != 0) { + return; + } + + /* The mutex must be recursive: a callback runs with it held and is allowed + to call hid_hotplug_register_callback()/hid_hotplug_deregister_callback() */ + if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) != 0) { pthread_mutexattr_destroy(&attr); + return; + } - /* Set state to Ready */ - hid_hotplug_context.mutex_ready = 1; - hid_hotplug_context.mutex_in_use = 0; - hid_hotplug_context.cb_list_dirty = 0; - if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) - hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + if (pthread_mutex_init(&hid_hotplug_context.mutex, &attr) != 0) { + pthread_mutexattr_destroy(&attr); + return; } + + pthread_mutexattr_destroy(&attr); + + if (pthread_cond_init(&hid_hotplug_context.join_done, NULL) != 0) { + pthread_mutex_destroy(&hid_hotplug_context.mutex); + return; + } + + hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + + /* Publish the mutex as usable, last */ + hid_hotplug_context.mutex_ready = 1; } -static void hid_internal_hotplug_exit(void) +/* Ensures the hotplug mutex is created. Returns 0 when the hotplug machinery is + usable, -1 when it could not be initialized (the caller must then fail with a + retrievable error - locking an uninitialized mutex is undefined behavior). + pthread_once() provides both the one-time guarantee and the memory + synchronization for the read of mutex_ready below. */ +static int hid_internal_hotplug_init(void) { - if (!hid_hotplug_context.mutex_ready) { - return; + pthread_once(&hid_hotplug_init_once, hid_internal_hotplug_init_once); + + return hid_hotplug_context.mutex_ready ? 0 : -1; +} + +/* Tears the hotplug machinery down (from hid_exit()). Leaves `exiting` set, so + that a concurrent hid_hotplug_register_callback()/hid_hotplug_deregister_callback() + fails instead of racing the rest of hid_exit(); hid_internal_hotplug_exit_done() + clears it once hid_exit() is finished. */ +static int hid_internal_hotplug_exit(void) +{ + struct hid_hotplug_callback **current; + + if (hid_internal_hotplug_init() != 0) { + /* The hotplug mutex could not be created: nothing can ever have been + registered, and there is nothing to tear down */ + return 0; } pthread_mutex_lock(&hid_hotplug_context.mutex); - struct hid_hotplug_callback** current = &hid_hotplug_context.hotplug_cbs; + + /* Close the hotplug API for the duration of the teardown */ + hid_hotplug_context.exiting = 1; /* Remove all callbacks from the list */ + current = &hid_hotplug_context.hotplug_cbs; while (*current) { struct hid_hotplug_callback* next = (*current)->next; + hid_free_enumeration((*current)->replay); free(*current); *current = next; } hid_internal_hotplug_cleanup(); pthread_mutex_unlock(&hid_hotplug_context.mutex); - hid_hotplug_context.mutex_ready = 0; - pthread_mutex_destroy(&hid_hotplug_context.mutex); + + /* Join the stopped event thread, with the hotplug mutex released. + A failed join is a non-recoverable internal error: `exiting` stays set + (hotplug registration keeps failing with "hid_exit() is in progress") + and hid_exit() leaves hid_mgr alive, because the thread record must be + retained until pthread_join() succeeds (see hid_internal_hotplug_collect_thread()) + and the unjoined thread may still use the run loop resources. It is + practically unreachable: the thread is joinable, never detached, and + the identity check keeps the event thread from joining itself. */ + if (hid_internal_hotplug_collect_thread() != 0) + return -1; + + /* The hotplug mutex is deliberately NOT destroyed: another thread may be + about to lock it (it only has to observe `exiting` afterwards), and + destroying a mutex under it would be undefined behavior. It costs nothing + to keep it for the lifetime of the process. */ + return 0; +} + +/* Re-opens the hotplug API after hid_exit() has finished. */ +static void hid_internal_hotplug_exit_done(void) +{ + if (hid_internal_hotplug_init() != 0) { + return; + } + + pthread_mutex_lock(&hid_hotplug_context.mutex); + + /* The event thread has been joined, including its thread-specific + destructors; releasing its resources in the epilogue alone is not enough. */ + if (hid_hotplug_context.run_loop_mode) { + CFRelease(hid_hotplug_context.run_loop_mode); + hid_hotplug_context.run_loop_mode = NULL; + } + + hid_hotplug_context.exiting = 0; + + pthread_mutex_unlock(&hid_hotplug_context.mutex); } /* Initialize the IOHIDManager if necessary. This is the public function, and @@ -628,17 +936,28 @@ int HID_API_EXPORT hid_init(void) int HID_API_EXPORT hid_exit(void) { + /* Hotplug synchronization state exists independently of hid_mgr; teardown + is safe even when nothing was initialized. Set `exiting` under the hotplug + mutex before destroying hid_mgr, so registration cannot re-enter hid_init() + during the teardown below. */ + if (hid_internal_hotplug_exit() != 0) { + register_global_error("hid_exit: failed to join the hotplug events thread"); + return -1; + } + if (hid_mgr) { /* Close the HID manager. */ IOHIDManagerClose(hid_mgr, kIOHIDOptionsTypeNone); CFRelease(hid_mgr); hid_mgr = NULL; - hid_internal_hotplug_exit(); } /* Free global error message */ register_global_error(NULL); + /* Re-open the hotplug API: the library may be initialized again */ + hid_internal_hotplug_exit_done(); + return 0; } @@ -746,9 +1065,11 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, 9+1+20+1=31 bytes buffer, but allocate 32 for simple alignment */ const size_t path_len = 32; cur_dev->path = (char *) calloc(1, path_len); - if (cur_dev->path != NULL) { - snprintf(cur_dev->path, path_len, "DevSrvsID:%llu", entry_id); + if (cur_dev->path == NULL) { + hid_free_enumeration(cur_dev); + return NULL; } + snprintf(cur_dev->path, path_len, "DevSrvsID:%llu", entry_id); } if (cur_dev->path == NULL) { @@ -766,6 +1087,12 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, get_product_string(dev, buf, BufLen); cur_dev->product_string = dup_wcs(buf); + if (!cur_dev->path || !cur_dev->serial_number + || !cur_dev->manufacturer_string || !cur_dev->product_string) { + hid_free_enumeration(cur_dev); + return NULL; + } + /* VID/PID */ cur_dev->vendor_id = dev_vid; cur_dev->product_id = dev_pid; @@ -812,7 +1139,8 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, return cur_dev; } -static struct hid_device_info *create_device_info(IOHIDDeviceRef device) +/* Hotplug requires every usage entry; ordinary enumeration is best effort. */ +static struct hid_device_info *create_device_info(IOHIDDeviceRef device, int strict) { const int32_t primary_usage_page = get_int_property(device, CFSTR(kIOHIDPrimaryUsagePageKey)); const int32_t primary_usage = get_int_property(device, CFSTR(kIOHIDPrimaryUsageKey)); @@ -849,6 +1177,10 @@ static struct hid_device_info *create_device_info(IOHIDDeviceRef device) continue; /* Already added. */ next = create_device_info_with_usage(device, usage_page, usage); + if (next == NULL && strict) { + hid_free_enumeration(root); + return NULL; + } cur->next = next; if (next != NULL) { cur = next; @@ -918,7 +1250,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, continue; } - struct hid_device_info *tmp = create_device_info(dev); + struct hid_device_info *tmp = create_device_info(dev, 0); if (tmp == NULL) { continue; } @@ -967,110 +1299,309 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } +/* Makes a deep copy of a single hid_device_info entry (the next pointer of + the copy is always NULL). Returns NULL on allocation failure. + Every field is copied by hand: this function must be updated whenever + struct hid_device_info gains a new field. + Note the allocation asymmetry with the hotplug device cache: the entries of + the cache are allocated as struct hid_device_info_ex (they carry the + io_service_t used to recognize a device on removal), while a copy made here + is a plain struct hid_device_info. A copy must therefore never be passed to + match_ref_to_info() or added to the device cache - it is only ever handed to + a hotplug callback, and freed with hid_free_enumeration() like any other + hid_device_info. */ +static struct hid_device_info *hid_internal_copy_device_info(const struct hid_device_info *src) +{ + struct hid_device_info *dst = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (dst == NULL) { + return NULL; + } + + dst->path = src->path ? strdup(src->path) : NULL; + dst->vendor_id = src->vendor_id; + dst->product_id = src->product_id; + dst->serial_number = src->serial_number ? dup_wcs(src->serial_number) : NULL; + dst->release_number = src->release_number; + dst->manufacturer_string = src->manufacturer_string ? dup_wcs(src->manufacturer_string) : NULL; + dst->product_string = src->product_string ? dup_wcs(src->product_string) : NULL; + dst->usage_page = src->usage_page; + dst->usage = src->usage; + dst->interface_number = src->interface_number; + dst->bus_type = src->bus_type; + dst->next = NULL; + + /* Treat a failed string copy as a failed allocation */ + if ((src->path && !dst->path) + || (src->serial_number && !dst->serial_number) + || (src->manufacturer_string && !dst->manufacturer_string) + || (src->product_string && !dst->product_string)) { + hid_free_enumeration(dst); + return NULL; + } + + return dst; +} + +/* Delivers the pending synthetic HID_API_HOTPLUG_ENUMERATE events (the initial + pass) of a single callback. Called on the event thread, with the mutex held + and mutex_in_use set. */ +static void hid_internal_hotplug_replay_one(struct hid_hotplug_callback *callback) +{ + while (callback->replay != NULL) { + struct hid_device_info *info = callback->replay; + callback->replay = info->next; + info->next = NULL; + + /* Skip the delivery if the callback got deregistered meanwhile */ + if (callback->events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED) { + int result = (*callback->callback)(callback->handle, info, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, callback->user_data); + if (result) { + /* The callback asked to be deregistered: mark it for removal + and drop the rest of its initial pass */ + callback->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + hid_free_enumeration(callback->replay); + callback->replay = NULL; + } + } + + hid_free_enumeration(info); + } +} + +/* Dispatches one event to every matching callback. Called on the event thread + only, and only once it has passed its startup barrier. */ static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event) { + /* Defensive: during the startup phase the callback list is provably empty + (the first registration inserts its callback only after the barrier) and + the hotplug mutex is held by the registering thread parked at that + barrier - locking it here would deadlock it and this thread forever */ + if (hid_hotplug_context.startup_phase) { + return; + } + pthread_mutex_lock(&hid_hotplug_context.mutex); + + unsigned char old_state = hid_hotplug_context.mutex_in_use; hid_hotplug_context.mutex_in_use = 1; + /* Freeze the dispatch at the last callback registered at this moment: + a callback registered from within a callback must not receive the + in-flight event - its HID_API_HOTPLUG_ENUMERATE snapshot (taken at + registration) and the subsequent events cover it with no losses or + duplicates. While mutex_in_use is set, the list grows only at the tail, + except that a failed registration can unlink its own new tail before any + traversal reaches it (see the snapshot rollback in + hid_hotplug_register_callback()). */ + struct hid_hotplug_callback *stop_after = hid_hotplug_context.hotplug_cbs; + while (stop_after != NULL && stop_after->next != NULL) { + stop_after = stop_after->next; + } + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; while (*current) { struct hid_hotplug_callback *callback = *current; + /* The initial HID_API_HOTPLUG_ENUMERATE pass (if still pending) is always + delivered before any live events for the callback: the replay source + might not have fired yet - flush it first */ + if (callback->replay != NULL) { + hid_internal_hotplug_replay_one(callback); + } if ((callback->events & event) && hid_internal_match_device_id(info->vendor_id, info->product_id, callback->vendor_id, callback->product_id)) { int result = callback->callback(callback->handle, info, event, callback->user_data); - /* If the result is non-zero, we mark the callback for removal and proceed */ - /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + /* Traversal is active, so mark for postponed removal inline. Public + deregistration would do the same, plus redundant lookup and bookkeeping. */ if (result) { - (*current)->events = 0; + callback->events = 0; hid_hotplug_context.cb_list_dirty = 1; - continue; } } + if (callback == stop_after) { + break; + } current = &callback->next; } - - hid_hotplug_context.mutex_in_use = 0; + + hid_hotplug_context.mutex_in_use = old_state; hid_internal_hotplug_remove_postponed(); pthread_mutex_unlock(&hid_hotplug_context.mutex); } +/* Matches an IOHIDDeviceRef against an entry of the hotplug device cache. + The entries of the cache are allocated as struct hid_device_info_ex and carry + the io_service_t of the device: the path cannot be regenerated once the device + is gone. Never pass an entry that did not come from the cache (see + hid_internal_copy_device_info()). Comparison uses task-local io_service_t + port names, not registry-entry IDs. The manager-owned IOHIDDevice must retain + its service right until removal or teardown evicts every corresponding cache + entry, preventing port-name reuse while entries are live. */ +static int match_ref_to_info(IOHIDDeviceRef device, struct hid_device_info *info) +{ + if (!device || !info) { + return 0; + } + + struct hid_device_info_ex* ex = (struct hid_device_info_ex*)info; + io_service_t service = IOHIDDeviceGetService(device); + + /* MACH_PORT_NULL is not a valid identity: two devices that both lack a + service must not be treated as the same device (that would make the + arrival dedupe suppress the second one, and a removal evict the wrong + cache entry). */ + return (service != MACH_PORT_NULL && service == ex->service); +} + +/* Returns non-zero when the device is already in the hotplug device cache. + Called on the event thread, with the mutex held (or during its startup phase, + where the thread has exclusive access to the context). */ +static int hid_internal_hotplug_is_known_device(IOHIDDeviceRef device) +{ + struct hid_device_info *info; + + for (info = hid_hotplug_context.devs; info != NULL; info = info->next) { + if (match_ref_to_info(device, info)) { + return 1; + } + } + + return 0; +} + static void hid_internal_hotplug_connect_callback(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) { + struct hid_device_info *info; + + /* The event thread does not lock the mutex and does not dispatch anything + before it has passed the startup barrier (the whole initial enumeration is + such a window): the mutex is held by the registering thread parked at that + barrier - locking it here would deadlock - and the callback list is + provably empty then, so there is nothing to dispatch to. The device still + goes into the cache: that is what the initial HID_API_HOTPLUG_ENUMERATE + snapshot is taken from. */ + const int startup = hid_hotplug_context.startup_phase; + (void) context; (void) result; (void) sender; - struct hid_device_info* info = create_device_info(device); - if (!info) { + /* A device without a backing io_service_t carries no usable identity (see + match_ref_to_info()): once cached it would match neither the arrival + dedupe nor its own removal, so it would be reported more than once and + never evicted. Keep it consistently invisible instead. */ + if (!device || IOHIDDeviceGetService(device) == MACH_PORT_NULL) { return; } - struct hid_device_info* info_cur = info; - - /* NOTE: we don't call any callbacks and we don't lock the mutex during initialization: the mutex is held by the main thread, but it's waiting by a barrier */ - if (hid_hotplug_context.thread_state > 0) - { + + if (!startup) { /* Lock the mutex to avoid race conditions */ pthread_mutex_lock(&hid_hotplug_context.mutex); - - /* Invoke all callbacks */ - while (info_cur) - { - hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); - info_cur = info_cur->next; - } } - /* Append all we got to the end of the device list */ - if (info) { - if (hid_hotplug_context.devs != NULL) { - struct hid_device_info* last = hid_hotplug_context.devs; - while (last->next != NULL) { - last = last->next; - } - last->next = info; + /* Once the run loop runs, the IOHIDManager reports devices that were already + connected when matching was set up. Those devices are all in the cache - + it is completed synchronously during the thread's startup, before any + callback can be registered - so they are NOT new arrivals and must never be + dispatched as live events. This is what makes the snapshot boundary + deterministic instead of dependent on how long the initial matching burst + takes to be delivered. */ + if (hid_internal_hotplug_is_known_device(device)) { + if (!startup) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); } - else { - hid_hotplug_context.devs = info; + return; + } + + info = create_device_info(device, 1); + if (!info) { + /* Out of memory on the live-arrival path: the device ends up neither in + the cache nor in an event, so "reported exactly once" is best effort + here. The registration-time paths are hardened against this - + hid_internal_hotplug_build_device_cache() fails the startup and + hid_hotplug_register_callback() fails the registration rather than + commit a partial initial pass - because both still have a caller to + report the failure to. An IOKit callback has none, and the + IOHIDManager does not re-report the device, so there is nothing left + to fail or retry against. (During the startup phase the device is + still picked up by hid_internal_hotplug_build_device_cache(), which + does fail loudly if it cannot allocate either.) */ + /* An uncached live device is also absent from later ENUMERATE snapshots + and produces no LEFT callback until a successful arrival caches it, + normally after replug or monitor restart. */ + if (!startup) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); } + return; } - if (hid_hotplug_context.thread_state > 0) - { - pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* Append all we got to the end of the device list BEFORE invoking any + callbacks: a callback registering with HID_API_HOTPLUG_ENUMERATE from + within a callback must find the arriving entries in its snapshot, + as it does not receive the in-flight events */ + if (hid_hotplug_context.devs != NULL) { + struct hid_device_info* last = hid_hotplug_context.devs; + while (last->next != NULL) { + last = last->next; + } + last->next = info; + } + else { + hid_hotplug_context.devs = info; } -} -static int match_ref_to_info(IOHIDDeviceRef device, struct hid_device_info *info) -{ - if (!device || !info) { - return 0; + if (!startup) { + /* Invoke the callbacks for each entry; device->next must be NULL for + every delivery, so each entry is temporarily severed, truncating the + device cache at that entry for the duration of its dispatch (a + snapshot taken during the dispatch then ends at the delivered entry) */ + struct hid_device_info *info_cur = info; + while (info_cur) + { + struct hid_device_info *info_next = info_cur->next; + info_cur->next = NULL; + hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); + info_cur->next = info_next; + info_cur = info_next; + } + + /* Clean up if the last callback was removed during the events */ + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); } - - struct hid_device_info_ex* ex = (struct hid_device_info_ex*)info; - io_service_t service = IOHIDDeviceGetService(device); - - return (service == ex->service); } static void hid_internal_hotplug_disconnect_callback(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) { + struct hid_device_info **current; + + /* Same guard as in the connect callback - and it covers the dispatch, not + just the lock: a device removed while the event thread is still starting + up (the whole initial enumeration is such a window) must not lock the + mutex held by the registrant parked at the startup barrier, nor dispatch + anything. There is no callback to notify at that point either: dropping + the device from the cache is all that is needed, and the registration's + HID_API_HOTPLUG_ENUMERATE snapshot - copied from the cache only after this + thread reaches the barrier - then simply does not contain it. */ + const int startup = hid_hotplug_context.startup_phase; + (void) context; (void) result; (void) sender; - /* NOTE: we don't call any callbacks and we don't lock the mutex during initialization: the mutex is held by the main thread, but it's waiting by a barrier*/ - if (hid_hotplug_context.thread_state > 0) - { + if (!startup) { pthread_mutex_lock(&hid_hotplug_context.mutex); } - for (struct hid_device_info **current = &hid_hotplug_context.devs; *current;) { + for (current = &hid_hotplug_context.devs; *current;) { struct hid_device_info* info = *current; - if (match_ref_to_info(device, *current)) { + if (match_ref_to_info(device, info)) { /* If the IOHIDDeviceRef device that's left matches this HID device, we detach it from the list */ - *current = (*current)->next; + *current = info->next; info->next = NULL; - hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); + if (!startup) { + hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); + } /* Free every removed device */ hid_free_enumeration(info); } else { @@ -1078,8 +1609,7 @@ static void hid_internal_hotplug_disconnect_callback(void *context, IOReturn res } } - if (hid_hotplug_context.thread_state > 0) - { + if (!startup) { /* Clean up if the last callback was removed */ hid_internal_hotplug_cleanup(); pthread_mutex_unlock(&hid_hotplug_context.mutex); @@ -1092,72 +1622,336 @@ static void hotplug_stop_callback(void* context) CFRunLoopStop(hid_hotplug_context.run_loop); } +static void hotplug_replay_callback(void* context) +{ + (void) context; + + /* Defensive, same as in hid_internal_invoke_callbacks(): the replay source is + only ever signalled by hid_hotplug_register_callback() under the mutex and + after the startup barrier, so it cannot be performed by the startup drain - + where taking the mutex would deadlock against the parked registrant */ + if (hid_hotplug_context.startup_phase) { + return; + } + + pthread_mutex_lock(&hid_hotplug_context.mutex); + + unsigned char old_state = hid_hotplug_context.mutex_in_use; + hid_hotplug_context.mutex_in_use = 1; + + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; callback != NULL; callback = callback->next) { + if (callback->replay != NULL) { + hid_internal_hotplug_replay_one(callback); + } + } + + hid_hotplug_context.mutex_in_use = old_state; + + /* An initial-pass callback may have deregistered the last callback: clean up if so */ + hid_internal_hotplug_cleanup(); + + pthread_mutex_unlock(&hid_hotplug_context.mutex); +} + +/* Gives the hotplug IOHIDManager a short best-effort pump of queued matching + events for connected devices, like the process_pending_events() call + hid_enumerate() makes before IOHIDManagerCopyDevices(). + + This is NOT the snapshot boundary - hid_internal_hotplug_build_device_cache() + below is - and nothing depends on it draining the burst completely. It may + add cache entries and remove devices disconnected meanwhile. It runs on the + event thread during startup, so the connect/disconnect callbacks it triggers + only maintain the cache and dispatch + nothing (no callback is registered yet, and the mutex must not be taken - see + the locking note at the top of the hotplug code). */ +static void hid_internal_hotplug_drain_pending_events(void) +{ + CFRunLoopRunInMode(hid_hotplug_context.run_loop_mode, 0.001, FALSE); +} + +/* Completes the initial device cache from the devices the hotplug IOHIDManager + matches right now. Runs on the event thread during its startup phase, i.e. + before any callback can be registered and without the mutex. + + This is the deterministic boundary between "was already connected" and + "arrived live": IOHIDManagerSetDeviceMatching() populates the manager's device + set, which IOHIDManagerCopyDevices() copies synchronously - as hid_enumerate() + relies on - so, unlike a timed pump of the run loop, the completeness of the + snapshot does not depend on how long the initial + matching burst takes. Every device connected at this point ends up in the + cache; when the run loop later delivers the matching events for those same + devices, they are recognized as already known and dropped (see + hid_internal_hotplug_connect_callback()), so they can never surface as live + arrivals. + + Devices already added to the cache (by the drain above) are kept: the two + sources are merged by io_service_t. + + Returns 0 on success, -1 on failure. */ +static int hid_internal_hotplug_build_device_cache(void) +{ + CFSetRef device_set; + CFIndex num_devices; + CFIndex i; + IOHIDDeviceRef *device_array; + struct hid_device_info *tail = hid_hotplug_context.devs; + + while (tail != NULL && tail->next != NULL) { + tail = tail->next; + } + + device_set = IOHIDManagerCopyDevices(hid_hotplug_context.manager); + if (device_set == NULL) { + /* No device is currently matched: an empty cache is a valid snapshot */ + return 0; + } + + num_devices = CFSetGetCount(device_set); + if (num_devices <= 0) { + CFRelease(device_set); + return 0; + } + + device_array = (IOHIDDeviceRef*) calloc((size_t) num_devices, sizeof(IOHIDDeviceRef)); + if (device_array == NULL) { + CFRelease(device_set); + return -1; + } + CFSetGetValues(device_set, (const void **) device_array); + + for (i = 0; i < num_devices; i++) { + struct hid_device_info *info; + + if (device_array[i] == NULL) { + continue; + } + + /* Same identity requirement as the live-arrival path: an entry with no + backing io_service_t could never be deduped against, nor evicted */ + if (IOHIDDeviceGetService(device_array[i]) == MACH_PORT_NULL) { + continue; + } + + /* Already in the cache (the drain got to it first) */ + if (hid_internal_hotplug_is_known_device(device_array[i])) { + continue; + } + + info = create_device_info(device_array[i], 1); + if (info == NULL) { + /* Out of memory: fail the startup rather than commit a snapshot + that is missing a connected device (it would later be reported as + a live arrival, which is exactly what the snapshot must prevent) */ + free(device_array); + CFRelease(device_set); + return -1; + } + + if (tail != NULL) { + tail->next = info; + } + else { + hid_hotplug_context.devs = info; + } + + /* A device contributes one entry per usage pair */ + while (info->next != NULL) { + info = info->next; + } + tail = info; + } + + free(device_array); + CFRelease(device_set); + + return 0; +} + +/* Releases the event thread's resources after their last use. Self-removal + leaves the thread joinable: the next application-thread collector or hid_exit() + must wait for actual termination before the thread record can be reused. */ +static void hid_internal_hotplug_thread_epilogue(void) +{ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + hid_internal_hotplug_release_thread(); + + /* The collector still owns the thread record until pthread_join() succeeds. */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); +} + static void* hotplug_thread(void* user_data) { + uint64_t thread_id = 0; (void) user_data; - hid_hotplug_context.thread_state = 0; - hid_hotplug_context.manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + /* Mark this event thread as the very first action, before anything + here can attempt a global-error write, so that any such write on this + internal event thread - notably from a user callback that re-enters + hid_hotplug_(de)register_callback() - is suppressed (see + hid_internal_on_event_thread()). Destructor re-entry can fall back to the + atomically published OS thread ID if Darwin has cleared the TLS marker. */ + hid_hotplug_event_thread = 1; + if (pthread_threadid_np(NULL, &thread_id) != 0 || thread_id == 0) { + /* Without a published OS thread id the destructor-phase identity + fallback would silently be disabled (see hid_internal_on_event_thread()), + so refuse to start rather than run with a weaker identity protocol. + Apple's libpthread never fails this call for the calling thread; the + registrant joins this thread on failure, and no callback exists yet + that could observe the missing id. */ + thread_id = 0; + hid_hotplug_context.startup_error = "failed to read the event thread id"; + } + __atomic_store_n(&hid_hotplug_context.thread_id, thread_id, __ATOMIC_RELEASE); + + /* Startup phase: the registering thread holds the hotplug mutex and is + parked at the startup barrier, so this thread has exclusive access to the + context - and it MUST NOT take the mutex until the barrier has been passed + (see the locking note at the top of the hotplug code). No hotplug callback + can be dispatched here either: none is registered yet (the first one is + inserted only after the barrier). */ + + /* The cache should already be empty after the previous generation's + hid_internal_hotplug_cleanup(); clear it defensively before startup. */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + + /* Store a reference to this runloop if we ever need to wake it up - e.g. if we have no callbacks left or hid_exit was called */ + hid_hotplug_context.run_loop = CFRunLoopGetCurrent(); if (!hid_hotplug_context.run_loop_mode) { const char *str = "HIDAPI_hotplug"; hid_hotplug_context.run_loop_mode = CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); } - /* Ensure the manager runs in this thread */ - IOHIDManagerScheduleWithRunLoop(hid_hotplug_context.manager, CFRunLoopGetCurrent(), hid_hotplug_context.run_loop_mode); - /* Store a reference to this runloop if we ever need to stop it - e.g. if we have no callbacks left or hid_exit was called */ - hid_hotplug_context.run_loop = CFRunLoopGetCurrent(); - - /* Create the RunLoopSource which is used to signal the - event loop to stop when hid_internal_hotplug_cleanup() is called. */ - CFRunLoopSourceContext ctx; - memset(&ctx, 0, sizeof(ctx)); - ctx.version = 0; - ctx.perform = &hotplug_stop_callback; - hid_hotplug_context.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0/*order*/, &ctx); - CFRunLoopAddSource(hid_hotplug_context.run_loop, hid_hotplug_context.source, hid_hotplug_context.run_loop_mode); - - /* Set the manager to receive events for ALL HID devices */ - IOHIDManagerSetDeviceMatching(hid_hotplug_context.manager, NULL); - - /* Install callbacks */ - IOHIDManagerRegisterDeviceMatchingCallback(hid_hotplug_context.manager, - hid_internal_hotplug_connect_callback, - NULL); - - IOHIDManagerRegisterDeviceRemovalCallback(hid_hotplug_context.manager, - hid_internal_hotplug_disconnect_callback, - NULL); - - /* After monitoring is all set up, enumerate all devices */ - /* Opening the manager should result in the internal callback being called for all connected devices */ - IOHIDManagerOpen(hid_hotplug_context.manager, kIOHIDOptionsTypeNone); - - /* TODO: We need to flush all events from the runloop to ensure the already connected devices don't send any unwanted events */ - process_pending_events(); + if (hid_hotplug_context.startup_error) { + /* Thread id publication failed above: skip the startup, the epilogue + below hands the reason to the registrant. */ + } + else if (hid_hotplug_context.run_loop_mode) { + hid_hotplug_context.manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (!hid_hotplug_context.manager) { + hid_hotplug_context.startup_error = "failed to create the HID manager"; + } + } + else { + hid_hotplug_context.startup_error = "failed to create the run loop mode"; + } + + if (hid_hotplug_context.manager) { + CFRunLoopSourceContext ctx; + + /* Ensure the manager runs in this thread */ + IOHIDManagerScheduleWithRunLoop(hid_hotplug_context.manager, hid_hotplug_context.run_loop, hid_hotplug_context.run_loop_mode); + + /* Create the RunLoopSource which is used to signal the + event loop to stop when hid_internal_hotplug_cleanup() is called. */ + memset(&ctx, 0, sizeof(ctx)); + ctx.version = 0; + ctx.perform = &hotplug_stop_callback; + hid_hotplug_context.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0/*order*/, &ctx); + + /* Create the RunLoopSource used to deliver the initial + HID_API_HOTPLUG_ENUMERATE pass of new registrations on this thread. */ + memset(&ctx, 0, sizeof(ctx)); + ctx.version = 0; + ctx.perform = &hotplug_replay_callback; + hid_hotplug_context.replay_source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0/*order*/, &ctx); + + if (hid_hotplug_context.source && hid_hotplug_context.replay_source) { + CFRunLoopAddSource(hid_hotplug_context.run_loop, hid_hotplug_context.source, hid_hotplug_context.run_loop_mode); + CFRunLoopAddSource(hid_hotplug_context.run_loop, hid_hotplug_context.replay_source, hid_hotplug_context.run_loop_mode); + + /* Set the manager to receive events for ALL HID devices */ + IOHIDManagerSetDeviceMatching(hid_hotplug_context.manager, NULL); + + /* Install callbacks. They only ever fire from this thread's run loop + (the manager is scheduled in a private run loop mode of this + thread), i.e. from the startup drain below or from the event loop + once the startup barrier is passed. */ + IOHIDManagerRegisterDeviceMatchingCallback(hid_hotplug_context.manager, + hid_internal_hotplug_connect_callback, + NULL); + + IOHIDManagerRegisterDeviceRemovalCallback(hid_hotplug_context.manager, + hid_internal_hotplug_disconnect_callback, + NULL); + + /* Give the manager a chance to process the matching events + (best effort; not a fence - see the function comment) ... */ + hid_internal_hotplug_drain_pending_events(); + + /* ... and then take the authoritative snapshot of the connected + devices synchronously: THIS - and not a timed pump of the run + loop - is the boundary between the initial + HID_API_HOTPLUG_ENUMERATE pass and the live events */ + if (hid_internal_hotplug_build_device_cache() == 0) { + hid_hotplug_context.startup_ok = 1; + } + else { + hid_hotplug_context.startup_error = "failed to allocate the device cache"; + } + } + else { + hid_hotplug_context.startup_error = !hid_hotplug_context.source + ? "failed to create the stop source" : "failed to create the replay source"; + } + } - /* Now that all events are flushed, we are ready to notify the main thread that we are ready */ - hid_hotplug_context.thread_state = 1; + /* Hand the startup result over to hid_hotplug_register_callback(), which is + waiting at the barrier and publishes it (thread_state) under the mutex. + The barrier also publishes everything this thread has set up so far. */ pthread_barrier_wait(&hid_hotplug_context.startup_barrier); - while (hid_hotplug_context.thread_state != 2) { - int code = CFRunLoopRunInMode(hid_hotplug_context.run_loop_mode, 1000/*sec*/, FALSE); - /* If runloop stopped for whatever reason, exit the thread */ - if (code != kCFRunLoopRunTimedOut && - code != kCFRunLoopRunHandledSource) { + /* The context is shared again from here on: the mutex is required */ + hid_hotplug_context.startup_phase = 0; + + if (hid_hotplug_context.startup_ok) { + for (;;) { + SInt32 code; + int stop; + + /* All of the lifecycle state is read under the mutex */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + stop = (hid_hotplug_context.thread_state == 2); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + if (stop) { + break; + } + + code = CFRunLoopRunInMode(hid_hotplug_context.run_loop_mode, 1000/*sec*/, FALSE); + + if (code == kCFRunLoopRunTimedOut) { + continue; + } + + /* Publish the stopped state under the mutex. Registration is rejected + after publication while callbacks remain. An unsolicited stop can + leave existing and just-accepted callbacks without delivery, + including pending replay: a registration may still have observed + state 1 between the run loop's return and this publication. */ + pthread_mutex_lock(&hid_hotplug_context.mutex); hid_hotplug_context.thread_state = 2; + pthread_mutex_unlock(&hid_hotplug_context.mutex); break; } } + /* else: the startup failed - hid_hotplug_register_callback() fails the + registration and joins this thread after its epilogue releases the run + loop sources (if any got created) and the startup barrier */ - /* Kill the manager */ - IOHIDManagerClose(hid_hotplug_context.manager, kIOHIDOptionsTypeNone); + /* Release the manager. No mutex is needed: nothing else ever touches it, + and no other thread may start a new event thread or release the run loop + mode before this thread has been joined, after its last use of them here. */ + if (hid_hotplug_context.manager) { + IOHIDManagerUnscheduleFromRunLoop(hid_hotplug_context.manager, hid_hotplug_context.run_loop, hid_hotplug_context.run_loop_mode); - IOHIDManagerUnscheduleFromRunLoop(hid_hotplug_context.manager, hid_hotplug_context.run_loop, hid_hotplug_context.run_loop_mode); + CFRelease(hid_hotplug_context.manager); + hid_hotplug_context.manager = NULL; + } - CFRelease(hid_hotplug_context.manager); - hid_hotplug_context.manager = NULL; + hid_internal_hotplug_thread_epilogue(); return NULL; } @@ -1166,48 +1960,130 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven { struct hid_hotplug_callback* hotplug_cb; + /* No events are ever delivered for a failed registration */ + if (callback_handle != NULL) { + *callback_handle = 0; + } + /* Check params */ if (events == 0 || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT)) || (flags & ~(HID_API_HOTPLUG_ENUMERATE)) || callback == NULL) { + register_global_error("hid_hotplug_register_callback: invalid arguments"); return -1; } - hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); + /* Create the hotplug mutex, exactly once. There is deliberately no + bootstrap lock around this: any lock ordered outside the hotplug mutex + would deadlock against a registration made from within a callback (which + already holds the hotplug mutex) - see the locking note above. */ + if (hid_internal_hotplug_init() != 0) { + register_global_error("hid_hotplug_register_callback: failed to initialize the hotplug mutex"); + return -1; + } + /* Lock the mutex to avoid race conditions */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + if (hid_hotplug_context.exiting) { + /* hid_exit() is tearing the machinery down: it invalidates every + callback handle, so there is nothing to register into */ + register_global_error("hid_hotplug_register_callback: hid_exit() is in progress"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + + /* The registration initializes the library implicitly (as if by hid_init()). + Done under the hotplug mutex, together with the `exiting` check above, so + that it cannot race hid_exit() destroying hid_mgr (hid_exit() keeps + `exiting` set across the whole of its teardown). + NOTE: hid_init() schedules the global IOHIDManager on the run loop of the + CURRENT thread. Implicit initialization makes the registering thread the + hid_init() owner: on macOS it must remain alive until all devices are closed + and hid_exit() has run, and hid_exit() must run on that same thread. + Applications registering from a transient worker must call hid_init() + first from their intended long-lived owner thread. */ + if (!hid_mgr && hid_init() != 0) { + /* register_global_error: global error is already set by hid_init */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + + hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); if (hotplug_cb == NULL) { + register_global_error("hid_hotplug_register_callback: failed to allocate a callback"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } /* Fill out the record */ hotplug_cb->next = NULL; + hotplug_cb->replay = NULL; hotplug_cb->vendor_id = vendor_id; hotplug_cb->product_id = product_id; hotplug_cb->events = events; hotplug_cb->user_data = user_data; hotplug_cb->callback = callback; - /* Ensure we are ready to actually use the mutex */ - hid_internal_hotplug_init(); + /* If a stopped event thread has not been collected (joined) yet, collect + it before the machinery can be restarted; the join must not happen with + the mutex held, so drop the mutex for the collection and re-check. + The list cannot be empty while a callback dispatch is in flight, but + thread-specific destructors may re-enter after the epilogue. */ + while (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.thread_needs_join) { + if (hid_internal_on_event_thread()) { + /* A destructor cannot join itself or start a replacement generation. */ + register_global_error("hid_hotplug_register_callback: the hotplug event thread is stopping"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + return -1; + } - /* Lock the mutex to avoid race conditions */ - pthread_mutex_lock(&hid_hotplug_context.mutex); + /* Make sure the stop was actually requested (idempotent) */ + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + if (hid_internal_hotplug_collect_thread() != 0) { + register_global_error("hid_hotplug_register_callback: failed to join the hotplug events thread"); + free(hotplug_cb); + return -1; + } + pthread_mutex_lock(&hid_hotplug_context.mutex); - hotplug_cb->handle = hid_hotplug_context.next_handle++; + /* hid_exit() may have started while the mutex was released */ + if (hid_hotplug_context.exiting) { + register_global_error("hid_hotplug_register_callback: hid_exit() is in progress"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + return -1; + } + } - /* handle the unlikely case of handle overflow */ - if (hid_hotplug_context.next_handle < 0) - { - hid_hotplug_context.next_handle = 1; + /* A stopped event thread while callbacks are still registered means the + thread stopped on its own (an unsolicited run loop exit): a solicited stop + is only ever requested once the callback list is empty. The machinery is + dead - it can deliver neither the initial pass nor any live event - so the + registration must fail rather than silently attach to it. + (With no callbacks left, the loop above has already collected the thread + and a fresh one is started below.) */ + if (hid_hotplug_context.hotplug_cbs != NULL && hid_hotplug_context.thread_state == 2) { + register_global_error("hid_hotplug_register_callback: the hotplug event thread has stopped"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + return -1; } - /* Return allocated handle */ - if (callback_handle != NULL) { - *callback_handle = hotplug_cb->handle; + /* Handles are not recycled even on overflow: recycling could collide with a live handle */ + if (hid_hotplug_context.next_handle == INT_MAX) { + register_global_error("hid_hotplug_register_callback: out of callback handles"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + return -1; } + hotplug_cb->handle = hid_hotplug_context.next_handle++; + /* Append a new callback to the end */ if (hid_hotplug_context.hotplug_cbs != NULL) { struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; @@ -1217,36 +2093,119 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven last->next = hotplug_cb; } else { - pthread_barrier_init(&hid_hotplug_context.startup_barrier, NULL, 2); - pthread_create(&hid_hotplug_context.thread, NULL, hotplug_thread, NULL); + if (pthread_barrier_init(&hid_hotplug_context.startup_barrier, NULL, 2) != 0) { + register_global_error("hid_hotplug_register_callback: failed to create the startup barrier"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + return -1; + } + + /* Set up the state the event thread starts from. The thread must not + touch the mutex before the startup barrier - this thread holds it and + parks at that barrier - so it never writes thread_state itself: it + reports its result in startup_ok, which is published here instead. */ + hid_hotplug_context.thread_state = 0; + hid_hotplug_context.startup_ok = 0; + hid_hotplug_context.startup_error = NULL; + hid_hotplug_context.startup_phase = 1; + + if (pthread_create(&hid_hotplug_context.thread, NULL, hotplug_thread, NULL) != 0) { + register_global_error("hid_hotplug_register_callback: failed to create the hotplug events thread"); + hid_hotplug_context.startup_phase = 0; + pthread_barrier_destroy(&hid_hotplug_context.startup_barrier); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + free(hotplug_cb); + return -1; + } - /* Wait for the thread to finish setting up - without it the callback may be registered too early*/ + hid_hotplug_context.thread_needs_join = 1; + /* Wait for the thread to finish setting up - without it the callback may be registered too early */ pthread_barrier_wait(&hid_hotplug_context.startup_barrier); + /* Publish the thread's startup result */ + hid_hotplug_context.thread_state = hid_hotplug_context.startup_ok ? 1 : 2; + + if (!hid_hotplug_context.startup_ok) { + /* The thread failed to set up the device monitoring and is exiting: + it must be collected (joined) with the mutex released */ + register_global_error_format("hid_hotplug_register_callback: %s", hid_hotplug_context.startup_error); + + /* Free whatever the thread may have cached before it failed + (the callback list is empty, so this also stops nothing and + re-signals nothing: thread_state is already 2) */ + hid_internal_hotplug_cleanup(); + + pthread_mutex_unlock(&hid_hotplug_context.mutex); + hid_internal_hotplug_collect_thread(); + free(hotplug_cb); + return -1; + } + /* Don't forget to actually register the callback */ hid_hotplug_context.hotplug_cbs = hotplug_cb; } - /* Mark the mutex as IN USE, to prevent callback removal from inside a callback */ - unsigned char old_state = hid_hotplug_context.mutex_in_use; - hid_hotplug_context.mutex_in_use = 1; - if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { - struct hid_device_info* device = hid_hotplug_context.devs; - /* Notify about already connected devices, if asked so */ - while (device != NULL) { - if (hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { - (*hotplug_cb->callback)(hotplug_cb->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, hotplug_cb->user_data); + int snapshot_ok = 1; + struct hid_device_info *dev_info = hid_hotplug_context.devs; + struct hid_device_info **replay_tail = &hotplug_cb->replay; + + /* Take a snapshot of the already connected matching devices: it is + delivered ("replayed") as synthetic arrival events on the event + thread, never from within this call */ + for (; dev_info != NULL; dev_info = dev_info->next) { + struct hid_device_info *dev_info_copy; + if (!hid_internal_match_device_id(dev_info->vendor_id, dev_info->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { + continue; + } + dev_info_copy = hid_internal_copy_device_info(dev_info); + if (dev_info_copy == NULL) { + snapshot_ok = 0; + break; } + *replay_tail = dev_info_copy; + replay_tail = &dev_info_copy->next; + } - device = device->next; + if (!snapshot_ok) { + /* Fail the registration rather than deliver a partial initial pass. + The mutex has been held since before the callback became visible, + so it has not been invoked yet: it is safe to detach and free it. */ + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current != NULL && *current != hotplug_cb) { + current = &(*current)->next; + } + if (*current != NULL) { + *current = hotplug_cb->next; + } + hid_free_enumeration(hotplug_cb->replay); + free(hotplug_cb); + + register_global_error("hid_hotplug_register_callback: failed to take a snapshot of the connected devices"); + + /* Stop the event thread if no other callbacks are left, + and collect it with the mutex released */ + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + hid_internal_hotplug_collect_thread(); + return -1; } - } - hid_hotplug_context.mutex_in_use = old_state; + if (hotplug_cb->replay != NULL && hid_hotplug_context.thread_state == 1) { + /* Ask the event thread to deliver the initial pass. The state check + is defensive: startup or the earlier running-state check established + state 1 while this mutex has remained held. */ + CFRunLoopSourceSignal(hid_hotplug_context.replay_source); + CFRunLoopWakeUp(hid_hotplug_context.run_loop); + } + } - hid_internal_hotplug_cleanup(); + /* Return the allocated handle: written before any events can be delivered, + as the events are only ever delivered under this mutex */ + if (callback_handle != NULL) { + *callback_handle = hotplug_cb->handle; + } pthread_mutex_unlock(&hid_hotplug_context.mutex); @@ -1255,40 +2214,77 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - if (!hid_hotplug_context.mutex_ready) { + int result = -1; + + if (callback_handle <= 0) { + register_global_error("hid_hotplug_deregister_callback: not a registered callback handle"); + return -1; + } + + /* The mutex is created here as well: this may be the first hotplug call */ + if (hid_internal_hotplug_init() != 0) { + register_global_error("hid_hotplug_deregister_callback: failed to initialize the hotplug mutex"); return -1; } pthread_mutex_lock(&hid_hotplug_context.mutex); - if (hid_hotplug_context.hotplug_cbs == NULL) { + if (hid_hotplug_context.exiting) { + /* hid_exit() is tearing the machinery down and invalidates every handle: + deregistering is a no-op */ + register_global_error("hid_hotplug_deregister_callback: hid_exit() is in progress"); pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } - int result = -1; - - /* Remove this notification */ - for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { - if ((*current)->handle == callback_handle) { - /* Check if we were already in a locked state, as we are NOT allowed to remove any callbacks if we are */ - if (hid_hotplug_context.mutex_in_use) { - (*current)->events = 0; - hid_hotplug_context.cb_list_dirty = 1; - } else { - struct hid_hotplug_callback *next = (*current)->next; - free(*current); - *current = next; + if (hid_hotplug_context.hotplug_cbs == NULL) { + register_global_error("hid_hotplug_deregister_callback: no callbacks are registered"); + } + else { + /* Remove this notification: the entries already marked for removal are + skipped, so that a handle cannot be deregistered a second time */ + for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { + if ((*current)->handle == callback_handle && (*current)->events != 0) { + /* Free the undelivered initial pass: once deregistered, the callback must never fire */ + hid_free_enumeration((*current)->replay); + (*current)->replay = NULL; + /* Check if we were already in a locked state, as we are NOT allowed to remove any callbacks if we are */ + if (hid_hotplug_context.mutex_in_use) { + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } else { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + } + result = 0; + break; } - result = 0; - break; } - } - hid_internal_hotplug_cleanup(); + if (result != 0) { + register_global_error("hid_hotplug_deregister_callback: unknown callback handle"); + } + + hid_internal_hotplug_cleanup(); + } pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* If this deregistration stopped the event thread, join it with the mutex + released. A no-op when called from within a callback (the event thread + cannot join itself): its epilogue releases the resources, and the next + application-thread collector joins it before reusing the thread record. */ + if (hid_internal_hotplug_collect_thread() != 0) { + /* Reported as a failure even when the callback itself was removed above + (its handle is dead either way): a failed join of the library's own + thread is worth surfacing over the deregistration result, and the + next collector retries the join. Practically unreachable, see + hid_internal_hotplug_exit(). */ + register_global_error("hid_hotplug_deregister_callback: failed to join the hotplug events thread"); + return -1; + } + return result; } @@ -1977,7 +2973,7 @@ HID_API_EXPORT struct hid_device_info *HID_API_CALL hid_get_device_info(hid_devi register_device_error(dev, NULL); } else { - dev->device_info = create_device_info(dev->device_handle); + dev->device_info = create_device_info(dev->device_handle, 0); if (!dev->device_info) { register_device_error(dev, "Failed to create hid_device_info"); } diff --git a/netbsd/hid.c b/netbsd/hid.c index a8d088c30..40f3c1f96 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -779,7 +779,8 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven (void)flags; (void)callback; (void)user_data; - (void)callback_handle; + if (callback_handle) + *callback_handle = 0; return -1; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index ff17785c6..987881cfb 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,6 +1,6 @@ -# Backend-generic HIDAPI (unit-)tests, run against a virtual HID device. +# Backend-generic device-I/O tests and macOS hotplug lifecycle tests. # -# The tests are written against the public HIDAPI API and the backend-agnostic +# The device-I/O tests use the public HIDAPI API and the backend-agnostic # test_virtual_device interface (test_virtual_device.h), so the same test runs # against any backend for which a virtual-device provider exists. Each provider # implements the pre-recorded "scenario" protocol: the test triggers a scenario @@ -12,11 +12,12 @@ # - Windows / winapi : test_virtual_device_win.c (modified vhidmini2 UMDF2 driver) # - macOS / darwin : test_virtual_device_mac.c (IOHIDUserDevice) # -# The libusb (raw-gadget), Windows and macOS virtual devices need privileged -# out-of-band setup (kernel modules / a signed driver / an entitlement) that is -# only performed by the dedicated CI jobs. Whenever the virtual device cannot be -# created or does not enumerate, the test returns CTest's SKIP code (77) instead -# of failing, so ordinary builds on any host stay green. +# The libusb (raw-gadget) and Windows virtual devices need privileged out-of-band +# setup (kernel modules / a signed driver) that only the dedicated label-gated CI +# jobs perform; the macOS one needs an entitlement plus interactive consent and +# therefore runs only on a developer's entitled Mac. Whenever the virtual device +# cannot be created or does not enumerate, the test returns CTest's SKIP code +# (77) instead of failing, so ordinary builds on any host stay green. # SKIP_RETURN_CODE (used to report a provider self-skip as SKIP rather than # FAIL) needs CMake/CTest 3.16. With an older CMake a legitimate self-skip @@ -48,9 +49,62 @@ function(hidapi_add_vdev_test name provider backend) ) endfunction() +# Define a tier-1 hotplug-API test 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 . It +# retries a failed first registration after explicit hid_init(). It self-skips +# (77) only when that retry also fails (including a backend that reports hotplug +# as unsupported, e.g. 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 built from +# test_hotplug.c + . 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). +# The uhid, rawgadget and Windows vhidmini providers all implement presence +# toggling; the darwin provider does not yet. +# is the per-event wait budget inside the test; +# is a finite watchdog for a backend hang. The test reports a +# failure after its first event deadline, then safely cleans up and summarizes. +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 300) endif() # --- Linux: libusb backend via /dev/raw-gadget (+ dummy_hcd) ---------------- @@ -58,22 +112,48 @@ endif() # 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) + # The rawgadget provider toggles presence by unbinding/rebinding the gadget + # from the dummy_hcd UDC; this runs in the label-gated libusb-vhid-test VM + # job. 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 900) + target_compile_definitions(Hotplug_libusb PRIVATE TEST_VDEV_HAS_MANUFACTURER=1) + # These tests share dummy_udc.0, so concurrent CTest runs would otherwise + # create, destroy, or toggle the same virtual device. + set_tests_properties(DeviceIO_libusb Hotplug_libusb PROPERTIES + RESOURCE_LOCK vdev_rawgadget) 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) + # The vhidmini provider toggles presence by disabling/enabling the HID child + # devnode; this runs in the label-gated win-vhid-test job. + hidapi_add_hotplug_test(Hotplug_winapi test_virtual_device_win.c hidapi_winapi 30000 900) + # These tests share the installed vhidmini HID child, whose presence the + # hotplug test toggles. + set_tests_properties(DeviceIO_winapi Hotplug_winapi PROPERTIES + RESOURCE_LOCK vdev_vhidmini) + # hid: HidD_GetPreparsedData / HidP_GetCaps (device caps). + # cfgmgr32: CM_Locate_DevNodeA / CM_Get_Child / CM_Get_Sibling / + # CM_Get_Device_IDA / CM_Disable_DevNode / CM_Enable_DevNode, used by the + # provider to locate the root-enumerated function devnode and toggle its HID + # child 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 "$") # 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() @@ -83,6 +163,23 @@ 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") + + add_executable(HotplugLifecycle_darwin test_hotplug_mac.c) + set_target_properties(HotplugLifecycle_darwin PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED TRUE + ) + target_link_libraries(HotplugLifecycle_darwin PRIVATE hidapi_darwin Threads::Threads) + if(HIDAPI_ENABLE_ASAN) + target_link_options(HotplugLifecycle_darwin PRIVATE -fsanitize=address) + endif() + add_test(NAME HotplugLifecycle_darwin COMMAND HotplugLifecycle_darwin) + set_tests_properties(HotplugLifecycle_darwin PROPERTIES TIMEOUT 60) endif() diff --git a/src/tests/README.md b/src/tests/README.md index 1b6e08e29..1724809e5 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -22,17 +22,81 @@ 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, event masks, exactly-once ENUMERATE pass, callback-return deregistration, pass-before-live ordering, full payloads, filtering, dispatch order, deregistration post-condition, re-entrant registration and callback error isolation | + +## Hotplug tests + +The hotplug tests come in two tiers: + +* **Tier 1 — `HotplugAPI_`** (`test_hotplug_api.c`): the parts of the + hotplug contract observable *without* a device event: argument validation, + callback-handle properties and stale-handle safety, implicit `hid_init()`, + `hid_exit()` teardown (including the register-to-immediate-`hid_exit()` loop), + and two-thread register/deregister churn. Needs no virtual device or + privileges, so it runs against **every** backend in the ordinary per-push CI + matrix. If the first registration fails, the probe retries after explicit + `hid_init()`: a successful retry fails the test for broken implicit + initialization; it self-skips (77) only when the retry also fails, including + a backend that reports hotplug as unsupported at runtime (e.g. a libusb + without `LIBUSB_CAP_HAS_HOTPLUG`). +* **Tier 2 — `Hotplug_`** (`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`). The **uhid** + (`UHID_DESTROY` / `UHID_CREATE2` on the same open `/dev/uhid` fd), + **rawgadget** (unbind/rebind the gadget from the `dummy_hcd` UDC) and + Windows **vhidmini** (disable/enable the HID child devnode) providers all + implement toggling. `Hotplug_hidraw` runs per-push (in `builds.yml`'s + ubuntu-cmake job, like `DeviceIO_hidraw`); `Hotplug_libusb` and + `Hotplug_winapi` run in the label-gated `ci-virtual-device` jobs + (`libusb-vhid-test` / `win-vhid-test`), which provide the privileged + environment those providers need. The darwin provider still returns + `TEST_VDEV_UNAVAILABLE` from the toggle calls, so `Hotplug_darwin` + self-skips until presence toggling is implemented for it. + + T8b, cancellation during an ENUMERATE pass, needs two concurrent devices and + is reported as a distinct skipped subtest for the single-device Raw Gadget and + Windows providers; the UHID provider exercises it. + +| 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: MSVC/NMake/ClangCL; windows-cmake-mingw) | | +| `HotplugAPI_darwin` | yes (macos-cmake) | | +| `Hotplug_hidraw` | yes (ubuntu-cmake, via `uhid`) | the tier-2 test that runs per-push | +| `Hotplug_libusb` | builds, self-skips | runs in the label-gated `libusb-vhid-test` VM job | +| `Hotplug_winapi` | builds, self-skips | runs in the label-gated `win-vhid-test` job | +| `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): recording callbacks only deep-copy the event +into a log under a lock and never call `hid_enumerate`, `hid_open`, or +`hid_error(NULL)`; T14 deliberately holds a callback open so deregistration's +wait is observable, and T15 registers/deregisters from a callback as the API +allows. Every expectation is awaited with a deadline-based predicate poll; 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. + +T9b holds a snapshot callback while the device disconnects; T18/T18b exercise +immediate and queued-snapshot cancellation, and T19 exits with ENUMERATE work +before reinitializing. Set `HIDAPI_HOTPLUG_STRESS=1` to additionally run T20's +25 arrival-versus-snapshot races. After an event deadline fails, the suite +cleans up and prints its summary without starting another scenario. ## Providers | Platform / backend | Provider | Mechanism | CI | |--------------------|----------|-----------|----| | Linux / hidraw | `test_virtual_device_uhid.c` | kernel `/dev/uhid` | runs in `builds.yml` (ubuntu-cmake) | -| Linux / libusb | `test_virtual_device_rawgadget.c` | `/dev/raw-gadget` + `dummy_hcd` (in a VM) | builds + self-skips in `builds.yml`; runs in the manual `libusb-vhid-test` job (in a VM) | -| Windows / winapi | `test_virtual_device_win.c` + `windows/driver/` | modified vhidmini2 UMDF2 driver | builds + self-skips in `builds.yml`; runs in the manual `win-vhid-test` job | +| Linux / libusb | `test_virtual_device_rawgadget.c` | `/dev/raw-gadget` + `dummy_hcd` (in a VM) | builds + self-skips in `builds.yml`; runs in `libusb-vhid-test` (workflow dispatch or the `ci-virtual-device` PR label), in a VM | +| Windows / winapi | `test_virtual_device_win.c` + `windows/driver/` | modified vhidmini2 UMDF2 driver | builds + self-skips in `builds.yml`; runs in `win-vhid-test` (workflow dispatch or the `ci-virtual-device` PR label) | | macOS / darwin | `test_virtual_device_mac.c` | `IOHIDUserDevice` (IOKit) | builds + self-skips in `builds.yml` (macos-cmake); runs on a real Mac | -Whenever a virtual device cannot be created or does not enumerate, the test +Whenever a virtual device cannot initially be created or does not initially enumerate, the test returns CTest's **skip** code (77) instead of failing, so ordinary builds on any host stay green. @@ -52,8 +116,10 @@ for the per-push CI matrix: * **Linux / libusb** — needs the `raw_gadget` and `dummy_hcd` kernel modules, which the hosted `ubuntu-latest` kernel is built *without* (it has no USB gadget subsystem). The `libusb-vhid-test` workflow therefore runs the test - inside a lightweight VM (`virtme-ng` + QEMU) booting a *generic* Ubuntu kernel - whose `linux-modules-extra` ships both modules; the VM shares the host + inside a lightweight VM (`virtme-ng` + QEMU) booting a *generic* Ubuntu kernel: + `linux-modules-extra` supplies `raw_gadget`; Ubuntu does not package + `dummy_hcd`, so the 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 host-built binaries. The same approach works locally and on WSL2 (whose default kernel also lacks these modules). * **macOS** — creating an `IOHIDUserDevice` is gated by the @@ -89,7 +155,11 @@ the device-I/O test is not wired up there. 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 +cd build +sudo ctest -R DeviceIO_hidraw --output-on-failure +sudo ctest -R Hotplug_hidraw --output-on-failure +# tier-1 hotplug API tests need no device and no root: +ctest -R HotplugAPI --output-on-failure ``` On Windows/macOS configure with `-DHIDAPI_WITH_TESTS=ON` and run `ctest`; the diff --git a/src/tests/test_hotplug.c b/src/tests/test_hotplug.c new file mode 100644 index 000000000..5fb46ac9d --- /dev/null +++ b/src/tests/test_hotplug.c @@ -0,0 +1,1825 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Tier-2 hotplug tests, run against a virtual HID device whose + presence can be toggled (test_virtual_device_unplug/_replug): + asynchronous delivery, the exactly-once ENUMERATE pass, + callback-return deregistration, pass-before-live ordering, + ARRIVED/LEFT payloads, VID/PID filtering, dispatch order, + deregistration post-conditions and re-entrant (in-callback) + registration. + + Synchronization discipline (hotplug tests are notoriously + flaky when built on sleeps): + - recording callbacks only lock, deep-copy the event into a log, + unlock and return; they never call hid_enumerate/hid_open/ + hid_error(NULL); + - dedicated scenarios hold a callback on a bounded gate or call the + permitted re-entrant hotplug register/deregister APIs; + - every expectation is awaited with a deadline-based + predicate poll (hp_wait_*), never a bare sleep; + - 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. + + All assertions filter on the test's own VID/PID/serial: real + devices may be present on the host and may generate events + concurrently. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +#include +#include +#include + +#include + +#include "test_virtual_device.h" +#include "test_platform.h" + +/* CTest treats this exit code as "skipped" (see SKIP_RETURN_CODE in CMake). */ +#define EXIT_SKIP 77 + +/* Test-unique ids so enumeration/filtering cannot collide with real hardware. + On Linux/macOS the device is created on demand, so the primary uses a PID + distinct from test_device_io.c's 0x9001. On Windows the virtual device is a + single pre-installed static driver (src/tests/windows/driver) whose identity + is fixed, so the primary must match it (PID 0x9001, serial == the driver's + VHIDMINI_SERIAL_NUMBER_STRING); the second device has no counterpart there and + is reported UNAVAILABLE by the Windows provider. */ +#define TEST_VID 0xF1D0 +#if defined(_WIN32) +#define TEST_PID 0x9001 /* the static vhidmini driver's HIDMINI_PID */ +#else +#define TEST_PID 0x9002 +#endif +#define TEST_PID_2 0x9003 /* second device, for the mid-pass stop test */ +#define TEST_SERIAL "HIDAPI-HOTPLUG-TEST" +#define TEST_SERIAL_2 "HIDAPI-HOTPLUG-TEST-2" + +#define ALL_EVENTS (HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT) + +/* Budget for one awaited event/predicate. The uhid provider is fast (10s is + generous); the rawgadget/win providers go through a full (virtual) + USB stack, so their CMake target overrides this with 30s. */ +#ifndef TEST_HOTPLUG_EVENT_TIMEOUT_MS +#define TEST_HOTPLUG_EVENT_TIMEOUT_MS 30000 +#endif +#define EVENT_TIMEOUT_MS TEST_HOTPLUG_EVENT_TIMEOUT_MS + +#define WAIT_TICK_MS 10 + +static int g_failures = 0; +static int g_skipped = 0; +static test_atomic_int g_deadline_failed; + +static void hp_cleanup_callbacks(void); + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf(" CHECK failed: %s (line %d)\n", \ + #cond, __LINE__); \ + fflush(stdout); \ + g_failures++; \ + hp_cleanup_callbacks(); \ + return -1; \ + } \ + } while (0) + +/* Print a flushed progress marker so a hang is localised on a CTest timeout. */ +static void step(const char *what) +{ + printf(" -> %s\n", what); + fflush(stdout); +} + +static void report(const char *name, int rc) +{ + hp_cleanup_callbacks(); + if (rc == EXIT_SKIP) + g_skipped++; + printf("%s %s\n", rc == EXIT_SKIP ? "SKIP" : (rc == 0 ? "PASS" : "FAIL"), name); + fflush(stdout); +} + +/* ------------------------------------------------------------------ */ +/* The event log. One global, ordered log shared by every callback: */ +/* cross-callback ordering assertions (dispatch order, barriers) fall */ +/* out of the log order itself. */ + +#define HP_MAX_EVENTS 128 +#define HP_PATH_MAX 256 +#define HP_SERIAL_MAX 64 + +typedef struct hp_event { + int seq; /* global arrival order */ + hid_hotplug_callback_handle handle; /* the callback_handle parameter */ + hid_hotplug_event event; + unsigned short vendor_id; + unsigned short product_id; + char path[HP_PATH_MAX]; + char serial[HP_SERIAL_MAX]; /* narrowed; "" when NULL */ + wchar_t serial_full[HP_SERIAL_MAX]; + wchar_t manufacturer[HP_PATH_MAX]; + wchar_t product[HP_PATH_MAX]; + int path_was_null, serial_was_null, manufacturer_was_null, product_was_null; + unsigned short release_number, usage_page, usage; + int interface_number; + hid_bus_type bus_type; + int device_was_null, event_valid, string_truncated; + unsigned long long thread_id; /* thread the callback ran on */ + int next_was_null; /* device->next == NULL held */ +} hp_event; + +static test_mutex g_log_lock; +static hp_event g_events[HP_MAX_EVENTS]; +static int g_event_count; +static int g_event_overflow; +static int g_seq_counter; +static unsigned long long g_main_tid; + +/* Include registrations made from callbacks; drain before their contexts + leave scope on a failed CHECK, and before starting another scenario. */ +static hid_hotplug_callback_handle g_handles[256]; +static int g_handle_count; +static hid_hotplug_callback_handle g_retired[128]; +static int g_retired_count; +static int g_late_callback; + +static void hp_mark_retired(hid_hotplug_callback_handle handle) +{ + test_mutex_lock(&g_log_lock); + if (g_retired_count < (int)(sizeof(g_retired) / sizeof(g_retired[0]))) + g_retired[g_retired_count++] = handle; + else + g_event_overflow = 1; + test_mutex_unlock(&g_log_lock); +} + +static int hp_register(unsigned short vid, unsigned short pid, int events, + int flags, hid_hotplug_callback_fn callback, + void *user_data, hid_hotplug_callback_handle *handle) +{ + int rc = hid_hotplug_register_callback(vid, pid, events, flags, + callback, user_data, handle); + if (rc == 0) { + test_mutex_lock(&g_log_lock); + if (g_handle_count == (int)(sizeof(g_handles) / sizeof(g_handles[0]))) { + fprintf(stderr, "callback cleanup list overflow\n"); + fflush(stderr); + _Exit(EXIT_FAILURE); + } + g_handles[g_handle_count++] = *handle; + test_mutex_unlock(&g_log_lock); + } + return rc; +} + +static void hp_cleanup_callbacks(void) +{ + for (;;) { + hid_hotplug_callback_handle handle; + test_mutex_lock(&g_log_lock); + if (!g_handle_count) { + test_mutex_unlock(&g_log_lock); + return; + } + handle = g_handles[--g_handle_count]; + test_mutex_unlock(&g_log_lock); + /* A non-zero callback return or explicit teardown may have removed it. */ + (void)hid_hotplug_deregister_callback(handle); + } +} + +static void hp_copy_wide(wchar_t *out, size_t capacity, const wchar_t *in, + int *truncated) +{ + if (in) { + size_t n = wcslen(in); + if (n >= capacity) { + *truncated = 1; + n = capacity - 1; + } + memcpy(out, in, n * sizeof(*out)); + out[n] = L'\0'; + } +} + +/* Non-empty environment variable check. MSVC's /W4 /WX flags getenv() as + deprecated (C4996), so use the Win32 API there. */ +static int hp_env_set(const char *name) +{ +#ifdef _WIN32 + return GetEnvironmentVariableA(name, NULL, 0) != 0; +#else + const char *v = getenv(name); + return v != NULL && v[0] != '\0'; +#endif +} + +/* Deep-copy the fields the assertions need. Called from the callbacks, with + g_log_lock held for the shortest possible time; the device pointer is only + valid for the duration of the callback. */ +static void hp_record(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event) +{ + int i; + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_retired_count; i++) + if (g_retired[i] == handle) + g_late_callback = 1; + if (g_event_count < HP_MAX_EVENTS) { + hp_event *e = &g_events[g_event_count++]; + memset(e, 0, sizeof(*e)); + e->seq = g_seq_counter++; + e->handle = handle; + e->event = event; + e->device_was_null = (device == NULL); + e->event_valid = (event == HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED + || event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT); + e->thread_id = test_thread_id(); + if (device) { + e->vendor_id = device->vendor_id; + e->product_id = device->product_id; + e->next_was_null = (device->next == NULL); + e->release_number = device->release_number; + e->usage_page = device->usage_page; + e->usage = device->usage; + e->interface_number = device->interface_number; + e->bus_type = device->bus_type; + e->path_was_null = (device->path == NULL); + e->serial_was_null = (device->serial_number == NULL); + e->manufacturer_was_null = (device->manufacturer_string == NULL); + e->product_was_null = (device->product_string == NULL); + hp_copy_wide(e->serial_full, HP_SERIAL_MAX, device->serial_number, &e->string_truncated); + hp_copy_wide(e->manufacturer, HP_PATH_MAX, device->manufacturer_string, &e->string_truncated); + hp_copy_wide(e->product, HP_PATH_MAX, device->product_string, &e->string_truncated); + if (device->path) { + if (strlen(device->path) >= sizeof(e->path)) + e->string_truncated = 1; + snprintf(e->path, sizeof(e->path), "%s", device->path); + } + if (device->serial_number) { + size_t k; + for (k = 0; k + 1 < sizeof(e->serial) && device->serial_number[k]; k++) { + wchar_t wc = device->serial_number[k]; + e->serial[k] = (wc > 0 && wc < 128) ? (char)wc : '?'; + } + e->serial[k] = '\0'; + } + } + } else { + g_event_overflow = 1; + } + test_mutex_unlock(&g_log_lock); +} + +/* Does a logged event match? 0 acts as a wildcard for handle/event/pid; + NULL for serial. A non-zero pid additionally requires the test VID. */ +static int hp_match(const hp_event *e, hid_hotplug_callback_handle handle, + int event_mask, unsigned short pid, const char *serial) +{ + if (handle != 0 && e->handle != handle) + return 0; + if (event_mask != 0 && !(e->event & event_mask)) + return 0; + if (pid != 0 && (e->vendor_id != TEST_VID || e->product_id != pid)) + return 0; + if (serial != NULL && strcmp(e->serial, serial) != 0) + return 0; + return 1; +} + +static int hp_count(hid_hotplug_callback_handle handle, int event_mask, + unsigned short pid, const char *serial) +{ + int i, n = 0; + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) + if (hp_match(&g_events[i], handle, event_mask, pid, serial)) + n++; + test_mutex_unlock(&g_log_lock); + return n; +} + +/* Copy the first matching event out of the log. Returns 0 when found. */ +static int hp_find_first(hp_event *out, hid_hotplug_callback_handle handle, + int event_mask, unsigned short pid, const char *serial) +{ + int i, found = -1; + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) { + if (hp_match(&g_events[i], handle, event_mask, pid, serial)) { + *out = g_events[i]; + found = 0; + break; + } + } + test_mutex_unlock(&g_log_lock); + return found; +} + +/* Deadline-based predicate poll: the ONLY way the tests wait. */ +static int hp_wait_count_at_least(hid_hotplug_callback_handle handle, + int event_mask, unsigned short pid, + const char *serial, int min_count, + int timeout_ms) +{ + long long deadline = test_now_ms() + timeout_ms; + for (;;) { + if (hp_count(handle, event_mask, pid, serial) >= min_count) + return 0; + if (test_now_ms() >= deadline) { + test_atomic_store(&g_deadline_failed, 1); + return -1; + } + test_sleep_ms(WAIT_TICK_MS); + } +} + +/* Wait for *flag (read under the log lock) to become non-zero. */ +static int hp_wait_flag(const int *flag, int timeout_ms) +{ + long long deadline = test_now_ms() + timeout_ms; + for (;;) { + int set; + test_mutex_lock(&g_log_lock); + set = *flag; + test_mutex_unlock(&g_log_lock); + if (set) + return 0; + if (test_now_ms() >= deadline) { + test_atomic_store(&g_deadline_failed, 1); + return -1; + } + test_sleep_ms(WAIT_TICK_MS); + } +} + +/* Drain callbacks before sweeping every event's payload/thread invariants + and resetting the log. Test-device strings must fit the log's buffers. */ +static void hp_reset_log(const char *test_name) +{ + int i; + hp_cleanup_callbacks(); + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) { + if (g_events[i].thread_id == g_main_tid) { + printf(" INVARIANT failed before %s: an event was " + "delivered on the application's main thread\n", test_name); + fflush(stdout); + g_failures++; + break; + } + if (g_events[i].device_was_null || !g_events[i].event_valid + || !g_events[i].next_was_null + || (g_events[i].vendor_id == TEST_VID + && (g_events[i].product_id == TEST_PID || g_events[i].product_id == TEST_PID_2) + && g_events[i].string_truncated)) { + printf(" INVARIANT failed before %s: invalid or truncated event payload\n", test_name); + fflush(stdout); + g_failures++; + } + } + if (g_event_overflow) { + printf(" INVARIANT failed before %s: event log overflow\n", test_name); + fflush(stdout); + g_failures++; + } + if (g_late_callback) { + printf(" INVARIANT failed before %s: callback after deregistration returned\n", test_name); + fflush(stdout); + g_failures++; + } + g_event_count = 0; + g_event_overflow = 0; + g_retired_count = 0; + g_late_callback = 0; + test_mutex_unlock(&g_log_lock); +} + +/* ------------------------------------------------------------------ */ +/* Recording callbacks only copy into the log; dedicated callbacks */ +/* below deliberately hold a gate or exercise re-entrant hotplug calls. */ + +/* Plain recorder. */ +static int HID_API_CALL cb_log(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + (void)user_data; + hp_record(callback_handle, device, event); + return 0; +} + +/* Recorder that asks to be deregistered (returns the supplied non-zero value) on the first event for + the test's primary device. */ +static int HID_API_CALL cb_return_on_ours(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + hp_record(callback_handle, device, event); + if (device && device->vendor_id == TEST_VID && device->product_id == TEST_PID) + return *(int *)user_data; + return 0; +} + +/* Recorder that asks to be deregistered on its very first event, whichever + device it is for (the ENUMERATE snapshot order is unspecified). */ +static int HID_API_CALL cb_return_first(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + hp_record(callback_handle, device, event); + return *(int *)user_data; +} + +/* T14: signals "entered", waits on a bounded release gate, then signals + "exited". Lets the main thread observe that deregistration blocks until an + in-progress invocation has completed. The context is heap-allocated and + freed right after deregistration returns: if the backend ever invoked the + callback again, ASan would flag the use-after-free below. */ +typedef struct slow_ctx { + int entered; + int exited; + int release; + int expired; + int exit_seq; +} slow_ctx; + +static int HID_API_CALL cb_slow(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + slow_ctx *ctx = (slow_ctx *)user_data; + hp_record(callback_handle, device, event); + test_mutex_lock(&g_log_lock); + ctx->entered = 1; + test_mutex_unlock(&g_log_lock); + if (hp_wait_flag(&ctx->release, EVENT_TIMEOUT_MS) != 0) { + test_mutex_lock(&g_log_lock); + ctx->expired = 1; + test_mutex_unlock(&g_log_lock); + } + test_mutex_lock(&g_log_lock); + ctx->exited = 1; + ctx->exit_seq = g_seq_counter++; + test_mutex_unlock(&g_log_lock); + return 0; +} + +/* T15: on the first ARRIVED for the primary device, registers a child + callback WITH ENUMERATE and deregisters itself - both from within the + callback (the hotplug API is documented re-entrant). */ +typedef struct parent_ctx { + int acted; /* run-once guard, taken by the first qualifying ARRIVED */ + int done; /* published LAST, after the results below are stored */ + int child_rc; + hid_hotplug_callback_handle child_handle; + int self_dereg_rc; + int invalid_dereg_rc; + int parent_active; + int child_nested; +} parent_ctx; + +static int HID_API_CALL cb_child(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + parent_ctx *ctx = (parent_ctx *)user_data; + test_mutex_lock(&g_log_lock); + if (ctx->parent_active) + ctx->child_nested = 1; + test_mutex_unlock(&g_log_lock); + hp_record(handle, device, event); + return 0; +} + +static int HID_API_CALL cb_parent(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + parent_ctx *ctx = (parent_ctx *)user_data; + int act = 0; + + test_mutex_lock(&g_log_lock); + ctx->parent_active = 1; + test_mutex_unlock(&g_log_lock); + hp_record(callback_handle, device, event); + + if (event == HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED + && device && device->vendor_id == TEST_VID && device->product_id == TEST_PID) { + test_mutex_lock(&g_log_lock); + if (!ctx->acted) { + ctx->acted = 1; + act = 1; + } + test_mutex_unlock(&g_log_lock); + } + + if (act) { + hid_hotplug_callback_handle child = 0; + int rc = hp_register(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_child, ctx, &child); + int dereg_rc = hid_hotplug_deregister_callback(callback_handle); + int invalid_rc = hid_hotplug_deregister_callback(0); + /* Store the results and only then publish 'done', in one locked + section: the main thread waits on 'done', so it can never observe + the results half-written. */ + test_mutex_lock(&g_log_lock); + ctx->child_rc = rc; + ctx->child_handle = child; + ctx->self_dereg_rc = dereg_rc; + ctx->invalid_dereg_rc = invalid_rc; + ctx->parent_active = 0; + ctx->done = 1; + test_mutex_unlock(&g_log_lock); + } else { + test_mutex_lock(&g_log_lock); + ctx->parent_active = 0; + test_mutex_unlock(&g_log_lock); + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Device-presence plumbing */ + +static test_virtual_device *g_vdev; /* primary device (TEST_PID) */ + +/* One hid_enumerate() pass: is a device with this pid+serial visible? + Only ever called from the main thread (HIDAPI's general thread-safety + rule), and never from inside a callback. */ +static int hp_enumerated_now(unsigned short pid, const char *serial) +{ + struct hid_device_info *devs = hid_enumerate(TEST_VID, pid); + struct hid_device_info *cur; + int found = 0; + for (cur = devs; cur; cur = cur->next) { + size_t i; + char narrow[HP_SERIAL_MAX] = ""; + if (!cur->serial_number) + continue; + for (i = 0; i + 1 < sizeof(narrow) && cur->serial_number[i]; i++) { + wchar_t wc = cur->serial_number[i]; + narrow[i] = (wc > 0 && wc < 128) ? (char)wc : '?'; + } + narrow[i] = '\0'; + if (strcmp(narrow, serial) == 0) { + found = 1; + break; + } + } + hid_free_enumeration(devs); + return found; +} + +/* Readiness barrier: poll enumeration until the device is (not) visible. */ +static int hp_wait_enumerated(unsigned short pid, const char *serial, + int present, int timeout_ms) +{ + long long deadline = test_now_ms() + timeout_ms; + for (;;) { + if (hp_enumerated_now(pid, serial) == present) + return 0; + if (test_now_ms() >= deadline) { + test_atomic_store(&g_deadline_failed, 1); + return -1; + } + test_sleep_ms(50); + } +} + +/* Establish a known device state at the start of a test, whatever a previous + (possibly failed) test left behind. */ +static int ensure_present(void) +{ + if (!hp_enumerated_now(TEST_PID, TEST_SERIAL)) + (void)test_virtual_device_replug(g_vdev); + return hp_wait_enumerated(TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS); +} + +static int ensure_absent(void) +{ + if (hp_enumerated_now(TEST_PID, TEST_SERIAL)) + (void)test_virtual_device_unplug(g_vdev); + return hp_wait_enumerated(TEST_PID, TEST_SERIAL, 0, EVENT_TIMEOUT_MS); +} + +/* ------------------------------------------------------------------ */ +/* T6: events are delivered asynchronously (never on the registering */ +/* thread) and the callback receives the same handle that */ +/* hp_register() wrote to *callback_handle. */ +typedef struct publication_ctx { + hid_hotplug_callback_handle *out_handle; + int published; +} publication_ctx; + +static int HID_API_CALL cb_publication(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + publication_ctx *ctx = (publication_ctx *)user_data; + /* *out_handle is the registering thread's local written by + hid_hotplug_register_callback(). Reading it here without application + synchronization is legitimate only because the contract requires that + write to be ordered before any event can be delivered; that ordering + is exactly what T6 checks. */ + int published = (*ctx->out_handle == handle); + test_mutex_lock(&g_log_lock); + ctx->published = published; + test_mutex_unlock(&g_log_lock); + hp_record(handle, device, event); + return 0; +} + +static int t6_async_delivery(void) +{ + hid_hotplug_callback_handle h = 0; + hp_event ev; + publication_ctx ctx = { &h, 0 }; + int published; + + CHECK(ensure_present() == 0); + hp_reset_log("T6"); + + step("register with ENUMERATE while the device is present"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_publication, &ctx, &h) == 0); + CHECK(h > 0); + + step("wait for the synthetic ARRIVED"); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + CHECK(hp_find_first(&ev, h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(ev.thread_id != g_main_tid); /* asynchronous delivery */ + CHECK(ev.handle == h); /* handle parameter == *callback_handle */ + test_mutex_lock(&g_log_lock); + published = ctx.published; + test_mutex_unlock(&g_log_lock); + CHECK(published); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T7: each connection is reported exactly once - by the ENUMERATE */ +/* pass or as a live event, never both. The LEFT of a subsequent */ +/* unplug is the barrier proving no duplicate ARRIVED was in flight. */ +static int t7_exactly_once(void) +{ + hid_hotplug_callback_handle h = 0; + + CHECK(ensure_present() == 0); + hp_reset_log("T7"); + + step("register with ENUMERATE while the device is present"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h) == 0); + + step("wait for the synthetic ARRIVED"); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("unplug; the LEFT is the exactly-once barrier"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + + step("replug: the reconnection is one more ARRIVED"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 2, EVENT_TIMEOUT_MS) == 0); + + step("unplug again (barrier for the second ARRIVED)"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 2, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 2); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T8a: a non-zero callback return value deregisters the callback: the */ +/* handle is dead (-1) and no further events reach it. The barrier is */ +/* a second, still-registered callback observing a later event the */ +/* first one must not see. */ +static int t8a_return_deregisters(int result) +{ + hid_hotplug_callback_handle h_ret = 0, h_bar = 0; + + CHECK(ensure_present() == 0); + hp_reset_log("T8a"); + + step("register the non-zero-return callback and a barrier callback"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_return_on_ours, &result, &h_ret) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_bar) == 0); + + step("unplug: both callbacks see the LEFT; the first returns non-zero"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_ret, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_bar, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("replug: only the barrier callback may see the ARRIVED"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_bar, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + CHECK(hp_count(h_ret, 0, 0, NULL) == 1); /* exactly the one LEFT */ + step("the handle was already freed by the non-zero return"); + CHECK(hid_hotplug_deregister_callback(h_ret) == -1); + + CHECK(hid_hotplug_deregister_callback(h_bar) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T8b: a non-zero return during the ENUMERATE pass stops the */ +/* remainder of the pass: with TWO matching devices present, the */ +/* callback is invoked exactly once. A later ENUMERATE registration */ +/* observing both devices is the barrier. */ +static int t8b_return_stops_pass(int result) +{ + test_virtual_device *vdev2 = NULL; + hid_hotplug_callback_handle h_once = 0, h_probe = 0; + int rc; + + CHECK(ensure_present() == 0); + + step("create the second device"); + rc = test_virtual_device_create(&vdev2, TEST_VID, TEST_PID_2, TEST_SERIAL_2); + if (rc == TEST_VDEV_UNAVAILABLE) { + /* Some providers (raw-gadget: a single dummy_udc.0) can only expose one + device at a time. This sub-test needs two concurrent devices, so skip + it here rather than failing -- it is not counted as a failure. */ + printf(" T8b needs a second concurrent device, unavailable on this " + "provider - skipping this sub-test\n"); + fflush(stdout); + return EXIT_SKIP; + } + CHECK(rc == TEST_VDEV_OK && vdev2 != NULL); + if (hp_wait_enumerated(TEST_PID_2, TEST_SERIAL_2, 1, EVENT_TIMEOUT_MS) != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"second device did not enumerate"); + } + + hp_reset_log("T8b"); + + step("register a non-zero-return callback with ENUMERATE (both devices match)"); + rc = hp_register(TEST_VID, 0, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_return_first, &result, &h_once); + if (rc != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"registration failed"); + } + + step("wait for its single snapshot event"); + if (hp_wait_count_at_least(h_once, 0, 0, NULL, 1, EVENT_TIMEOUT_MS) != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"the non-zero-return callback never fired"); + } + + step("barrier: a fresh ENUMERATE registration sees both devices"); + rc = hp_register(TEST_VID, 0, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h_probe); + if (rc != 0) { + test_virtual_device_destroy(vdev2); + CHECK(!"barrier registration failed"); + } + rc = 0; + if (hp_wait_count_at_least(h_probe, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) != 0) + rc = -1; + if (hp_wait_count_at_least(h_probe, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID_2, TEST_SERIAL_2, 1, EVENT_TIMEOUT_MS) != 0) + rc = -1; + + if (rc == 0) { + /* Exactly one invocation total; which device is unspecified. */ + if (hp_count(h_once, 0, 0, NULL) != 1) { + printf(" CHECK failed: the non-zero-return callback saw %d events " + "(expected 1) (line %d)\n", + hp_count(h_once, 0, 0, NULL), __LINE__); + fflush(stdout); + rc = -1; + } + if (hid_hotplug_deregister_callback(h_once) != -1) { + printf(" CHECK failed: h_once was still registered (line %d)\n", __LINE__); + fflush(stdout); + rc = -1; + } + } + + hp_cleanup_callbacks(); + test_virtual_device_destroy(vdev2); + if (hp_wait_enumerated(TEST_PID_2, TEST_SERIAL_2, 0, EVENT_TIMEOUT_MS) != 0) + rc = -1; + if (rc != 0) { + g_failures++; + return -1; + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T9: the ENUMERATE pass is delivered before live events: even when */ +/* the device is unplugged immediately after registration, the LEFT */ +/* must be preceded by the snapshot ARRIVED (same path). */ +static int t9_pass_before_live(void) +{ + hid_hotplug_callback_handle h = 0; + hp_event arrived, left; + + CHECK(ensure_present() == 0); + hp_reset_log("T9"); + + step("register with ENUMERATE and unplug immediately"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h) == 0); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + + step("wait for the LEFT"); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("the ARRIVED must already be logged, before the LEFT"); + CHECK(hp_find_first(&arrived, h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_find_first(&left, h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(arrived.seq < left.seq); + CHECK(strcmp(arrived.path, left.path) == 0); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T10: live ARRIVED/LEFT payloads: matching VID/PID/serial on */ +/* arrival; the LEFT carries the same path and intact strings; and */ +/* device->next == NULL on every invocation. */ +static int t10_live_payloads(void) +{ + hid_hotplug_callback_handle h = 0; + hp_event arrived, left; + + CHECK(ensure_absent() == 0); + hp_reset_log("T10"); + + step("register (no ENUMERATE) while the device is absent"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h) == 0); + + step("plug: live ARRIVED"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_find_first(&arrived, h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(arrived.vendor_id == TEST_VID); + CHECK(arrived.product_id == TEST_PID); + CHECK(strcmp(arrived.serial, TEST_SERIAL) == 0); + CHECK(arrived.path[0] != '\0'); + + step("unplug: live LEFT correlates by path, strings intact"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_find_first(&left, h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(strcmp(left.path, arrived.path) == 0); + CHECK(strcmp(left.serial, TEST_SERIAL) == 0); + CHECK(left.vendor_id == TEST_VID && left.product_id == TEST_PID); + + CHECK(left.path_was_null == arrived.path_was_null); + CHECK(left.serial_was_null == arrived.serial_was_null); + CHECK(wcscmp(left.serial_full, arrived.serial_full) == 0); + CHECK(left.manufacturer_was_null == arrived.manufacturer_was_null); + CHECK(left.product_was_null == arrived.product_was_null); + CHECK(wcscmp(left.manufacturer, arrived.manufacturer) == 0); + CHECK(wcscmp(left.product, arrived.product) == 0); + #if defined(_WIN32) || defined(TEST_VDEV_HAS_MANUFACTURER) + CHECK(!arrived.manufacturer_was_null && arrived.manufacturer[0] != L'\0'); +#endif + CHECK(!arrived.product_was_null && arrived.product[0] != L'\0'); + CHECK(left.release_number == arrived.release_number); + CHECK(left.usage_page == arrived.usage_page); + CHECK(left.usage == arrived.usage); + CHECK(left.interface_number == arrived.interface_number); + CHECK(left.bus_type == arrived.bus_type); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T11: without ENUMERATE there is no synthetic ARRIVED, yet the LEFT */ +/* of an already-present device is still delivered. Because the pass */ +/* precedes live events, receiving the LEFT with no prior ARRIVED */ +/* proves no synthetic event was pending (zero-window proof). */ +static int t11_left_without_enumerate(void) +{ + hid_hotplug_callback_handle h = 0; + + CHECK(ensure_present() == 0); + hp_reset_log("T11"); + + step("register WITHOUT ENUMERATE while the device is present"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h) == 0); + + step("unplug: the LEFT must still be delivered"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("zero synthetic ARRIVED (the LEFT is the barrier)"); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 0); + + CHECK(hid_hotplug_deregister_callback(h) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T12: VID/PID filtering: exact and vid-only filters and the wildcard */ +/* see the event; a non-matching filter does not. The wildcard */ +/* (registered last, dispatch is in registration order) anchors the */ +/* absence assertion. */ +static int t12_vid_pid_filtering(void) +{ + hid_hotplug_callback_handle h_match = 0, h_vid = 0, h_wrong = 0, h_wild = 0; + hid_hotplug_callback_handle h_pid = 0, h_wrongpid = 0; + unsigned short wrong_pid = 1; + while (wrong_pid == TEST_PID || wrong_pid == TEST_PID_2) + wrong_pid++; + + CHECK(ensure_absent() == 0); + hp_reset_log("T12"); + + step("register exact / vid-only / wrong-vid / wildcard callbacks"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_match) == 0); + CHECK(hp_register(TEST_VID, 0, ALL_EVENTS, 0, + cb_log, NULL, &h_vid) == 0); + CHECK(hp_register(TEST_VID ^ 0x0001, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_wrong) == 0); + CHECK(hp_register(0, TEST_PID, ALL_EVENTS, 0, cb_log, NULL, &h_pid) == 0); + CHECK(hp_register(TEST_VID, wrong_pid, ALL_EVENTS, 0, cb_log, NULL, &h_wrongpid) == 0); + CHECK(hp_register(0, 0, ALL_EVENTS, 0, + cb_log, NULL, &h_wild) == 0); + + step("plug the device"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + + step("exact, vid-only and wildcard callbacks see the ARRIVED"); + CHECK(hp_wait_count_at_least(h_match, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_vid, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_wild, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("the non-matching callback saw nothing of our device"); + /* The wildcard is dispatched after h_wrong (registration order), so once + the wildcard has logged the event, h_wrong's turn is provably over. */ + CHECK(hp_count(h_wrong, 0, TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_count(h_wrongpid, 0, TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_count(h_pid, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_wild, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h_match, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, TEST_PID, TEST_SERIAL) == 1); + CHECK(hp_count(h_vid, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, TEST_PID, TEST_SERIAL) == 1); + CHECK(hp_count(h_pid, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, TEST_PID, TEST_SERIAL) == 1); + CHECK(hp_count(h_wrong, 0, TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_count(h_wrongpid, 0, TEST_PID, TEST_SERIAL) == 0); + + CHECK(hid_hotplug_deregister_callback(h_match) == 0); + CHECK(hid_hotplug_deregister_callback(h_vid) == 0); + CHECK(hid_hotplug_deregister_callback(h_wrong) == 0); + CHECK(hid_hotplug_deregister_callback(h_pid) == 0); + CHECK(hid_hotplug_deregister_callback(h_wrongpid) == 0); + CHECK(hid_hotplug_deregister_callback(h_wild) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T13: one event is dispatched to every matching callback in */ +/* registration order. */ +static int t13_dispatch_order(void) +{ + hid_hotplug_callback_handle h_a = 0, h_b = 0; + hp_event ev_a, ev_b; + + CHECK(ensure_present() == 0); + hp_reset_log("T13"); + + step("register two matching callbacks"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_a) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_b) == 0); + + step("unplug: both see the LEFT"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_a, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(h_b, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("registration order == dispatch order"); + CHECK(hp_find_first(&ev_a, h_a, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_find_first(&ev_b, h_b, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(ev_a.seq < ev_b.seq); + + CHECK(hid_hotplug_deregister_callback(h_a) == 0); + CHECK(hid_hotplug_deregister_callback(h_b) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T14: hid_hotplug_deregister_callback() (from a non-event thread) */ +/* returns only after an in-progress invocation has completed; then */ +/* the callback's resources can be freed safely even though more */ +/* events keep flowing (an ASan leg would catch a use-after-free). */ +typedef struct deregister_ctx { + hid_hotplug_callback_handle handle; + int started, done, rc, return_seq; +} deregister_ctx; + +static void hp_join_or_exit(test_thread *thread) +{ + if (test_thread_join_timeout(thread, EVENT_TIMEOUT_MS) != 0) { + fprintf(stderr, "hotplug helper failed to join; shared state is still in use\n"); + fflush(stderr); + _Exit(EXIT_FAILURE); + } +} + +static void deregister_thread(void *arg) +{ + deregister_ctx *ctx = (deregister_ctx *)arg; + int rc; + test_mutex_lock(&g_log_lock); + ctx->started = 1; + test_mutex_unlock(&g_log_lock); + rc = hid_hotplug_deregister_callback(ctx->handle); + if (rc == 0) + hp_mark_retired(ctx->handle); + test_mutex_lock(&g_log_lock); + ctx->rc = rc; + ctx->return_seq = g_seq_counter++; + ctx->done = 1; + test_mutex_unlock(&g_log_lock); +} + +static void replug_thread(void *arg) +{ + int *rc = (int *)arg; + *rc = test_virtual_device_replug(g_vdev); +} + +static int t14_deregister_postcondition(void) +{ + hid_hotplug_callback_handle h_slow = 0, h_bar = 0; + slow_ctx *ctx; + deregister_ctx dereg = { 0, 0, 0, -2, 0 }; + test_thread plug_thread, dereg_thread; + hp_event barrier; + int plug_rc = TEST_VDEV_ERROR, entered, started = -1, early = 0; + int worker_started = 0, expired, exit_seq; + + CHECK(ensure_absent() == 0); + hp_reset_log("T14"); + ctx = (slow_ctx *)calloc(1, sizeof(*ctx)); + CHECK(ctx != NULL); + if (hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, cb_slow, ctx, &h_slow) != 0 + || hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, cb_log, NULL, &h_bar) != 0 + || test_thread_start(&plug_thread, replug_thread, &plug_rc) != 0) { + hp_cleanup_callbacks(); + free(ctx); + CHECK(!"failed to prepare gated callback"); + } + + /* Replug may wait for the platform to process arrival, so it must not prevent the + application from releasing the callback's gate. */ + entered = hp_wait_flag(&ctx->entered, EVENT_TIMEOUT_MS); + if (entered == 0) { + dereg.handle = h_slow; + worker_started = (test_thread_start(&dereg_thread, deregister_thread, &dereg) == 0); + if (worker_started) { + started = hp_wait_flag(&dereg.started, EVENT_TIMEOUT_MS); + /* This bounded observation rejects an early return while parked; + the sequence checks below also cover a delayed helper start. */ + { + long long deadline = test_now_ms() + 100; + do { + test_mutex_lock(&g_log_lock); + early = dereg.done; + test_mutex_unlock(&g_log_lock); + if (early) + break; + test_sleep_ms(WAIT_TICK_MS); + } while (test_now_ms() < deadline); + } + } + } + test_mutex_lock(&g_log_lock); + ctx->release = 1; + test_mutex_unlock(&g_log_lock); + hp_join_or_exit(&plug_thread); + if (worker_started) + hp_join_or_exit(&dereg_thread); + /* Keep storage alive even on a failed start/entry/early-return path. */ + (void)hid_hotplug_deregister_callback(h_slow); + test_mutex_lock(&g_log_lock); + expired = ctx->expired; + exit_seq = ctx->exit_seq; + test_mutex_unlock(&g_log_lock); + free(ctx); + CHECK(entered == 0 && worker_started && started == 0); + CHECK(plug_rc == TEST_VDEV_OK); + CHECK(!early && !expired); + CHECK(dereg.rc == 0 && exit_seq < dereg.return_seq); + CHECK(hp_wait_count_at_least(h_bar, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_find_first(&barrier, h_bar, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(exit_seq < barrier.seq); + + step("free the callback's resources and keep events flowing"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_bar, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h_slow, 0, 0, NULL) == 1); + CHECK(hid_hotplug_deregister_callback(h_bar) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T15: register and deregister from within a callback: on its first */ +/* ARRIVED the parent registers a child callback with ENUMERATE (the */ +/* child must see the device exactly once, by snapshot or live delivery) */ +/* and deregisters itself. */ +static int t15_reentrant_registration(void) +{ + static parent_ctx ctx; /* static: zeroed, outlives any late invocation */ + hid_hotplug_callback_handle h_parent = 0, h_child = 0; + int child_rc, self_dereg_rc; + int invalid_rc, error_unchanged, nested, plug_rc, done; + wchar_t *saved_error; + size_t error_size; + hp_event parent_event, child_event; + + CHECK(ensure_absent() == 0); + hp_reset_log("T15"); + memset(&ctx, 0, sizeof(ctx)); + + step("register the parent callback"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_parent, &ctx, &h_parent) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, NULL, NULL, &h_child) == -1); + error_size = (wcslen(hid_error(NULL)) + 1) * sizeof(wchar_t); + saved_error = (wchar_t *)malloc(error_size); + CHECK(saved_error != NULL); + memcpy(saved_error, hid_error(NULL), error_size); + + step("plug: the parent registers the child and deregisters itself"); + plug_rc = test_virtual_device_replug(g_vdev); + done = hp_wait_flag(&ctx.done, EVENT_TIMEOUT_MS); + error_unchanged = (wcscmp(hid_error(NULL), saved_error) == 0); + free(saved_error); + CHECK(plug_rc == TEST_VDEV_OK && done == 0); + CHECK(error_unchanged); + + test_mutex_lock(&g_log_lock); + child_rc = ctx.child_rc; + h_child = ctx.child_handle; + self_dereg_rc = ctx.self_dereg_rc; + invalid_rc = ctx.invalid_dereg_rc; + test_mutex_unlock(&g_log_lock); + CHECK(child_rc == 0); + CHECK(h_child > 0); + CHECK(self_dereg_rc == 0); /* deregistering itself, mid-callback, works */ + CHECK(invalid_rc == -1); + + step("the child sees the device exactly once (snapshot or live delivery)"); + CHECK(hp_wait_count_at_least(h_child, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + step("unplug (barrier for the exactly-once assertion)"); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h_child, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h_child, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 1); + test_mutex_lock(&g_log_lock); + nested = ctx.child_nested; + test_mutex_unlock(&g_log_lock); + CHECK(!nested); + CHECK(hp_find_first(&parent_event, h_parent, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_find_first(&child_event, h_child, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + /* Both callbacks ran on HIDAPI's internal event context, never on the + registering (main) thread. The contract only promises that the context + is not the application's thread: the Windows backend delivers from a + threadpool / CM notification thread, so the two invocations may carry + different thread ids. */ + CHECK(parent_event.thread_id != g_main_tid); + CHECK(child_event.thread_id != g_main_tid); + + step("the parent saw only its one ARRIVED and its handle is dead"); + CHECK(hp_count(h_parent, 0, 0, NULL) == 1); + CHECK(hid_hotplug_deregister_callback(h_parent) == -1); + + CHECK(hid_hotplug_deregister_callback(h_child) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ + +/* T17: removing a later callback from inside dispatch cancels its turn. */ +typedef struct remove_ctx { + hid_hotplug_callback_handle other; + int rc; +} remove_ctx; + +static int HID_API_CALL cb_remove_other(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + remove_ctx *ctx = (remove_ctx *)user_data; + hp_record(handle, device, event); + if (event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT) { + hid_hotplug_callback_handle other; + int rc; + test_mutex_lock(&g_log_lock); + other = ctx->other; + test_mutex_unlock(&g_log_lock); + rc = hid_hotplug_deregister_callback(other); + test_mutex_lock(&g_log_lock); + ctx->rc = rc; + test_mutex_unlock(&g_log_lock); + } + return 0; +} + +static int t17_remove_other(void) +{ + hid_hotplug_callback_handle a = 0, b = 0, c = 0; + remove_ctx ctx = { 0, -2 }; + int rc; + CHECK(ensure_present() == 0); + hp_reset_log("T17"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, cb_remove_other, &ctx, &a) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, cb_log, NULL, &b) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, cb_log, NULL, &c) == 0); + test_mutex_lock(&g_log_lock); + ctx.other = b; + test_mutex_unlock(&g_log_lock); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(c, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + test_mutex_lock(&g_log_lock); + rc = ctx.rc; + test_mutex_unlock(&g_log_lock); + CHECK(rc == 0); + CHECK(hp_count(b, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, 0, NULL) == 0); + CHECK(hid_hotplug_deregister_callback(b) == -1); + CHECK(hid_hotplug_deregister_callback(a) == 0); + CHECK(hid_hotplug_deregister_callback(c) == 0); + return 0; +} + +/* T18: immediate deregistration cancels any still-pending snapshot work. + The log outlives user_data, so late calls are visible even without ASan. */ +static int HID_API_CALL cb_heap_record(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + int *count = (int *)user_data; + hp_record(handle, device, event); + test_mutex_lock(&g_log_lock); + (*count)++; + test_mutex_unlock(&g_log_lock); + return 0; +} + +static int t18_immediate_deregister(void) +{ + int i; + CHECK(ensure_present() == 0); + for (i = 0; i < 50; i++) { + hid_hotplug_callback_handle h = 0, barrier = 0; + int *count, before, rc; + hp_reset_log("T18"); + count = (int *)calloc(1, sizeof(*count)); + CHECK(count != NULL); + rc = hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_heap_record, count, &h); + if (rc == 0) + rc = hid_hotplug_deregister_callback(h); + if (rc == 0) + hp_mark_retired(h); + /* Retry cleanup only on failure; a second cancellation could mask a bug. */ + if (rc != 0) + hp_cleanup_callbacks(); + test_mutex_lock(&g_log_lock); + before = *count; + test_mutex_unlock(&g_log_lock); + free(count); + CHECK(rc == 0); + CHECK(before <= 1); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &barrier) == 0); + CHECK(hp_wait_count_at_least(barrier, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h, 0, 0, NULL) == before); + CHECK(hid_hotplug_deregister_callback(h) == -1); + hp_cleanup_callbacks(); + } + return 0; +} + +/* T11b: masks filter both synthetic and live events. */ +static int t11b_event_masks(void) +{ + hid_hotplug_callback_handle left = 0, arrived = 0, barrier = 0; + CHECK(ensure_present() == 0); + hp_reset_log("T11b"); + CHECK(hp_register(TEST_VID, TEST_PID, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + HID_API_HOTPLUG_ENUMERATE, cb_log, NULL, &left) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + 0, cb_log, NULL, &arrived) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, cb_log, NULL, &barrier) == 0); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(left, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_wait_count_at_least(barrier, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(left, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, 0, NULL) == 0); + CHECK(hp_count(left, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, 0, NULL) == 1); + CHECK(hp_count(arrived, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, 0, NULL) == 0); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(barrier, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(arrived, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + CHECK(hp_count(left, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, 0, NULL) == 0); + CHECK(hid_hotplug_deregister_callback(left) == 0); + CHECK(hid_hotplug_deregister_callback(arrived) == 0); + CHECK(hid_hotplug_deregister_callback(barrier) == 0); + return 0; +} + +/* Registration may wait for an already-running snapshot callback. */ +typedef struct register_ctx { + hid_hotplug_callback_fn callback; + void *user_data; + hid_hotplug_callback_handle handle; + int rc; +} register_ctx; + +static void register_thread(void *arg) +{ + register_ctx *ctx = (register_ctx *)arg; + ctx->rc = hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + ctx->callback, ctx->user_data, &ctx->handle); +} + +/* T9b: force the disconnect to happen while the initial pass is active. */ +static int t9b_parked_snapshot(void) +{ + slow_ctx gate = { 0 }; + register_ctx reg = { cb_slow, &gate, 0, -2 }; + test_thread thread; + hp_event arrived, left; + int entered, unplug_rc = TEST_VDEV_ERROR, expired; + CHECK(ensure_present() == 0); + hp_reset_log("T9b"); + CHECK(test_thread_start(&thread, register_thread, ®) == 0); + entered = hp_wait_flag(&gate.entered, EVENT_TIMEOUT_MS); + if (entered == 0) + unplug_rc = test_virtual_device_unplug(g_vdev); + test_mutex_lock(&g_log_lock); + gate.release = 1; + test_mutex_unlock(&g_log_lock); + hp_join_or_exit(&thread); + CHECK(entered == 0 && unplug_rc == TEST_VDEV_OK && reg.rc == 0); + CHECK(hp_wait_count_at_least(reg.handle, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hid_hotplug_deregister_callback(reg.handle) == 0); + test_mutex_lock(&g_log_lock); + expired = gate.expired; + test_mutex_unlock(&g_log_lock); + CHECK(!expired); + CHECK(hp_find_first(&arrived, reg.handle, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL) == 0); + CHECK(hp_find_first(&left, reg.handle, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL) == 0); + CHECK(arrived.seq < left.seq && strcmp(arrived.path, left.path) == 0); + CHECK(hp_count(reg.handle, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + return 0; +} + +/* T8c uses the callback parameter, including before register returns. */ +static int HID_API_CALL cb_remove_self(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + int *result = (int *)user_data; + int rc; + hp_record(handle, device, event); + rc = hid_hotplug_deregister_callback(handle); + test_mutex_lock(&g_log_lock); + *result = rc; + test_mutex_unlock(&g_log_lock); + return 0; +} + +static int t8c_snapshot_self_deregister(void) +{ + hid_hotplug_callback_handle h = 0, barrier = 0; + int result = -2, rc; + CHECK(ensure_present() == 0); + hp_reset_log("T8c"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_remove_self, &result, &h) == 0); + /* Await its own event first: snapshots of different registrations have + no cross-registration ordering guarantee. */ + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &barrier) == 0); + CHECK(hp_wait_count_at_least(barrier, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + test_mutex_lock(&g_log_lock); + rc = result; + test_mutex_unlock(&g_log_lock); + CHECK(rc == 0 && hp_count(h, 0, 0, NULL) == 1); + CHECK(hid_hotplug_deregister_callback(h) == -1); + CHECK(hid_hotplug_deregister_callback(barrier) == 0); + return 0; +} + +/* T18b publishes a child with queued ENUMERATE work while the parent holds + the event context. A second application thread then cancels that child. */ +typedef struct queued_ctx { + slow_ctx gate; + int *count; + hid_hotplug_callback_handle child; + int child_rc; +} queued_ctx; + +static int HID_API_CALL cb_queue_child(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + queued_ctx *ctx = (queued_ctx *)user_data; + hid_hotplug_callback_handle child = 0; + int rc = hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_heap_record, ctx->count, &child); + test_mutex_lock(&g_log_lock); + ctx->child = child; + ctx->child_rc = rc; + test_mutex_unlock(&g_log_lock); + cb_slow(handle, device, event, &ctx->gate); + return 1; +} + +static int t18b_queued_deregister(void) +{ + queued_ctx ctx = { 0 }; + register_ctx reg = { cb_queue_child, &ctx, 0, -2 }; + deregister_ctx dereg = { 0, 0, 0, -2, 0 }; + test_thread registration, cancellation; + hid_hotplug_callback_handle barrier = 0; + int entered, worker_started = 0, started = -1, before, expired, child_rc; + CHECK(ensure_present() == 0); + hp_reset_log("T18b"); + ctx.count = (int *)calloc(1, sizeof(*ctx.count)); + CHECK(ctx.count != NULL); + if (test_thread_start(®istration, register_thread, ®) != 0) { + free(ctx.count); + CHECK(!"failed to start snapshot registration"); + } + entered = hp_wait_flag(&ctx.gate.entered, EVENT_TIMEOUT_MS); + test_mutex_lock(&g_log_lock); + dereg.handle = ctx.child; + child_rc = ctx.child_rc; + test_mutex_unlock(&g_log_lock); + if (entered == 0 && child_rc == 0) { + worker_started = (test_thread_start(&cancellation, deregister_thread, &dereg) == 0); + if (worker_started) + started = hp_wait_flag(&dereg.started, EVENT_TIMEOUT_MS); + } + test_mutex_lock(&g_log_lock); + ctx.gate.release = 1; + test_mutex_unlock(&g_log_lock); + hp_join_or_exit(®istration); + if (worker_started) + hp_join_or_exit(&cancellation); + if (!worker_started || dereg.rc != 0) + hp_cleanup_callbacks(); + test_mutex_lock(&g_log_lock); + before = *ctx.count; + expired = ctx.gate.expired; + test_mutex_unlock(&g_log_lock); + free(ctx.count); + CHECK(entered == 0 && !expired && reg.rc == 0 && child_rc == 0); + CHECK(worker_started && started == 0 && dereg.rc == 0 && before <= 1); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &barrier) == 0); + CHECK(hp_wait_count_at_least(barrier, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(dereg.handle, 0, 0, NULL) == before); + CHECK(hid_hotplug_deregister_callback(dereg.handle) == -1); + CHECK(hid_hotplug_deregister_callback(barrier) == 0); + return 0; +} + +/* T19: exit with a gated ENUMERATE callback, then reinitialize and deliver + again. Old callback state stays alive across every initialization lifetime. */ +typedef struct exit_ctx { + slow_ctx gate; + int exiting, returned, late, count; +} exit_ctx; + +static int HID_API_CALL cb_exit_record(hid_hotplug_callback_handle handle, + struct hid_device_info *device, + hid_hotplug_event event, void *user_data) +{ + exit_ctx *ctx = (exit_ctx *)user_data; + cb_slow(handle, device, event, &ctx->gate); + test_mutex_lock(&g_log_lock); + ctx->count++; + if (ctx->returned) + ctx->late = 1; + test_mutex_unlock(&g_log_lock); + return 0; +} + +static void release_exit_thread(void *arg) +{ + exit_ctx *ctx = (exit_ctx *)arg; + if (hp_wait_flag(&ctx->exiting, EVENT_TIMEOUT_MS) == 0) { + long long deadline = test_now_ms() + 100; + /* Keep the callback parked during the exit attempt, as in T14. */ + do { + int returned; + test_mutex_lock(&g_log_lock); + returned = ctx->returned; + test_mutex_unlock(&g_log_lock); + if (returned) + break; + test_sleep_ms(WAIT_TICK_MS); + } while (test_now_ms() < deadline); + } + test_mutex_lock(&g_log_lock); + ctx->gate.release = 1; + test_mutex_unlock(&g_log_lock); +} + +static int t19_pending_exit(void) +{ + static exit_ctx ctxs[10]; + int i, late = 0, count = 0; + CHECK(ensure_present() == 0); + for (i = 0; i < 10; i++) { + hid_hotplug_callback_handle h = 0, barrier = 0; + test_thread release; + int entered, started, exit_rc, exited, expired, post_count = 0; + hp_reset_log("T19"); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_exit_record, &ctxs[i], &h) == 0); + entered = hp_wait_flag(&ctxs[i].gate.entered, EVENT_TIMEOUT_MS); + started = test_thread_start(&release, release_exit_thread, &ctxs[i]); + if (started != 0) { + test_mutex_lock(&g_log_lock); + ctxs[i].gate.release = 1; + test_mutex_unlock(&g_log_lock); + CHECK(started == 0); + } + /* Lifecycle calls stay on the initializing thread, including on macOS. */ + test_mutex_lock(&g_log_lock); + ctxs[i].exiting = 1; + test_mutex_unlock(&g_log_lock); + exit_rc = hid_exit(); + test_mutex_lock(&g_log_lock); + ctxs[i].returned = 1; + exited = ctxs[i].gate.exited; + expired = ctxs[i].gate.expired; + /* Handles need not remain unique across initialization lifetimes. */ + g_handle_count = 0; + test_mutex_unlock(&g_log_lock); + hp_join_or_exit(&release); + CHECK(entered == 0 && !expired && exited && exit_rc == 0); + CHECK(hid_init() == 0); + CHECK(hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_heap_record, &post_count, &barrier) == 0); + CHECK(hp_wait_flag(&post_count, EVENT_TIMEOUT_MS) == 0); + hp_cleanup_callbacks(); + } + test_mutex_lock(&g_log_lock); + for (i = 0; i < 10; i++) { + late |= ctxs[i].late; + count += ctxs[i].count; + } + test_mutex_unlock(&g_log_lock); + printf(" pending-exit callback invocations: %d\n", count); + CHECK(!late); + return 0; +} + +/* Optional arrival-versus-snapshot race; HIDAPI lifecycle calls remain on + main, and the provider is used by only one thread at a time. */ +static int t20_arrival_stress(void) +{ + int i; + for (i = 0; i < 25; i++) { + test_thread plug; + hid_hotplug_callback_handle h = 0; + int plug_rc = TEST_VDEV_ERROR, register_rc; + CHECK(ensure_absent() == 0); + hp_reset_log("T20"); + CHECK(test_thread_start(&plug, replug_thread, &plug_rc) == 0); + register_rc = hp_register(TEST_VID, TEST_PID, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h); + hp_join_or_exit(&plug); + CHECK(register_rc == 0 && plug_rc == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(test_virtual_device_unplug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_count_at_least(h, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + CHECK(hp_count(h, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, TEST_PID, TEST_SERIAL) == 1); + CHECK(hid_hotplug_deregister_callback(h) == 0); + } + return 0; +} + +#define RUN_TEST(name, call) do { \ + report(name, call); \ + if (test_atomic_load(&g_deadline_failed)) goto done; \ +} while (0) + +int main(void) +{ + int rc; + hid_hotplug_callback_handle probe = 0; + + g_main_tid = test_thread_id(); + test_mutex_init(&g_log_lock); + + if (hid_init() != 0) { + printf("hid_init() failed\n"); + test_mutex_destroy(&g_log_lock); + return EXIT_FAILURE; + } + + step("probe hotplug support"); + if (hp_register(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &probe) != 0) { + printf("hotplug reported unsupported here - skipping\n"); + hid_exit(); + test_mutex_destroy(&g_log_lock); + return EXIT_SKIP; + } + (void)hid_hotplug_deregister_callback(probe); + + step("create virtual device"); + rc = test_virtual_device_create(&g_vdev, TEST_VID, TEST_PID, TEST_SERIAL); + if (rc == TEST_VDEV_UNAVAILABLE) { + printf("virtual device unavailable on this host - skipping\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + test_mutex_destroy(&g_log_lock); + return EXIT_SKIP; + } + if (rc != TEST_VDEV_OK || !g_vdev) { + printf("failed to create virtual device (rc=%d)\n", rc); + test_virtual_device_destroy(g_vdev); + hid_exit(); + test_mutex_destroy(&g_log_lock); + return EXIT_FAILURE; + } + + step("wait for initial device presence"); + if (hp_wait_enumerated(TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) != 0) { + printf("virtual device did not initially enumerate - skipping\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + test_mutex_destroy(&g_log_lock); + return EXIT_SKIP; + } + + step("probe unplug support"); + rc = test_virtual_device_unplug(g_vdev); + if (rc == TEST_VDEV_UNAVAILABLE) { + printf("this provider cannot toggle device presence - skipping\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + test_mutex_destroy(&g_log_lock); + return EXIT_SKIP; + } + if (rc != TEST_VDEV_OK) { + printf("unplug probe failed (rc=%d)\n", rc); + goto probe_failed; + } + if (hp_wait_enumerated(TEST_PID, TEST_SERIAL, 0, EVENT_TIMEOUT_MS) != 0) { + printf("device remained present after unplug probe\n"); + goto probe_failed; + } + rc = test_virtual_device_replug(g_vdev); + if (rc != TEST_VDEV_OK) { + printf("replug probe failed (rc=%d)\n", rc); + goto probe_failed; + } + if (hp_wait_enumerated(TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) != 0) { + printf("device did not reappear after successful unplug/replug\n"); + goto probe_failed; + } + + printf("running hotplug tests...\n"); + fflush(stdout); + + printf("T6: asynchronous delivery + handle parameter\n"); + RUN_TEST("T6 async_delivery", t6_async_delivery()); + printf("T7: exactly-once (ENUMERATE pass vs live events)\n"); + RUN_TEST("T7 exactly_once", t7_exactly_once()); + printf("T8a: non-zero callback return deregisters\n"); + RUN_TEST("T8a return_deregisters", t8a_return_deregisters(1)); + RUN_TEST("T8a negative_return_deregisters", t8a_return_deregisters(-1)); + printf("T8b: non-zero return stops the rest of the ENUMERATE pass\n"); + RUN_TEST("T8b return_stops_pass", t8b_return_stops_pass(1)); + RUN_TEST("T8b negative_return_stops_pass", t8b_return_stops_pass(-1)); + printf("T9: ENUMERATE pass delivered before live events\n"); + RUN_TEST("T9 pass_before_live", t9_pass_before_live()); + printf("T10: live ARRIVED/LEFT payloads\n"); + RUN_TEST("T10 live_payloads", t10_live_payloads()); + printf("T11: LEFT without ENUMERATE (zero-window proof)\n"); + RUN_TEST("T11 left_without_enumerate", t11_left_without_enumerate()); + printf("T12: VID/PID filtering\n"); + RUN_TEST("T12 vid_pid_filtering", t12_vid_pid_filtering()); + printf("T13: dispatch in registration order\n"); + RUN_TEST("T13 dispatch_order", t13_dispatch_order()); + printf("T14: deregistration post-condition\n"); + RUN_TEST("T14 deregister_postcondition", t14_deregister_postcondition()); + printf("T15: register/deregister from within a callback\n"); + RUN_TEST("T15 reentrant_registration", t15_reentrant_registration()); + RUN_TEST("T11b event_masks", t11b_event_masks()); + RUN_TEST("T17 remove_other", t17_remove_other()); + RUN_TEST("T18 immediate_deregister", t18_immediate_deregister()); + RUN_TEST("T8c snapshot_self_deregister", t8c_snapshot_self_deregister()); + RUN_TEST("T9b parked_snapshot", t9b_parked_snapshot()); + RUN_TEST("T18b queued_deregister", t18b_queued_deregister()); + RUN_TEST("T19 pending_exit", t19_pending_exit()); + if (hp_env_set("HIDAPI_HOTPLUG_STRESS")) + RUN_TEST("T20 arrival_stress", t20_arrival_stress()); + +done: + hp_reset_log("(final sweep)"); /* global invariants over the last test */ + + test_virtual_device_destroy(g_vdev); + hid_exit(); + test_mutex_destroy(&g_log_lock); + + printf("%s hotplug (%d failed checks, %d skipped scenarios)\n", + g_failures == 0 ? "PASS" : "FAIL", g_failures, g_skipped); + return (g_failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; + +probe_failed: + test_virtual_device_destroy(g_vdev); + hid_exit(); + test_mutex_destroy(&g_log_lock); + return EXIT_FAILURE; +} diff --git a/src/tests/test_hotplug_api.c b/src/tests/test_hotplug_api.c new file mode 100644 index 000000000..8a823ef47 --- /dev/null +++ b/src/tests/test_hotplug_api.c @@ -0,0 +1,505 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Tier-1 hotplug API tests: argument validation, callback-handle + properties, implicit initialization, hid_exit() teardown and + register/deregister thread-safety. + + These tests need NO device (virtual or real) and no privileges, + so they run against every backend in the ordinary CI matrix. + They only exercise the parts of the hotplug contract that are + observable without a device event; the device-backed scenarios + live in test_hotplug.c. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +#include +#include +#include +#include +#include + +#include + +#include "test_platform.h" + +/* CTest treats this exit code as "skipped" (see SKIP_RETURN_CODE in CMake). */ +#define EXIT_SKIP 77 + +#define ALL_EVENTS (HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT) + +/* Tier-1 runs with no device churn, so ABSENCE of callback invocations is + checked with a short bounded settle window (there is no event to use as a + barrier when the expectation is "no events at all"). */ +#define SETTLE_MS 1000 + +/* How long the two churn threads of T16 keep registering/deregistering. */ +#define CHURN_MS 2000 + +static int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf(" CHECK failed: %s (line %d)\n", \ + #cond, __LINE__); \ + fflush(stdout); \ + g_failures++; \ + return -1; \ + } \ + } while (0) + +/* Print a flushed progress marker so a hang is localised on a CTest timeout. */ +static void step(const char *what) +{ + printf(" -> %s\n", what); + fflush(stdout); +} + +static void report(const char *name, int rc) +{ + printf("%s %s\n", rc == 0 ? "PASS" : "FAIL", name); + fflush(stdout); +} + +/* ------------------------------------------------------------------ */ +/* Shared callback state */ + +static test_mutex g_lock; +static int g_cb_invocations; /* every invocation of cb_record */ +static int g_exit_returned; /* set by T5 right after hid_exit() returns */ +static int g_fired_after_exit; /* cb_record ran after g_exit_returned was set */ + +static int HID_API_CALL cb_record(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, + void *user_data) +{ + (void)callback_handle; + (void)device; + (void)event; + (void)user_data; + test_mutex_lock(&g_lock); + g_cb_invocations++; + if (g_exit_returned) + g_fired_after_exit = 1; + test_mutex_unlock(&g_lock); + return 0; +} + +static int HID_API_CALL cb_noop(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, + void *user_data) +{ + (void)callback_handle; + (void)device; + (void)event; + (void)user_data; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T4 doubles as the support probe: hid_hotplug_register_callback() as + the very FIRST library call must initialize the library implicitly + and succeed. If it still fails after an explicit hid_init(), this + backend/host has a hotplug capability or setup failure and the whole + test is skipped: the libusb backend checks the capability before + validating arguments, so not even T1 is meaningful without support. */ +static int t4_implicit_init_probe(int *supported) +{ + hid_hotplug_callback_handle handle = -123; + const wchar_t *error; + int rc; + + *supported = 0; + + step("register as the very first library call"); + rc = hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &handle); + if (rc != 0) { + error = hid_error(NULL); + printf(" first registration failed (rc=%d): %ls; retry after hid_init()\n", + rc, error != NULL ? error : L"(no error string)"); + fflush(stdout); + if (hid_init() != 0) { + error = hid_error(NULL); + printf(" hid_init() failed after the first registration failure: %ls\n", + error != NULL ? error : L"(no error string)"); + fflush(stdout); + g_failures++; + return -1; + } + handle = -123; + rc = hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &handle); + if (rc == 0) { + printf(" registration succeeded after explicit initialization\n"); + fflush(stdout); + *supported = 1; + CHECK(hid_hotplug_deregister_callback(handle) == 0); + g_failures++; + return -1; + } + error = hid_error(NULL); + printf(" registration still failed after explicit initialization (rc=%d): %ls - skipping\n", + rc, error != NULL ? error : L"(no error string)"); + fflush(stdout); + return 0; + } + *supported = 1; + + CHECK(handle > 0); + step("deregister the probe callback"); + CHECK(hid_hotplug_deregister_callback(handle) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T1: invalid registration arguments -> -1, *callback_handle zeroed, + and a global error string different from the no-error baseline. The + exact error text is backend-specific. */ + +static wchar_t *copy_global_error(void) +{ + const wchar_t *error = hid_error(NULL); + size_t length; + wchar_t *copy; + + if (error == NULL) + return NULL; + length = wcslen(error); + copy = (wchar_t *)malloc((length + 1) * sizeof(*copy)); + if (copy != NULL) + memcpy(copy, error, (length + 1) * sizeof(*copy)); + return copy; +} + +static int t1_check_invalid(unsigned short vid, unsigned short pid, + int events, int flags, hid_hotplug_callback_fn cb) +{ + hid_hotplug_callback_handle handle = 12345; /* poisoned: must be zeroed */ + wchar_t *baseline; + const wchar_t *error; + int rc, valid; + + CHECK(hid_init() == 0); + baseline = copy_global_error(); + CHECK(baseline != NULL); + rc = hid_hotplug_register_callback(vid, pid, events, flags, cb, NULL, &handle); + error = hid_error(NULL); + valid = rc == -1 && handle == 0 && error != NULL && wcscmp(error, baseline) != 0; + free(baseline); + CHECK(valid); + return 0; +} + +static int t1_arg_validation(void) +{ + step("NULL callback"); + if (t1_check_invalid(0, 0, ALL_EVENTS, 0, NULL) != 0) + return -1; + + step("events == 0"); + if (t1_check_invalid(0, 0, 0, 0, cb_noop) != 0) + return -1; + + step("unknown events bits"); + if (t1_check_invalid(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | (1 << 10), 0, cb_noop) != 0) + return -1; + + step("unknown flags bits"); + if (t1_check_invalid(0, 0, ALL_EVENTS, (1 << 10), cb_noop) != 0) + return -1; + + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T2: handles are positive, never 0, and not reused while the library + remains initialized (a later registration gets a different handle; + hidapi.h promises uniqueness, not monotonicity). */ +static int t2_handle_properties(hid_hotplug_callback_handle *out_stale) +{ + hid_hotplug_callback_handle h1 = 0, h2 = 0; + + step("register/deregister twice"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &h1) == 0); + CHECK(h1 > 0); + CHECK(hid_hotplug_deregister_callback(h1) == 0); + + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &h2) == 0); + CHECK(h2 > 0); + CHECK(h2 != h1); /* handles are not reused while initialized */ + CHECK(hid_hotplug_deregister_callback(h2) == 0); + + *out_stale = h2; /* a genuine but no-longer-registered handle for T3 */ + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T3: deregistering 0, negative, not-registered and already-deregistered + handles fails with -1, sets an error string and leaves a + still-registered callback untouched. */ +static int t3_check_invalid(hid_hotplug_callback_handle handle) +{ + wchar_t *baseline; + const wchar_t *error; + int rc, valid; + + CHECK(hid_init() == 0); + baseline = copy_global_error(); + CHECK(baseline != NULL); + rc = hid_hotplug_deregister_callback(handle); + error = hid_error(NULL); + valid = rc == -1 && error != NULL && wcscmp(error, baseline) != 0; + free(baseline); + CHECK(valid); + return 0; +} + +static int t3_stale_handles(hid_hotplug_callback_handle stale) +{ + hid_hotplug_callback_handle live = 0; + hid_hotplug_callback_handle unregistered; + + step("register a live callback"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &live) == 0); + CHECK(live > 0); + unregistered = live == INT_MAX ? INT_MAX - 1 : INT_MAX; + + step("deregister invalid handles"); + if (t3_check_invalid(0) != 0) + return -1; + if (t3_check_invalid(-1) != 0) + return -1; + if (t3_check_invalid(unregistered) != 0) /* not a registered handle */ + return -1; + if (t3_check_invalid(stale) != 0) /* already deregistered */ + return -1; + + step("the live callback is unaffected"); + CHECK(hid_hotplug_deregister_callback(live) == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T19: callback_handle is optional for a successful registration. */ +static int t19_null_output_handle(void) +{ + int user_data = 0; + + step("register with a NULL callback-handle pointer"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, + cb_noop, &user_data, NULL) == 0); + step("clean up the anonymous registration with hid_exit()"); + CHECK(hid_exit() == 0); + CHECK(hid_init() == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T5: hid_exit() with callbacks still registered returns (a hang is + caught by the CTest timeout), invalidates the handles, and no + callback fires after it returned. Then the register->immediate-exit + teardown race is stressed in a loop. */ +static int t5_hid_exit_teardown(void) +{ + hid_hotplug_callback_handle ha = 0, hb = 0; + int i; + + step("register two callbacks"); + test_mutex_lock(&g_lock); + g_exit_returned = 0; + g_fired_after_exit = 0; + test_mutex_unlock(&g_lock); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_record, NULL, &ha) == 0); + test_mutex_lock(&g_lock); + g_exit_returned = 0; + test_mutex_unlock(&g_lock); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_record, NULL, &hb) == 0); + + step("hid_exit() with callbacks still registered"); + CHECK(hid_exit() == 0); + test_mutex_lock(&g_lock); + g_exit_returned = 1; + test_mutex_unlock(&g_lock); + CHECK(hid_hotplug_deregister_callback(ha) == -1); + + step("old handles are invalid after re-init"); + CHECK(hid_init() == 0); + CHECK(hid_hotplug_deregister_callback(ha) == -1); + CHECK(hid_hotplug_deregister_callback(hb) == -1); + + step("no callback fires after hid_exit returned (settle window)"); + test_sleep_ms(SETTLE_MS); + test_mutex_lock(&g_lock); + i = g_fired_after_exit; + printf(" callback invocations: %d\n", g_cb_invocations); + fflush(stdout); + test_mutex_unlock(&g_lock); + CHECK(i == 0); + + step("register -> immediate hid_exit stress loop"); + for (i = 0; i < 50; i++) { + hid_hotplug_callback_handle h = 0; + test_mutex_lock(&g_lock); + g_exit_returned = 0; + test_mutex_unlock(&g_lock); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, HID_API_HOTPLUG_ENUMERATE, + cb_record, NULL, &h) == 0); + CHECK(h > 0); + CHECK(hid_exit() == 0); + test_mutex_lock(&g_lock); + g_exit_returned = 1; + test_mutex_unlock(&g_lock); + } + test_mutex_lock(&g_lock); + i = g_fired_after_exit; + printf(" callback invocations: %d\n", g_cb_invocations); + fflush(stdout); + test_mutex_unlock(&g_lock); + CHECK(i == 0); + CHECK(hid_init() == 0); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* T16: two threads register/deregister wildcard callbacks concurrently + (the hotplug API is documented thread-safe). Pass = no crash, no + hang (join timeout), no failed call. The threads never call + hid_error(NULL): the global error string is the one part of the + hotplug API the application must serialize itself. */ + +typedef struct churn_ctx { + test_atomic_int stop; + long iterations; + long failures; +} churn_ctx; + +static void churn_thread_fn(void *arg) +{ + churn_ctx *ctx = (churn_ctx *)arg; + + while (!test_atomic_load(&ctx->stop)) { + hid_hotplug_callback_handle h = 0; + if (hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &h) != 0) { + ctx->failures++; + continue; + } + if (h <= 0) + ctx->failures++; + if (hid_hotplug_deregister_callback(h) != 0) + ctx->failures++; + ctx->iterations++; + } +} + +static void join_churn_thread_or_abort(test_thread *thread, int timeout_ms) +{ + if (test_thread_join_timeout(thread, timeout_ms) != 0) { + printf(" churn thread did not stop within %d ms\n", timeout_ms); + fflush(stdout); + abort(); + } +} + +static int t16_thread_churn(void) +{ + test_thread threads[2]; + churn_ctx ctxs[2]; + int i; + + memset(ctxs, 0, sizeof(ctxs)); + + step("start two register/deregister churn threads"); + CHECK(test_thread_start(&threads[0], churn_thread_fn, &ctxs[0]) == 0); + if (test_thread_start(&threads[1], churn_thread_fn, &ctxs[1]) != 0) { + test_atomic_store(&ctxs[0].stop, 1); + join_churn_thread_or_abort(&threads[0], 10000); + CHECK(!"failed to start the second churn thread"); + } + + test_sleep_ms(CHURN_MS); + test_atomic_store(&ctxs[0].stop, 1); + test_atomic_store(&ctxs[1].stop, 1); + + step("join the churn threads"); + join_churn_thread_or_abort(&threads[0], 30000); + join_churn_thread_or_abort(&threads[1], 30000); + + for (i = 0; i < 2; i++) { + printf(" thread %d: %ld iterations, %ld failures\n", + i, ctxs[i].iterations, ctxs[i].failures); + fflush(stdout); + CHECK(ctxs[i].failures == 0); + CHECK(ctxs[i].iterations > 0); + } + return 0; +} + +/* ------------------------------------------------------------------ */ + +int main(void) +{ + hid_hotplug_callback_handle stale = 0; + int supported = 0; + int rc; + + test_mutex_init(&g_lock); + + /* NOTE: no hid_init() here on purpose: T4 requires that the hotplug + registration is the very first library call. */ + + printf("running hotplug API tests...\n"); + fflush(stdout); + + printf("T4: implicit init (register as first library call)\n"); + fflush(stdout); + rc = t4_implicit_init_probe(&supported); + report("T4 implicit_init", rc); + if (!supported) { + hid_exit(); + test_mutex_destroy(&g_lock); + return rc == 0 ? EXIT_SKIP : EXIT_FAILURE; + } + + printf("T1: registration argument validation\n"); + fflush(stdout); + report("T1 arg_validation", t1_arg_validation()); + + printf("T2: callback handle properties\n"); + fflush(stdout); + report("T2 handle_properties", t2_handle_properties(&stale)); + + printf("T3: stale/unknown handle deregistration\n"); + fflush(stdout); + report("T3 stale_handles", t3_stale_handles(stale)); + + printf("T19: NULL callback-handle output\n"); + fflush(stdout); + report("T19 null_output_handle", t19_null_output_handle()); + + printf("T5: hid_exit teardown with registered callbacks\n"); + fflush(stdout); + report("T5 hid_exit_teardown", t5_hid_exit_teardown()); + + printf("T16: register/deregister thread churn\n"); + fflush(stdout); + report("T16 thread_churn", t16_thread_churn()); + + hid_exit(); + test_mutex_destroy(&g_lock); + + printf("%s hotplug_api (%d failed checks)\n", + g_failures == 0 ? "PASS" : "FAIL", g_failures); + return (g_failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/tests/test_hotplug_mac.c b/src/tests/test_hotplug_mac.c new file mode 100644 index 000000000..807feffb5 --- /dev/null +++ b/src/tests/test_hotplug_mac.c @@ -0,0 +1,320 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + macOS hotplug lifecycle regression tests. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +#include +#include +#include +#include +#include +#include + +#include + +#define WORKERS 4 +#define ITERATIONS 8 +#define CHECK(cond) do { \ + if (!(cond)) { \ + fprintf(stderr, "CHECK failed: %s (line %d)\n", #cond, __LINE__); \ + exit(EXIT_FAILURE); \ + } \ +} while (0) + +static struct timespec deadline_ms(int ms) +{ + struct timespec deadline; + CHECK(clock_gettime(CLOCK_REALTIME, &deadline) == 0); + deadline.tv_sec += ms / 1000; + deadline.tv_nsec += (long)(ms % 1000) * 1000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000L; + } + return deadline; +} + +/* All condition waits and API calls are bounded, including pthread_join: + alarm() terminates the standalone test if a library call deadlocks. */ +static void wait_flag(pthread_cond_t *cond, pthread_mutex_t *mutex, const int *flag) +{ + struct timespec deadline = deadline_ms(5000); + while (!*flag) + CHECK(pthread_cond_timedwait(cond, mutex, &deadline) == 0); +} + +static int HID_API_CALL keep_callback(hid_hotplug_callback_handle handle, + struct hid_device_info *device, hid_hotplug_event event, void *user_data) +{ + (void)handle; + (void)device; + (void)event; + (void)user_data; + return 0; +} + +static hid_hotplug_callback_handle register_quiet(void) +{ + hid_hotplug_callback_handle handle = 0; + CHECK(hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + 0, keep_callback, NULL, &handle) == 0); + CHECK(handle > 0); + return handle; +} + +struct worker_gate { + pthread_mutex_t mutex; + pthread_cond_t cond; + int arrived; + int generation; +}; + +static void wait_workers(struct worker_gate *gate) +{ + struct timespec deadline = deadline_ms(5000); + int generation; + CHECK(pthread_mutex_lock(&gate->mutex) == 0); + generation = gate->generation; + if (++gate->arrived == WORKERS) { + gate->arrived = 0; + gate->generation++; + CHECK(pthread_cond_broadcast(&gate->cond) == 0); + } else { + while (generation == gate->generation) + CHECK(pthread_cond_timedwait(&gate->cond, &gate->mutex, &deadline) == 0); + } + CHECK(pthread_mutex_unlock(&gate->mutex) == 0); +} + +static void *registration_worker(void *arg) +{ + struct worker_gate *gate = (struct worker_gate *)arg; + int i; + for (i = 0; i < ITERATIONS; i++) { + hid_hotplug_callback_handle handle; + wait_workers(gate); + handle = register_quiet(); + wait_workers(gate); + CHECK(hid_hotplug_deregister_callback(handle) == 0); + } + return NULL; +} + +static void test_concurrent_registration(void) +{ + struct worker_gate gate = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, 0, 0 }; + pthread_t workers[WORKERS]; + int i; + puts("Concurrent register/deregister and competing collectors"); + for (i = 0; i < WORKERS; i++) + CHECK(pthread_create(&workers[i], NULL, registration_worker, &gate) == 0); + for (i = 0; i < WORKERS; i++) + CHECK(pthread_join(workers[i], NULL) == 0); + CHECK(pthread_cond_destroy(&gate.cond) == 0); + CHECK(pthread_mutex_destroy(&gate.mutex) == 0); +} + +enum callback_action { REMOVE_BY_RETURN, REMOVE_EXPLICITLY, WAIT_FOR_RELEASE, REMOVE_THEN_EXIT, WAIT_FOR_APP_MUTEX }; + +static pthread_key_t callback_key; + +struct callback_state { + pthread_mutex_t mutex; + pthread_mutex_t api_mutex; + pthread_cond_t cond; + enum callback_action action; + int entered; + int release; + int completed; + int deregister_started; + int deregister_done; + int destructor_completed; + hid_hotplug_callback_handle handle; +}; + +static void event_thread_destructor(void *arg) +{ + struct callback_state *state = (struct callback_state *)arg; + struct timespec delay = { 0, 100000000L }; + hid_hotplug_callback_handle handle = -1; + /* Resource release alone must not let a collector skip this destructor. */ + while (nanosleep(&delay, &delay) != 0) + CHECK(errno == EINTR); + CHECK(hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + 0, keep_callback, NULL, &handle) == -1); + CHECK(handle == 0); + CHECK(pthread_mutex_lock(&state->mutex) == 0); + state->destructor_completed = 1; + CHECK(pthread_mutex_unlock(&state->mutex) == 0); +} + +static int HID_API_CALL lifecycle_callback(hid_hotplug_callback_handle handle, + struct hid_device_info *device, hid_hotplug_event event, void *user_data) +{ + struct callback_state *state = (struct callback_state *)user_data; + (void)device; + CHECK(event == HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); + if (state->action != WAIT_FOR_RELEASE) + CHECK(pthread_setspecific(callback_key, state) == 0); + if (state->action == REMOVE_EXPLICITLY) + CHECK(hid_hotplug_deregister_callback(handle) == 0); + CHECK(pthread_mutex_lock(&state->mutex) == 0); + if (state->action != WAIT_FOR_RELEASE) + CHECK(!state->entered); + state->entered = 1; + CHECK(pthread_cond_broadcast(&state->cond) == 0); + if (state->action == WAIT_FOR_RELEASE) + wait_flag(&state->cond, &state->mutex, &state->release); + state->completed = 1; + CHECK(pthread_mutex_unlock(&state->mutex) == 0); + return state->action == REMOVE_BY_RETURN || state->action == REMOVE_THEN_EXIT; +} + +static int HID_API_CALL serialized_callback(hid_hotplug_callback_handle handle, + struct hid_device_info *device, hid_hotplug_event event, void *user_data) +{ + struct callback_state *state = (struct callback_state *)user_data; + (void)handle; + (void)device; + CHECK(event == HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); + CHECK(pthread_mutex_lock(&state->mutex) == 0); + state->entered = 1; + CHECK(pthread_cond_broadcast(&state->cond) == 0); + CHECK(pthread_mutex_unlock(&state->mutex) == 0); + /* The owner holds api_mutex across hid_init(); this callback already holds + HIDAPI's hotplug mutex. General API calls are serialized by api_mutex. */ + CHECK(pthread_mutex_lock(&state->api_mutex) == 0); + hid_close(NULL); + CHECK(pthread_mutex_unlock(&state->api_mutex) == 0); + return 0; +} + +static void *deregister_worker(void *arg) +{ + struct callback_state *state = (struct callback_state *)arg; + CHECK(pthread_mutex_lock(&state->mutex) == 0); + state->deregister_started = 1; + CHECK(pthread_cond_broadcast(&state->cond) == 0); + CHECK(pthread_mutex_unlock(&state->mutex) == 0); + CHECK(hid_hotplug_deregister_callback(state->handle) == 0); + CHECK(pthread_mutex_lock(&state->mutex) == 0); + CHECK(state->release && state->completed); + state->deregister_done = 1; + CHECK(pthread_cond_broadcast(&state->cond) == 0); + CHECK(pthread_mutex_unlock(&state->mutex) == 0); + return NULL; +} + +static void test_device_callback(enum callback_action action, const char *name) +{ + struct callback_state state = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, + action, 0, 0, 0, 0, 0, 0, 0 }; + struct hid_device_info *devices = hid_enumerate(0, 0); + unsigned short vendor_id, product_id; + + /* Enumeration and init/exit stay on the owner thread, outside worker calls. */ + if (!devices) { + printf("SKIP: %s (no HID device available)\n", name); + } else { + vendor_id = devices->vendor_id; + product_id = devices->product_id; + hid_free_enumeration(devices); + printf("%s (VID %04hx, PID %04hx)\n", name, vendor_id, product_id); + if (action == WAIT_FOR_APP_MUTEX) + CHECK(pthread_mutex_lock(&state.api_mutex) == 0); + CHECK(hid_hotplug_register_callback(vendor_id, product_id, + HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, HID_API_HOTPLUG_ENUMERATE, + action == WAIT_FOR_APP_MUTEX ? serialized_callback : lifecycle_callback, + &state, &state.handle) == 0); + CHECK(pthread_mutex_lock(&state.mutex) == 0); + wait_flag(&state.cond, &state.mutex, &state.entered); + CHECK(pthread_mutex_unlock(&state.mutex) == 0); + + if (action == WAIT_FOR_APP_MUTEX) { + /* The process alarm bounds this call if its error-clearing path tries + to take the hotplug mutex held by the callback waiting for us. */ + CHECK(hid_init() == 0); + CHECK(pthread_mutex_unlock(&state.api_mutex) == 0); + CHECK(hid_hotplug_deregister_callback(state.handle) == 0); + } else if (action == WAIT_FOR_RELEASE) { + pthread_t worker; + struct timespec deadline; + int result = 0; + CHECK(pthread_create(&worker, NULL, deregister_worker, &state) == 0); + CHECK(pthread_mutex_lock(&state.mutex) == 0); + wait_flag(&state.cond, &state.mutex, &state.deregister_started); + /* Give the external call time to block while the callback is held. */ + deadline = deadline_ms(100); + while (!state.deregister_done && result == 0) + result = pthread_cond_timedwait(&state.cond, &state.mutex, &deadline); + CHECK(result == ETIMEDOUT && !state.deregister_done); + state.release = 1; + CHECK(pthread_cond_broadcast(&state.cond) == 0); + wait_flag(&state.cond, &state.mutex, &state.deregister_done); + CHECK(pthread_mutex_unlock(&state.mutex) == 0); + CHECK(pthread_join(worker, NULL) == 0); + } else { + /* The API mutex waits for the self-removing callback to return; + collection must also wait for its thread-specific destructor. */ + hid_hotplug_callback_handle restarted = 0; + if (action == REMOVE_THEN_EXIT) + CHECK(hid_exit() == 0); + else + restarted = register_quiet(); + CHECK(pthread_mutex_lock(&state.mutex) == 0); + CHECK(state.destructor_completed); + CHECK(pthread_mutex_unlock(&state.mutex) == 0); + if (action == REMOVE_THEN_EXIT) { + CHECK(hid_init() == 0); + } else { + CHECK(hid_hotplug_deregister_callback(restarted) == 0); + CHECK(hid_hotplug_deregister_callback(state.handle) == -1); + } + } + } + CHECK(pthread_cond_destroy(&state.cond) == 0); + CHECK(pthread_mutex_destroy(&state.api_mutex) == 0); + CHECK(pthread_mutex_destroy(&state.mutex) == 0); +} + +int main(void) +{ + int i; + setvbuf(stdout, NULL, _IONBF, 0); + alarm(45); + /* macOS requires the initializing thread to remain alive through hid_exit. */ + CHECK(hid_init() == 0); + test_concurrent_registration(); + puts("Immediate restart after last deregistration"); + for (i = 0; i < ITERATIONS; i++) { + hid_hotplug_callback_handle handle = register_quiet(); + CHECK(hid_hotplug_deregister_callback(handle) == 0); + } + CHECK(pthread_key_create(&callback_key, event_thread_destructor) == 0); + test_device_callback(REMOVE_BY_RETURN, "Last callback removal by return value and restart"); + test_device_callback(REMOVE_EXPLICITLY, "Last callback explicit deregistration and restart"); + test_device_callback(REMOVE_THEN_EXIT, "Last callback removal followed by owner-thread hid_exit"); + test_device_callback(WAIT_FOR_RELEASE, "External deregistration waits for an in-flight callback"); + test_device_callback(WAIT_FOR_APP_MUTEX, "Owner hid_init while a callback waits for the application mutex"); + CHECK(pthread_key_delete(callback_key) == 0); + CHECK(hid_exit() == 0); + puts("Repeated owner-thread hid_init/register/hid_exit"); + for (i = 0; i < ITERATIONS; i++) { + CHECK(hid_init() == 0); + (void)register_quiet(); + CHECK(hid_exit() == 0); + } + alarm(0); + puts("Hotplug lifecycle tests passed"); + return EXIT_SUCCESS; +} diff --git a/src/tests/test_platform.h b/src/tests/test_platform.h index c06a8857b..f60f4f3d4 100644 --- a/src/tests/test_platform.h +++ b/src/tests/test_platform.h @@ -6,8 +6,8 @@ Copyright 2026. - Test support: tiny cross-platform helpers (threads, timing) - so the HIDAPI unit tests stay platform-neutral. + Test support: tiny cross-platform helpers (threads, mutexes, + timing) so the HIDAPI unit tests stay platform-neutral. The contents of this file may be used by anyone for any reason without any conditions and may be used as a @@ -21,11 +21,37 @@ #include #else #include + #include #include #endif +/* A small cross-platform atomic integer for test state shared by threads. */ +#ifdef _WIN32 +typedef volatile LONG test_atomic_int; +#else +typedef int test_atomic_int; +#endif + +static inline int test_atomic_load(test_atomic_int *value) +{ +#ifdef _WIN32 + return (int)InterlockedCompareExchange(value, 0, 0); +#else + return __atomic_load_n(value, __ATOMIC_ACQUIRE); +#endif +} + +static inline void test_atomic_store(test_atomic_int *value, int new_value) +{ +#ifdef _WIN32 + InterlockedExchange(value, (LONG)new_value); +#else + __atomic_store_n(value, new_value, __ATOMIC_RELEASE); +#endif +} + /* Monotonic milliseconds for measuring elapsed time. */ -static long long test_now_ms(void) +static inline long long test_now_ms(void) { #ifdef _WIN32 return (long long)GetTickCount64(); @@ -36,7 +62,7 @@ static long long test_now_ms(void) #endif } -static void test_sleep_ms(int ms) +static inline void test_sleep_ms(int ms) { #ifdef _WIN32 Sleep((DWORD)ms); @@ -48,6 +74,64 @@ static void test_sleep_ms(int ms) #endif } +/* An id of the calling thread, usable for equality comparison only. */ +static inline unsigned long long test_thread_id(void) +{ +#ifdef _WIN32 + return (unsigned long long)GetCurrentThreadId(); +#else + /* pthread_t is opaque; the tests only ever compare ids for (in)equality, + and on every platform HIDAPI supports pthread_t is an integer or a + pointer, so the cast preserves the identity the tests care about. */ + return (unsigned long long)(uintptr_t)pthread_self(); +#endif +} + +/* A plain (non-recursive) mutex. */ +typedef struct test_mutex { +#ifdef _WIN32 + CRITICAL_SECTION cs; +#else + pthread_mutex_t mutex; +#endif +} test_mutex; + +static inline void test_mutex_init(test_mutex *m) +{ +#ifdef _WIN32 + InitializeCriticalSection(&m->cs); +#else + pthread_mutex_init(&m->mutex, NULL); +#endif +} + +static inline void test_mutex_destroy(test_mutex *m) +{ +#ifdef _WIN32 + DeleteCriticalSection(&m->cs); +#else + pthread_mutex_destroy(&m->mutex); +#endif +} + +static inline void test_mutex_lock(test_mutex *m) +{ +#ifdef _WIN32 + EnterCriticalSection(&m->cs); +#else + pthread_mutex_lock(&m->mutex); +#endif +} + +static inline void test_mutex_unlock(test_mutex *m) +{ +#ifdef _WIN32 + LeaveCriticalSection(&m->cs); +#else + pthread_mutex_unlock(&m->mutex); +#endif +} + /* A joinable thread running void fn(void*). Results are communicated through * the user's arg (this matches how the tests use a context struct). */ typedef struct test_thread { @@ -62,14 +146,14 @@ typedef struct test_thread { } test_thread; #ifdef _WIN32 -static DWORD WINAPI test__thread_entry(LPVOID p) +static inline DWORD WINAPI test__thread_entry(LPVOID p) { test_thread *t = (test_thread *)p; t->fn(t->arg); return 0; } #else -static void *test__thread_entry(void *p) +static inline void *test__thread_entry(void *p) { test_thread *t = (test_thread *)p; t->fn(t->arg); @@ -79,7 +163,7 @@ static void *test__thread_entry(void *p) #endif /* Returns 0 on success, -1 on failure. */ -static int test_thread_start(test_thread *t, void (*fn)(void *), void *arg) +static inline int test_thread_start(test_thread *t, void (*fn)(void *), void *arg) { t->fn = fn; t->arg = arg; @@ -93,7 +177,7 @@ static int test_thread_start(test_thread *t, void (*fn)(void *), void *arg) } /* Join with a timeout. Returns 0 if the thread finished, -1 on timeout. */ -static int test_thread_join_timeout(test_thread *t, int timeout_ms) +static inline int test_thread_join_timeout(test_thread *t, int timeout_ms) { #ifdef _WIN32 DWORD r = WaitForSingleObject(t->handle, (DWORD)timeout_ms); diff --git a/src/tests/test_virtual_device.h b/src/tests/test_virtual_device.h index c326c2312..a88b93ef7 100644 --- a/src/tests/test_virtual_device.h +++ b/src/tests/test_virtual_device.h @@ -28,12 +28,14 @@ * (it only ever calls public hid_*() functions); all device behaviour lives in * the per-backend provider: * - * - Linux: test_virtual_device_uhid.c (kernel /dev/uhid -> hidraw) - * - Windows: test_virtual_device_win.c (modified vhidmini2 UMDF driver) - * - others: (future) + * - Linux / hidraw: test_virtual_device_uhid.c (kernel /dev/uhid) + * - Linux / libusb: test_virtual_device_rawgadget.c (/dev/raw-gadget + dummy_hcd) + * - Windows: test_virtual_device_win.c (modified vhidmini2 UMDF driver) + * - macOS: test_virtual_device_mac.c (IOHIDUserDevice) * - * The provider only needs to implement create / open / destroy; the scenario - * playback is part of the virtual device (the uhid event pump, or the driver). + * The provider implements create / open / destroy / unplug / replug / trigger; + * scenario playback is part of the virtual device (the uhid event pump, or the + * driver). */ #ifndef HIDAPI_TEST_VIRTUAL_DEVICE_H__ @@ -95,6 +97,26 @@ hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeou /* Destroy the virtual device and free all resources. */ void test_virtual_device_destroy(test_virtual_device *dev); +/* + * Make the device disappear from the system (as if physically unplugged) + * WITHOUT destroying the test_virtual_device context: the context stays valid + * and the device can be re-plugged later with test_virtual_device_replug(). + * Used by the hotplug tests to generate disconnect/reconnect events. + * + * Returns TEST_VDEV_OK on success, TEST_VDEV_UNAVAILABLE when this provider + * cannot toggle device presence (the caller should then skip the test), or + * TEST_VDEV_ERROR on a hard failure. + */ +int test_virtual_device_unplug(test_virtual_device *dev); + +/* + * Make an unplugged device reappear, with the same VID/PID/serial (the + * platform device path MAY differ from the previous appearance). Only valid + * after a successful test_virtual_device_unplug(). Same return codes as + * test_virtual_device_unplug(). + */ +int test_virtual_device_replug(test_virtual_device *dev); + /* * Trigger a pre-recorded scenario on the device by sending the given command * as the first byte of a Feature report, using the ordinary public HIDAPI diff --git a/src/tests/test_virtual_device_mac.c b/src/tests/test_virtual_device_mac.c index 1241cb65b..f76825eac 100644 --- a/src/tests/test_virtual_device_mac.c +++ b/src/tests/test_virtual_device_mac.c @@ -107,6 +107,7 @@ struct test_virtual_device { pthread_mutex_t lock; pthread_cond_t cond; int ready; /* run loop scheduled and running */ + int shutdown; /* protected by lock */ unsigned short vendor_id; unsigned short product_id; @@ -199,16 +200,26 @@ static IOReturn get_report_cb(void *refcon, IOHIDReportType type, static void *runloop_thread_fn(void *arg) { struct test_virtual_device *dev = (struct test_virtual_device *)arg; + CFRunLoopRef runloop = CFRunLoopGetCurrent(); - dev->runloop = CFRunLoopGetCurrent(); - dev->spi.schedule(dev->device, dev->runloop, kCFRunLoopDefaultMode); + dev->spi.schedule(dev->device, runloop, kCFRunLoopDefaultMode); pthread_mutex_lock(&dev->lock); + dev->runloop = runloop; dev->ready = 1; pthread_cond_signal(&dev->cond); pthread_mutex_unlock(&dev->lock); - CFRunLoopRun(); + for (;;) { + int shutdown; + + pthread_mutex_lock(&dev->lock); + shutdown = dev->shutdown; + pthread_mutex_unlock(&dev->lock); + if (shutdown) + break; + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true); + } if (dev->spi.unschedule) dev->spi.unschedule(dev->device, dev->runloop, kCFRunLoopDefaultMode); @@ -380,8 +391,19 @@ void test_virtual_device_destroy(test_virtual_device *dev) return; if (dev->thread_started) { - if (dev->runloop) - CFRunLoopStop(dev->runloop); + CFRunLoopRef runloop; + + pthread_mutex_lock(&dev->lock); + dev->shutdown = 1; + runloop = dev->runloop; + if (runloop) + CFRetain(runloop); + pthread_mutex_unlock(&dev->lock); + if (runloop) { + CFRunLoopWakeUp(runloop); + CFRunLoopStop(runloop); + CFRelease(runloop); + } pthread_join(dev->runloop_thread, NULL); dev->thread_started = 0; } @@ -399,3 +421,17 @@ void test_virtual_device_destroy(test_virtual_device *dev) pthread_mutex_destroy(&dev->lock); free(dev); } + +/* Unplug/replug (device-presence toggling for the hotplug tests) is not + * implemented for this provider yet; the hotplug tests self-skip here. */ +int test_virtual_device_unplug(test_virtual_device *dev) +{ + (void)dev; + return TEST_VDEV_UNAVAILABLE; +} + +int test_virtual_device_replug(test_virtual_device *dev) +{ + (void)dev; + return TEST_VDEV_UNAVAILABLE; +} diff --git a/src/tests/test_virtual_device_rawgadget.c b/src/tests/test_virtual_device_rawgadget.c index 5b2851c16..930ea2784 100644 --- a/src/tests/test_virtual_device_rawgadget.c +++ b/src/tests/test_virtual_device_rawgadget.c @@ -37,6 +37,7 @@ */ #include "test_virtual_device.h" +#include "test_platform.h" #include #include @@ -177,10 +178,11 @@ struct test_virtual_device { pthread_t int_in_thread; int ep0_started; int int_in_started; - volatile int stop; - volatile int ep0_exited; /* ep0 thread has left its fetch loop */ + test_atomic_int stop; + test_atomic_int ep0_exited; /* ep0 thread has left its fetch loop */ + test_atomic_int int_in_exited; /* int_in thread has left its write loop */ - volatile int configured; /* SET_CONFIGURATION seen, IN ep enabled */ + int configured; /* protected by lock */ int int_in_ep; /* raw-gadget handle for the IN endpoint */ __u8 int_in_addr; /* bEndpointAddress chosen from EPS_INFO */ @@ -191,6 +193,7 @@ struct test_virtual_device { unsigned short vendor_id; unsigned short product_id; char serial[64]; + int signal_installed; }; static void sleep_ms(int ms) @@ -209,12 +212,41 @@ static void rg_sig_noop(int sig) (void)sig; } -static void rg_install_signal(void) +static pthread_mutex_t rg_signal_lock = PTHREAD_MUTEX_INITIALIZER; +static struct sigaction rg_previous_signal; +static unsigned int rg_signal_users; + +static int rg_install_signal(struct test_virtual_device *dev) { struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = rg_sig_noop; - sigaction(SIGUSR1, &sa, NULL); + int rc = 0; + + if (dev->signal_installed) + return 0; + pthread_mutex_lock(&rg_signal_lock); + if (rg_signal_users == 0) { + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = rg_sig_noop; + if (sigaction(SIGUSR1, &sa, &rg_previous_signal) != 0) + rc = -1; + } + if (rc == 0) { + rg_signal_users++; + dev->signal_installed = 1; + } + pthread_mutex_unlock(&rg_signal_lock); + return rc; +} + +static void rg_restore_signal(struct test_virtual_device *dev) +{ + if (!dev->signal_installed) + return; + pthread_mutex_lock(&rg_signal_lock); + dev->signal_installed = 0; + if (--rg_signal_users == 0) + (void)sigaction(SIGUSR1, &rg_previous_signal, NULL); + pthread_mutex_unlock(&rg_signal_lock); } /* EP0/EP I/O via a heap buffer sized for the flexible-array struct (heap memory @@ -466,9 +498,11 @@ static void handle_control(struct test_virtual_device *dev, ep.bInterval = 5; handle = ioctl(dev->fd, USB_RAW_IOCTL_EP_ENABLE, &ep); if (handle >= 0) { - dev->int_in_ep = handle; ioctl(dev->fd, USB_RAW_IOCTL_CONFIGURE, 0); + pthread_mutex_lock(&dev->lock); + dev->int_in_ep = handle; dev->configured = 1; + pthread_mutex_unlock(&dev->lock); } ep0_ack(dev->fd, ctrl); /* status ACK */ break; @@ -556,12 +590,26 @@ static void *ep0_thread_fn(void *arg) struct test_virtual_device *dev = (struct test_virtual_device *)arg; struct usb_raw_event *ev; size_t evsz = sizeof(*ev) + sizeof(struct usb_ctrlrequest); + sigset_t set; + + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + if (pthread_sigmask(SIG_UNBLOCK, &set, NULL) != 0) { + fprintf(stderr, "[raw-gadget] could not unblock SIGUSR1 in ep0 worker\n"); + fflush(stderr); + test_atomic_store(&dev->ep0_exited, 1); + return NULL; + } ev = (struct usb_raw_event *)calloc(1, evsz); - if (!ev) + if (!ev) { + /* Report the exit here too, or rg_unplug() would spin its full + SIGUSR1 budget signalling a thread that is already gone. */ + test_atomic_store(&dev->ep0_exited, 1); return NULL; + } - while (!dev->stop) { + while (!test_atomic_load(&dev->stop)) { int rv; ev->type = 0; ev->length = sizeof(struct usb_ctrlrequest); @@ -583,7 +631,7 @@ static void *ep0_thread_fn(void *arg) } free(ev); - dev->ep0_exited = 1; + test_atomic_store(&dev->ep0_exited, 1); return NULL; } @@ -592,13 +640,24 @@ static void *ep0_thread_fn(void *arg) static void *int_in_thread_fn(void *arg) { struct test_virtual_device *dev = (struct test_virtual_device *)arg; + sigset_t set; + + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + if (pthread_sigmask(SIG_UNBLOCK, &set, NULL) != 0) { + fprintf(stderr, "[raw-gadget] could not unblock SIGUSR1 in interrupt-IN worker\n"); + fflush(stderr); + test_atomic_store(&dev->int_in_exited, 1); + return NULL; + } for (;;) { unsigned char command; const unsigned char *payload; + int int_in_ep; pthread_mutex_lock(&dev->lock); - while (!dev->stop && dev->pending == TEST_VDEV_CMD_NONE) { + while (!test_atomic_load(&dev->stop) && dev->pending == TEST_VDEV_CMD_NONE) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_nsec += 100 * 1000000L; @@ -608,58 +667,138 @@ static void *int_in_thread_fn(void *arg) } pthread_cond_timedwait(&dev->cond, &dev->lock, &ts); } - if (dev->stop) { + if (test_atomic_load(&dev->stop)) { pthread_mutex_unlock(&dev->lock); break; } command = dev->pending; dev->pending = TEST_VDEV_CMD_NONE; - pthread_mutex_unlock(&dev->lock); - - if (!dev->configured || dev->int_in_ep < 0) + int_in_ep = dev->int_in_ep; + if (!dev->configured || int_in_ep < 0) { + pthread_mutex_unlock(&dev->lock); continue; + } + pthread_mutex_unlock(&dev->lock); payload = (command == TEST_VDEV_CMD_EMIT_B) ? k_input_b : k_input_a; /* Best effort: may fail if the host isn't reading; ignore. */ - (void)ep_io_write(dev->fd, USB_RAW_IOCTL_EP_WRITE, dev->int_in_ep, + (void)ep_io_write(dev->fd, USB_RAW_IOCTL_EP_WRITE, int_in_ep, payload, TEST_VDEV_REPORT_SIZE); } + test_atomic_store(&dev->int_in_exited, 1); return NULL; } -int test_virtual_device_create(test_virtual_device **out_dev, - unsigned short vendor_id, - unsigned short product_id, - const char *serial) +/* rg_unplug()/rg_plug() are the repeatable "presence toggle" machinery factored + out of destroy()/create(): a plug (re)binds the gadget to the dummy_hcd UDC + and starts the workers (USB attach -> libusb ARRIVED); an unplug stops the + workers and closes the fd (USB detach -> libusb LEFT). Neither touches the + mutex/cond or the identity fields (vendor_id/product_id/serial), so the same + dev struct survives an unplug/replug cycle with its identity intact. */ + +/* The teardown half of the "plug": stop the worker threads and unbind the + gadget from the UDC. Closing the fd performs the USB detach. Leaves the + mutex/cond and the dev struct intact so the same dev can be replugged. + Idempotent: safe to call when already unplugged (fd == -1, no threads). */ +static void rg_unplug(struct test_virtual_device *dev) { - struct test_virtual_device *dev; - struct usb_raw_init init; - int rc; + test_atomic_store(&dev->stop, 1); + pthread_mutex_lock(&dev->lock); + pthread_cond_broadcast(&dev->cond); + pthread_mutex_unlock(&dev->lock); - if (!out_dev) - return TEST_VDEV_ERROR; - *out_dev = NULL; + /* The ep0 thread is parked in the blocking EVENT_FETCH ioctl and the int_in + thread may be parked in EP_WRITE; interrupt BOTH with SIGUSR1 until each + reports it has left its loop, so the joins below don't hang. Signalling + must not stop at the ep0 thread: an int_in thread blocked in EP_WRITE + would then never be woken and its join would hang until the CTest + timeout. */ + { + int spins = 0; + while (spins++ < 500) { + int ep0_busy = dev->ep0_started && !test_atomic_load(&dev->ep0_exited); + int int_in_busy = dev->int_in_started && !test_atomic_load(&dev->int_in_exited); - dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); - if (!dev) - return TEST_VDEV_ERROR; + if (!ep0_busy && !int_in_busy) + break; + if (ep0_busy) + pthread_kill(dev->ep0_thread, SIGUSR1); + if (int_in_busy) + pthread_kill(dev->int_in_thread, SIGUSR1); + sleep_ms(10); + } + } + if ((dev->ep0_started && !test_atomic_load(&dev->ep0_exited)) || + (dev->int_in_started && !test_atomic_load(&dev->int_in_exited))) { + fprintf(stderr, "[raw-gadget] worker did not exit during shutdown:"); + if (dev->ep0_started && !test_atomic_load(&dev->ep0_exited)) + fprintf(stderr, " ep0"); + if (dev->int_in_started && !test_atomic_load(&dev->int_in_exited)) + fprintf(stderr, " interrupt-IN"); + fprintf(stderr, "\n"); + fflush(stderr); + abort(); + } + if (dev->int_in_started) { + pthread_join(dev->int_in_thread, NULL); + dev->int_in_started = 0; + } + if (dev->ep0_started) { + pthread_join(dev->ep0_thread, NULL); + dev->ep0_started = 0; + } + + if (dev->fd >= 0) { + close(dev->fd); + dev->fd = -1; + } +} + +/* (Re-)bind the gadget to the dummy_hcd UDC and start the worker threads. This + is the "plug": open + INIT + RUN performs the USB attach and the ep0 thread + answers enumeration. Returns TEST_VDEV_UNAVAILABLE when there is no raw-gadget + node or no dummy_hcd UDC to bind (INIT/RUN failure, e.g. the UDC is already + bound by another gadget). On any failure it cleans up like the old create() + fail path and leaves dev in the unplugged state (fd == -1, threads stopped). + If dev still looks plugged (open fd or live workers) it is unplugged first, + so a redundant plug cannot leak the old fd or orphan the old threads. */ +static int rg_plug(struct test_virtual_device *dev) +{ + struct usb_raw_init init; + int rc; + + /* Never plug on top of a plug: that would overwrite fd/thread handles and + leave the old ones behind. rg_unplug() is idempotent, so this is a no-op + when the device is already unplugged. */ + if (dev->fd >= 0 || dev->ep0_started || dev->int_in_started) + rg_unplug(dev); + + /* Reset every per-plug field so a 2nd/3rd plug behaves exactly like the + first. The signal handler remains installed across unplug/replug, and the + *_exited flags MUST be 0 here so rg_unplug's SIGUSR1 spin drives the + *new* threads. The mutex/cond and identity (vendor/product/serial) are + intentionally left untouched -- they persist across the unplug/replug + cycle. */ + test_atomic_store(&dev->stop, 0); dev->fd = -1; - dev->int_in_ep = -1; dev->int_in_addr = 0x81; + pthread_mutex_lock(&dev->lock); + dev->configured = 0; + dev->int_in_ep = -1; + pthread_mutex_unlock(&dev->lock); + dev->ep0_started = 0; + dev->int_in_started = 0; + test_atomic_store(&dev->ep0_exited, 0); + test_atomic_store(&dev->int_in_exited, 0); dev->pending = TEST_VDEV_CMD_NONE; - dev->vendor_id = vendor_id; - dev->product_id = product_id; - snprintf(dev->serial, sizeof(dev->serial), "%s", serial ? serial : ""); - pthread_mutex_init(&dev->lock, NULL); - pthread_cond_init(&dev->cond, NULL); - dev->fd = open("/dev/raw-gadget", O_RDWR); + dev->fd = open("/dev/raw-gadget", O_RDWR | O_CLOEXEC); if (dev->fd < 0) { int e = errno; - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + dev->fd = -1; + fprintf(stderr, "[raw-gadget] open /dev/raw-gadget failed: errno %d (%s)\n", + e, strerror(e)); if (e == ENOENT || e == EACCES || e == EPERM || e == ENODEV) return TEST_VDEV_UNAVAILABLE; return TEST_VDEV_ERROR; @@ -670,51 +809,96 @@ int test_virtual_device_create(test_virtual_device **out_dev, snprintf((char *)init.driver_name, sizeof(init.driver_name), "dummy_udc"); snprintf((char *)init.device_name, sizeof(init.device_name), "dummy_udc.0"); init.speed = USB_SPEED_HIGH; - if (ioctl(dev->fd, USB_RAW_IOCTL_INIT, &init) < 0 || - ioctl(dev->fd, USB_RAW_IOCTL_RUN, 0) < 0) { - /* No dummy_hcd UDC present -> nothing to emulate on; skip. */ + if (ioctl(dev->fd, USB_RAW_IOCTL_INIT, &init) < 0) { + int e = errno; + /* No dummy_hcd UDC to bind (absent, or already in use) -> skip. */ + fprintf(stderr, "[raw-gadget] USB_RAW_IOCTL_INIT failed: errno %d (%s)\n", + e, strerror(e)); close(dev->fd); - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + dev->fd = -1; + return TEST_VDEV_UNAVAILABLE; + } + if (ioctl(dev->fd, USB_RAW_IOCTL_RUN, 0) < 0) { + int e = errno; + fprintf(stderr, "[raw-gadget] USB_RAW_IOCTL_RUN failed: errno %d (%s)\n", + e, strerror(e)); + close(dev->fd); + dev->fd = -1; return TEST_VDEV_UNAVAILABLE; } - rg_install_signal(); + if (rg_install_signal(dev) != 0) { + fprintf(stderr, "[raw-gadget] sigaction(SIGUSR1) failed: errno %d (%s)\n", + errno, strerror(errno)); + rg_unplug(dev); + return TEST_VDEV_ERROR; + } rc = pthread_create(&dev->ep0_thread, NULL, ep0_thread_fn, dev); - if (rc != 0) + if (rc != 0) { + fprintf(stderr, "[raw-gadget] pthread_create(ep0) failed: %s\n", strerror(rc)); goto fail_threads; + } dev->ep0_started = 1; rc = pthread_create(&dev->int_in_thread, NULL, int_in_thread_fn, dev); - if (rc != 0) + if (rc != 0) { + fprintf(stderr, "[raw-gadget] pthread_create(interrupt-IN) failed: %s\n", + strerror(rc)); goto fail_threads; + } dev->int_in_started = 1; - *out_dev = dev; return TEST_VDEV_OK; fail_threads: - dev->stop = 1; - pthread_mutex_lock(&dev->lock); - pthread_cond_broadcast(&dev->cond); - pthread_mutex_unlock(&dev->lock); - if (dev->ep0_started) { - int spins = 0; - while (!dev->ep0_exited && spins++ < 500) { - pthread_kill(dev->ep0_thread, SIGUSR1); - sleep_ms(10); - } - pthread_join(dev->ep0_thread, NULL); - } - close(dev->fd); - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + /* Stop+join whatever started and close the fd; rg_unplug leaves dev in the + unplugged state (fd == -1, *_started == 0), ready for a later replug. */ + rg_unplug(dev); + rg_restore_signal(dev); return TEST_VDEV_ERROR; } +int test_virtual_device_create(test_virtual_device **out_dev, + unsigned short vendor_id, + unsigned short product_id, + const char *serial) +{ + struct test_virtual_device *dev; + int rc; + + if (!out_dev) + return TEST_VDEV_ERROR; + *out_dev = NULL; + + dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); + if (!dev) + return TEST_VDEV_ERROR; + /* calloc leaves fd == 0, a valid descriptor (stdin). Put dev in the + documented unplugged state before rg_plug() inspects it, or its + already-plugged guard would take fd 0 for an open gadget fd and close it. */ + dev->fd = -1; + + /* Identity + lifetime state: these outlive any unplug/replug. The per-plug + fields are (re)initialised by rg_plug. */ + dev->vendor_id = vendor_id; + dev->product_id = product_id; + snprintf(dev->serial, sizeof(dev->serial), "%s", serial ? serial : ""); + pthread_mutex_init(&dev->lock, NULL); + pthread_cond_init(&dev->cond, NULL); + + rc = rg_plug(dev); + if (rc != TEST_VDEV_OK) { + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return rc; + } + + *out_dev = dev; + return TEST_VDEV_OK; +} + hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms) { wchar_t wserial[64]; @@ -771,40 +955,32 @@ void test_virtual_device_destroy(test_virtual_device *dev) { if (!dev) return; - - dev->stop = 1; - pthread_mutex_lock(&dev->lock); - pthread_cond_broadcast(&dev->cond); - pthread_mutex_unlock(&dev->lock); - - /* The ep0 thread is parked in the blocking EVENT_FETCH ioctl (and the - int_in thread may be in EP_WRITE); interrupt them with SIGUSR1 until the - ep0 thread reports it has left its loop, so the joins below don't hang. */ - { - int spins = 0; - while (dev->ep0_started && !dev->ep0_exited && spins++ < 500) { - pthread_kill(dev->ep0_thread, SIGUSR1); - if (dev->int_in_started) - pthread_kill(dev->int_in_thread, SIGUSR1); - sleep_ms(10); - } - } - - if (dev->int_in_started) { - pthread_join(dev->int_in_thread, NULL); - dev->int_in_started = 0; - } - if (dev->ep0_started) { - pthread_join(dev->ep0_thread, NULL); - dev->ep0_started = 0; - } - - if (dev->fd >= 0) { - close(dev->fd); - dev->fd = -1; - } - + rg_unplug(dev); + rg_restore_signal(dev); pthread_cond_destroy(&dev->cond); pthread_mutex_destroy(&dev->lock); free(dev); } + +/* Device-presence toggling for the hotplug tests: unplug detaches the gadget + * (USB disconnect -> libusb LEFT) while keeping dev alive; replug re-attaches it + * with the same VID/PID/serial (USB connect -> libusb ARRIVED). */ +int test_virtual_device_unplug(test_virtual_device *dev) +{ + if (!dev) + return TEST_VDEV_ERROR; + rg_unplug(dev); + return TEST_VDEV_OK; +} + +int test_virtual_device_replug(test_virtual_device *dev) +{ + int rc; + + if (!dev) + return TEST_VDEV_ERROR; + rc = rg_plug(dev); + if (rc == TEST_VDEV_UNAVAILABLE) + return TEST_VDEV_ERROR; + return rc; +} diff --git a/src/tests/test_virtual_device_uhid.c b/src/tests/test_virtual_device_uhid.c index a86fd1da5..9b6dbd52a 100644 --- a/src/tests/test_virtual_device_uhid.c +++ b/src/tests/test_virtual_device_uhid.c @@ -206,6 +206,33 @@ static void *pump_thread_fn(void *arg) return NULL; } +/* Announce the device to the kernel (UHID_CREATE2 on the open uhid fd). + * Returns 0 on success, -1 on failure (with errno set by write()). Used both + * by the initial create() and by test_virtual_device_replug(): the kernel + * allows a new UHID_CREATE2 on the same fd after a UHID_DESTROY. */ +static int uhid_write_create2(struct test_virtual_device *dev) +{ + struct uhid_event ev; + ssize_t written; + + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_CREATE2; + snprintf((char *)ev.u.create2.name, sizeof(ev.u.create2.name), "HIDAPI Test Device"); + snprintf((char *)ev.u.create2.uniq, sizeof(ev.u.create2.uniq), "%s", dev->serial); + memcpy(ev.u.create2.rd_data, k_report_descriptor, sizeof(k_report_descriptor)); + ev.u.create2.rd_size = (uint16_t)sizeof(k_report_descriptor); + ev.u.create2.bus = 0x03; /* BUS_USB */ + ev.u.create2.vendor = dev->vendor_id; + ev.u.create2.product = dev->product_id; + ev.u.create2.version = 0; + ev.u.create2.country = 0; + + pthread_mutex_lock(&dev->write_lock); + written = write(dev->fd, &ev, sizeof(ev)); + pthread_mutex_unlock(&dev->write_lock); + return written < 0 ? -1 : 0; +} + int test_virtual_device_create(test_virtual_device **out_dev, unsigned short vendor_id, unsigned short product_id, @@ -239,19 +266,7 @@ int test_virtual_device_create(test_virtual_device **out_dev, return TEST_VDEV_ERROR; } - memset(&ev, 0, sizeof(ev)); - ev.type = UHID_CREATE2; - snprintf((char *)ev.u.create2.name, sizeof(ev.u.create2.name), "HIDAPI Test Device"); - snprintf((char *)ev.u.create2.uniq, sizeof(ev.u.create2.uniq), "%s", dev->serial); - memcpy(ev.u.create2.rd_data, k_report_descriptor, sizeof(k_report_descriptor)); - ev.u.create2.rd_size = (uint16_t)sizeof(k_report_descriptor); - ev.u.create2.bus = 0x03; /* BUS_USB */ - ev.u.create2.vendor = vendor_id; - ev.u.create2.product = product_id; - ev.u.create2.version = 0; - ev.u.create2.country = 0; - - if (write(dev->fd, &ev, sizeof(ev)) < 0) { + if (uhid_write_create2(dev) != 0) { int e = errno; close(dev->fd); pthread_mutex_destroy(&dev->write_lock); @@ -321,6 +336,37 @@ hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeou } } +int test_virtual_device_unplug(test_virtual_device *dev) +{ + struct uhid_event ev; + ssize_t written; + + if (!dev || dev->fd < 0) + return TEST_VDEV_ERROR; + + /* UHID_DESTROY unregisters the HID device from the kernel (the hidraw + node disappears) but keeps the uhid fd usable: a later UHID_CREATE2 on + the same fd brings the device back. The event pump keeps running; it + simply sees no events while the device is unplugged. */ + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_DESTROY; + + pthread_mutex_lock(&dev->write_lock); + written = write(dev->fd, &ev, sizeof(ev)); + pthread_mutex_unlock(&dev->write_lock); + return written < 0 ? TEST_VDEV_ERROR : TEST_VDEV_OK; +} + +int test_virtual_device_replug(test_virtual_device *dev) +{ + if (!dev || dev->fd < 0) + return TEST_VDEV_ERROR; + + /* Same ids and serial as the original appearance; the kernel assigns a + fresh hidraw node, so the HIDAPI path may differ. */ + return uhid_write_create2(dev) != 0 ? TEST_VDEV_ERROR : TEST_VDEV_OK; +} + int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, unsigned char command) { diff --git a/src/tests/test_virtual_device_win.c b/src/tests/test_virtual_device_win.c index 6ad0961e5..cb8c6d74a 100644 --- a/src/tests/test_virtual_device_win.c +++ b/src/tests/test_virtual_device_win.c @@ -21,9 +21,10 @@ * afterwards. That driver implements the same pre-recorded scenario protocol * as the Linux uhid provider (see test_virtual_device.h). * - * create() just records the ids; presence is confirmed by open_hidapi(), which - * also caches the device's feature-report length so that trigger() can send a - * feature report of exactly the size Windows requires. + * create() rejects unsupported ids, re-enables a disabled HID child if needed, + * and confirms presence by enumeration. open_hidapi() caches the device's + * feature-report length so that trigger() can send a feature report of exactly + * the size Windows requires. */ #include "test_virtual_device.h" @@ -31,9 +32,29 @@ #include #include #include +#include #include #include #include +#include + +/* + * The virtual-HID devnode is located by its INF hardware id, not a fixed instance + * path. The CI job installs it with `devcon install VhidminiUm.inf + * "root\VhidminiUm"`, but PnP derives the devnode's *instance id* from the driver's + * setup class (Class=HIDClass in the INF -> observed instance ROOT\HIDCLASS\0000), + * not from the hardware id, so the instance path is not knowable a priori. + * locate_vhid_devnode() instead scans the ROOT enumerator for the devnode whose + * hardware id exactly matches this id (case-insensitively). + * + * Presence toggling (unplug/replug) disables/enables that devnode via cfgmgr32: + * disabling it tears down the HIDClass child PDO so the GUID_DEVINTERFACE_HID + * interface disappears (the winapi backend sees a removal); enabling it re-creates + * the interface (an arrival). + */ +#define VHID_HARDWARE_ID_MATCH "ROOT\\VHIDMINIUM" +#define VHID_VENDOR_ID 0xF1D0 +#define VHID_PRODUCT_ID 0x9001 struct test_virtual_device { unsigned short vendor_id; @@ -42,6 +63,140 @@ struct test_virtual_device { ULONG feature_len; /* FeatureReportByteLength of the opened device */ }; +/* Locate the root-enumerated virtual-HID devnode by matching its INF hardware id + (VHID_HARDWARE_ID_MATCH), robust to the PnP-generated instance path and index. + Every devnode under the ROOT enumerator is scanned - enabled or disabled, since + a disabled root devnode is still enumerated and "configured" - so both unplug's + disable and replug's re-enable resolve the same node. Returns CR_SUCCESS with + *out_devinst set; CR_NO_SUCH_DEVNODE if no such devnode exists (driver/device + not installed here); otherwise the failing CONFIGRET. */ +static CONFIGRET locate_vhid_devnode(DEVINST *out_devinst) +{ + CONFIGRET cr; + ULONG list_len = 0; + char *list = NULL; + char *inst; + int attempt; + + for (attempt = 0; attempt < 3; attempt++) { + cr = CM_Get_Device_ID_List_SizeA(&list_len, "ROOT", + CM_GETIDLIST_FILTER_ENUMERATOR); + if (cr != CR_SUCCESS) + return cr; + if (list_len < 2) + return CR_NO_SUCH_DEVNODE; + + list = (char *)malloc(list_len); + if (!list) + return CR_OUT_OF_MEMORY; + + cr = CM_Get_Device_ID_ListA("ROOT", list, list_len, + CM_GETIDLIST_FILTER_ENUMERATOR); + if (cr == CR_SUCCESS) + break; + free(list); + list = NULL; + if (cr != CR_BUFFER_SMALL) + return cr; + } + if (!list) + return cr; + + /* The list is a REG_MULTI_SZ of instance ids; walk each one. */ + for (inst = list; *inst != '\0'; inst += strlen(inst) + 1) { + DEVINST devinst; + char *hwids = NULL; + char *h; + ULONG hwlen; + + if (CM_Locate_DevNodeA(&devinst, inst, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) + continue; + for (attempt = 0; attempt < 3; attempt++) { + hwlen = 0; + cr = CM_Get_DevNode_Registry_PropertyA(devinst, CM_DRP_HARDWAREID, NULL, + NULL, &hwlen, 0); + if (cr != CR_BUFFER_SMALL && cr != CR_SUCCESS) { + if (cr == CR_NO_SUCH_VALUE || cr == CR_INVALID_DEVNODE || cr == CR_NO_SUCH_DEVNODE) + break; + free(list); + return cr; + } + if (hwlen == 0) { + cr = CR_NO_SUCH_VALUE; + break; + } + hwids = (char *)malloc(hwlen); + if (!hwids) { + free(list); + return CR_OUT_OF_MEMORY; + } + cr = CM_Get_DevNode_Registry_PropertyA(devinst, CM_DRP_HARDWAREID, NULL, + hwids, &hwlen, 0); + if (cr == CR_SUCCESS) + break; + free(hwids); + hwids = NULL; + if (cr == CR_NO_SUCH_VALUE || cr == CR_INVALID_DEVNODE || cr == CR_NO_SUCH_DEVNODE) + break; + if (cr != CR_BUFFER_SMALL) { + free(list); + return cr; + } + } + if (cr == CR_NO_SUCH_VALUE || cr == CR_INVALID_DEVNODE || cr == CR_NO_SUCH_DEVNODE) + continue; + if (!hwids) { + free(list); + return cr; + } + /* CM_DRP_HARDWAREID is itself a REG_MULTI_SZ; match any of its ids. */ + for (h = hwids; *h != '\0'; h += strlen(h) + 1) { + if (_stricmp(h, VHID_HARDWARE_ID_MATCH) == 0) { + *out_devinst = devinst; + free(hwids); + free(list); + return CR_SUCCESS; + } + } + free(hwids); + } + + free(list); + return CR_NO_SUCH_DEVNODE; +} + +/* Find the HID child PDO of the vhidmini function devnode. Presence toggling acts + on this leaf (not the function device): disabling/enabling it raises the HID + interface removal/arrival the winapi backend watches, while leaving the UMDF + host running - disabling the function device instead re-creates the child in a + non-started state, so its HID interface never comes back. Prefers the child + whose instance id is under the HID enumerator; falls back to the first child. */ +static CONFIGRET find_hid_child(DEVINST func, DEVINST *out_child) +{ + DEVINST child, first; + CONFIGRET cr; + + cr = CM_Get_Child(&child, func, 0); + if (cr != CR_SUCCESS) + return cr; /* CR_NO_SUCH_DEVNODE if the function device has no child */ + + first = child; /* fallback: the function device's first child */ + for (;;) { + char cid[MAX_DEVICE_ID_LEN]; + + if (CM_Get_Device_IDA(child, cid, (ULONG)sizeof(cid), 0) == CR_SUCCESS && + strncmp(cid, "HID\\", 4) == 0) { + *out_child = child; + return CR_SUCCESS; + } + if (CM_Get_Sibling(&child, child, 0) != CR_SUCCESS) + break; + } + + *out_child = first; + return CR_SUCCESS; +} + int test_virtual_device_create(test_virtual_device **out_dev, unsigned short vendor_id, unsigned short product_id, @@ -52,6 +207,48 @@ int test_virtual_device_create(test_virtual_device **out_dev, if (!out_dev) return TEST_VDEV_ERROR; *out_dev = NULL; + if (vendor_id != VHID_VENDOR_ID || product_id != VHID_PRODUCT_ID) + return TEST_VDEV_UNAVAILABLE; + + /* Windows cannot create HID devices on the fly; the CI job pre-installs a + single static vhidmini device whose identity is fixed (0xF1D0/0x9001; + see src/tests/windows/driver). A disabled HID child is re-enabled to + recover from an interrupted earlier run. */ + { + DEVINST func, child; + CONFIGRET cr; + ULONG status, problem; + int enabled = 0; + + /* A previous run killed mid-test (e.g. a CTest timeout) never reached + destroy(), so the HID child may still be disabled from an unplug. */ + if (locate_vhid_devnode(&func) == CR_SUCCESS && + find_hid_child(func, &child) == CR_SUCCESS) { + cr = CM_Get_DevNode_Status(&status, &problem, child, 0); + if (cr == CR_SUCCESS && problem == CM_PROB_DISABLED) { + if (CM_Enable_DevNode(child, 0) == CR_SUCCESS) + enabled = 1; + } + } + if (enabled) { + int spins; + for (spins = 0; spins < 30; spins++) { + struct hid_device_info *infos = hid_enumerate(vendor_id, product_id); + if (infos) { + hid_free_enumeration(infos); + break; + } + Sleep(100); + } + if (spins == 30) + return TEST_VDEV_UNAVAILABLE; + } else { + struct hid_device_info *infos = hid_enumerate(vendor_id, product_id); + if (!infos) + return TEST_VDEV_UNAVAILABLE; + hid_free_enumeration(infos); + } + } dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); if (!dev) @@ -62,8 +259,7 @@ int test_virtual_device_create(test_virtual_device **out_dev, if (serial) strncpy_s(dev->serial, sizeof(dev->serial), serial, _TRUNCATE); - /* The device (if any) is installed by the harness; presence is verified - by open_hidapi(). */ + /* The harness installed the device; enumeration above confirmed presence. */ *out_dev = dev; return TEST_VDEV_OK; } @@ -141,6 +337,105 @@ int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, void test_virtual_device_destroy(test_virtual_device *dev) { - /* The harness uninstalls the driver/device after the test. */ + /* The harness (CI) uninstalls the driver/device after the test. But if a + test unplugged (disabled the HID child) and exited before replugging it, + re-enable the child best-effort so a later run on the same host is not left + with a disabled device. Locating/enabling an absent, already-enabled, or + access-denied node is harmless, so the CONFIGRETs are intentionally ignored + here. */ + DEVINST func, child; + if (locate_vhid_devnode(&func) == CR_SUCCESS && + find_hid_child(func, &child) == CR_SUCCESS) + (void)CM_Enable_DevNode(child, 0); free(dev); } + +/* Unplug = disable the HID child PDO. Its GUID_DEVINTERFACE_HID interface + * disappears and the winapi backend's PnP notification fires a removal (the test + * then sees the device LEFT / gone from hid_enumerate), while the UMDF function + * device keeps running. This is also the hotplug test's capability probe, so a + * function devnode that cannot be located (driver/device not installed) or a + * child that cannot be disabled for lack of elevation (CR_ACCESS_DENIED) returns + * UNAVAILABLE, which makes the test skip cleanly instead of failing. */ +int test_virtual_device_unplug(test_virtual_device *dev) +{ + DEVINST func, child; + CONFIGRET cr; + + (void)dev; /* the devnode is installed out-of-band by the CI job */ + + cr = locate_vhid_devnode(&func); + if (cr == CR_NO_SUCH_DEVNODE) { + fprintf(stderr, "[win-vdev] no ROOT devnode with hardware id '%s' " + "(driver/device not installed) -> hotplug test skips\n", + VHID_HARDWARE_ID_MATCH); + return TEST_VDEV_UNAVAILABLE; /* driver/device not installed here */ + } + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] locate failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + + cr = find_hid_child(func, &child); + if (cr == CR_NO_SUCH_DEVNODE) + return TEST_VDEV_OK; /* no HID child -> already absent, nothing to disable */ + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] find HID child failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + + cr = CM_Disable_DevNode(child, CM_DISABLE_UI_NOT_OK); + if (cr == CR_SUCCESS) + return TEST_VDEV_OK; + if (cr == CR_ACCESS_DENIED) { + fprintf(stderr, "[win-vdev] CM_Disable_DevNode(child) -> CR_ACCESS_DENIED " + "(not elevated) -> hotplug test skips\n"); + return TEST_VDEV_UNAVAILABLE; /* not elevated -> skip, don't fail */ + } + fprintf(stderr, "[win-vdev] CM_Disable_DevNode(child) failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; +} + +/* Replug = re-enable the HID child PDO disabled by unplug(). Its HID interface is + * re-created, so the backend sees an arrival (the device is back in + * hid_enumerate). Failure here is a hard error, not a skip: if unplug() disabled + * the child we must be able to re-enable it. */ +int test_virtual_device_replug(test_virtual_device *dev) +{ + DEVINST func, child; + CONFIGRET cr; + + (void)dev; + + cr = locate_vhid_devnode(&func); + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] replug locate failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + + cr = find_hid_child(func, &child); + if (cr != CR_SUCCESS) { + /* The child devnode is gone entirely (not merely disabled); ask the + function device to re-report it, then retry. */ + (void)CM_Reenumerate_DevNode(func, CM_REENUMERATE_SYNCHRONOUS); + cr = find_hid_child(func, &child); + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] replug: no HID child to enable: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + } + + cr = CM_Enable_DevNode(child, 0); + if (cr != CR_SUCCESS) { + fprintf(stderr, "[win-vdev] CM_Enable_DevNode(child) failed: CONFIGRET 0x%lX\n", + (unsigned long)cr); + return TEST_VDEV_ERROR; + } + + return TEST_VDEV_OK; +} diff --git a/src/tests/windows/driver/README.md b/src/tests/windows/driver/README.md index 07a7651b6..c3d3a8ed9 100644 --- a/src/tests/windows/driver/README.md +++ b/src/tests/windows/driver/README.md @@ -2,7 +2,7 @@ This directory contains a small **virtual HID minidriver** used only by the HIDAPI virtual-device tests on Windows (the `winapi` backend's -`DeviceIO_winapi` test). It is **not** part of the HIDAPI library: it is a +`DeviceIO_winapi` and `Hotplug_winapi` tests). It is **not** part of the HIDAPI library: it is a standalone UMDF 2 driver that the `win-vhid-test` CI job builds, self-signs and installs out-of-band, runs the test against, then removes. @@ -38,4 +38,5 @@ presence in the source tree is therefore mere aggregation, not a combined work. |------|--------| | `vhidmini.c` | **Modified** — default report descriptor matches the Linux uhid test device byte-for-byte; implements the HIDAPI pre-recorded "scenario" protocol (a Feature `SET_REPORT` command makes the device replay a canned input report; see `../../test_virtual_device.h`). | | `vhidmini.h` | **Modified** — supporting declarations for the scenario protocol. | -| `common.h`, `util.c`, `vhidmini.rc`, `VhidminiUm.inx`, `VhidminiUm.vcxproj` | Used essentially as-is (no HIDAPI-specific changes beyond what's needed to build the standalone `VhidminiUm.dll`). | +| `common.h` | **Modified** — test serial-number declarations. | +| `util.c`, `vhidmini.rc`, `VhidminiUm.inx`, `VhidminiUm.vcxproj` | Used essentially as-is (no HIDAPI-specific changes beyond what's needed to build the standalone `VhidminiUm.dll`). | diff --git a/src/tests/windows/driver/common.h b/src/tests/windows/driver/common.h index 9b14cc6d4..2ed4cbb74 100644 --- a/src/tests/windows/driver/common.h +++ b/src/tests/windows/driver/common.h @@ -39,7 +39,9 @@ Module Name: #define MAXIMUM_STRING_LENGTH (126 * sizeof(WCHAR)) #define VHIDMINI_MANUFACTURER_STRING L"UMDF Virtual hidmini device Manufacturer string" #define VHIDMINI_PRODUCT_STRING L"UMDF Virtual hidmini device Product string" -#define VHIDMINI_SERIAL_NUMBER_STRING L"UMDF Virtual hidmini device Serial Number string" +/* Must equal test_hotplug.c's TEST_SERIAL: the device-backed hotplug test + (Hotplug_winapi) matches this single static device by serial number. */ +#define VHIDMINI_SERIAL_NUMBER_STRING L"HIDAPI-HOTPLUG-TEST" #define VHIDMINI_DEVICE_STRING L"UMDF Virtual hidmini device" #define VHIDMINI_DEVICE_STRING_INDEX 5 #include diff --git a/src/tests/windows/driver/vhidmini.h b/src/tests/windows/driver/vhidmini.h index 9dc037508..16ccff991 100644 --- a/src/tests/windows/driver/vhidmini.h +++ b/src/tests/windows/driver/vhidmini.h @@ -18,9 +18,9 @@ Module Name: /* * Modified by the libusb/hidapi team for the HIDAPI virtual-device tests - * (implements the HIDAPI "scenario" protocol; see - * src/tests/test_virtual_device.h). Derived from the vhidmini2 sample in - * microsoft/Windows-driver-samples, which is licensed under the Microsoft + * (implements the HIDAPI "scenario" protocol used by test_device_io.c and + * test_hotplug.c; see src/tests/test_virtual_device.h). Derived from the + * vhidmini2 sample in microsoft/Windows-driver-samples, which is licensed under the Microsoft * Public License (MS-PL); see README.md and LICENSE.txt in this directory. */ diff --git a/windows/hid.c b/windows/hid.c index fceaaa05c..9b1d1c7e9 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -48,7 +48,7 @@ typedef LONG NTSTATUS; #include #include #define _wcsdup wcsdup -#define _stricmp strcasecmp +#define _strdup strdup #endif /*#define HIDAPI_USE_DDK*/ @@ -57,6 +57,7 @@ typedef LONG NTSTATUS; #include "hidapi_hidclass.h" #include "hidapi_hidsdi.h" +#include #include #include #include @@ -126,6 +127,12 @@ static CM_Get_DevNode_PropertyW_ CM_Get_DevNode_PropertyW = NULL; static CM_Get_Device_Interface_PropertyW_ CM_Get_Device_Interface_PropertyW = NULL; static CM_Get_Device_Interface_List_SizeW_ CM_Get_Device_Interface_List_SizeW = NULL; static CM_Get_Device_Interface_ListW_ CM_Get_Device_Interface_ListW = NULL; + +/* Windows 8 and up: NOT resolved by lookup_functions() - that one is mandatory + and would fail hid_init() itself on older Windows, taking the whole library + down for everyone, including those that never touch hotplug. Resolved on the + first hotplug registration instead, and only there + (see hid_internal_hotplug_resolve_cm_notification). */ static CM_Register_Notification_ CM_Register_Notification = NULL; static CM_Unregister_Notification_ CM_Unregister_Notification = NULL; @@ -141,6 +148,11 @@ static void free_library_handles() if (cfgmgr32_lib_handle) FreeLibrary(cfgmgr32_lib_handle); cfgmgr32_lib_handle = NULL; + /* Lazily resolved (see above): unlike the pointers lookup_functions() resolves + unconditionally, nothing re-resolves these on the next hid_init() unless + they are cleared here */ + CM_Register_Notification = NULL; + CM_Unregister_Notification = NULL; } static int lookup_functions() @@ -184,8 +196,6 @@ static int lookup_functions() RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_PropertyW); RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_List_SizeW); RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_ListW); - RESOLVE(cfgmgr32_lib_handle, CM_Register_Notification); - RESOLVE(cfgmgr32_lib_handle, CM_Unregister_Notification); #undef RESOLVE @@ -216,10 +226,59 @@ struct hid_device_ { DWORD write_timeout_ms; }; +/* The threadpool work item that delivers the HID_API_HOTPLUG_ENUMERATE pass is + Windows Vista and up, so - like every other API this file uses above its + minimum target - it is resolved dynamically: a static import would raise the + Windows version hidapi can be loaded on for every user, including those that + never use hotplug. Hotplug registration fails with an error when it is not + available; nothing else in the library depends on it. + + The threadpool handles are declared as opaque pointers (which is what they are + in the SDK, too), so that none of this needs headers newer than the file's + minimum target. */ +typedef VOID (WINAPI *hid_internal_tp_work_callback)(PVOID instance, PVOID context, PVOID work); +typedef PVOID (WINAPI *CreateThreadpoolWork_)(hid_internal_tp_work_callback callback, PVOID context, PVOID callback_environ); +typedef VOID (WINAPI *SubmitThreadpoolWork_)(PVOID work); +typedef VOID (WINAPI *CloseThreadpoolWork_)(PVOID work); +typedef VOID (WINAPI *WaitForThreadpoolWorkCallbacks_)(PVOID work, BOOL cancel_pending); + +static CreateThreadpoolWork_ hid_internal_CreateThreadpoolWork = NULL; +static SubmitThreadpoolWork_ hid_internal_SubmitThreadpoolWork = NULL; +static CloseThreadpoolWork_ hid_internal_CloseThreadpoolWork = NULL; +static WaitForThreadpoolWorkCallbacks_ hid_internal_WaitForThreadpoolWorkCallbacks = NULL; + static struct hid_hotplug_context { /* Win32 notification handle */ HCMNOTIFICATION notify_handle; + /* Threadpool work item (a PTP_WORK): delivers pending snapshots and recovered + arrivals, and performs cleanup deferred from the notification callback. + Created with the first callback registration, closed by hid_exit(). */ + PVOID event_work; + unsigned char work_submitted; /* Guarded by the critical section */ + + /* Number of notification handles detached from the context whose + CM_Unregister_Notification call has not completed yet, and a manual-reset + event that is signaled exactly while that count is zero. Both are guarded + by the critical section (the event is only ever waited on without it). + A new notification is only armed once the count is zero: the OS keeps a + detached handle live until the unregistration completes, and two live + registrations would deliver every event twice. */ + LONG pending_unregistrations; + HANDLE quiescent_event; + + /* Set when CM_Unregister_Notification failed: the OS-side registration may + still be live and call into this module at any time. Sticky for the whole + process - the state such a notification can reach is never destroyed, the + libraries it calls into are never unloaded, module pinning is attempted, and no + second notification is ever armed next to it (every event would be + delivered twice). */ + unsigned char notification_leaked; + + /* Failed CONFIGRET recorded since the previous hid_exit, including earlier + deregistration, event work and registration-unwind failures. */ + CONFIGRET unregistration_error; + /* Critical section (faster mutex substitute), for both cached device list and callback list changes */ CRITICAL_SECTION critical_section; @@ -234,8 +293,14 @@ static struct hid_hotplug_context { /* Linked list of the hotplug callbacks */ struct hid_hotplug_callback *hotplug_cbs; - /* Linked list of the device infos (mandatory when the device is disconnected) */ + /* Recovered arrivals, dispatched between events in registration order */ + struct hid_hotplug_recovered_event *recovered_events; + + /* Linked list of the device infos (mandatory when the device is disconnected). + Doubles as the arrival dedupe set: an arrival for a path that is already in + here has already been reported or queued (see hid_internal_notify_callback). */ struct hid_device_info *devs; + unsigned char devs_incomplete; /* An arrival could not be allocated */ } hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ static hid_device *new_hid_device() @@ -283,7 +348,7 @@ static void free_hid_device(hid_device *dev) free(dev); } -static void register_winapi_error_to_buffer(wchar_t **error_buffer, const WCHAR *op) +static void register_winapi_error_code_to_buffer(wchar_t **error_buffer, const WCHAR *op, DWORD error_code) { free(*error_buffer); *error_buffer = NULL; @@ -294,7 +359,6 @@ static void register_winapi_error_to_buffer(wchar_t **error_buffer, const WCHAR } WCHAR system_err_buf[1024]; - DWORD error_code = GetLastError(); DWORD system_err_len = FormatMessageW( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, @@ -340,6 +404,15 @@ static void register_winapi_error_to_buffer(wchar_t **error_buffer, const WCHAR } } +static void register_winapi_error_to_buffer(wchar_t **error_buffer, const WCHAR *op) +{ + /* Capture the error code first: free() (and, for the global error buffer, + acquiring its lock) is not required to preserve it */ + DWORD error_code = GetLastError(); + + register_winapi_error_code_to_buffer(error_buffer, op, error_code); +} + #if defined(__GNUC__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Warray-bounds" @@ -373,16 +446,123 @@ static void register_string_error(hid_device *dev, const WCHAR *string_error) register_string_error_to_buffer(&dev->last_error_str, string_error); } +/* A minimal exclusive lock that needs no initialization. + + Deliberately not an SRWLOCK (nor a CONDITION_VARIABLE, nor the CRITICAL_SECTION + that cannot be initialized statically): those APIs are Windows Vista and up, so + using them would turn hidapi into a load-time importer of Vista-only kernel32 + symbols - for every user of the library, including those that never touch + hotplug. The historical DLL-load target is XP, subject to compiler/runtime + requirements (notably clang-MinGW may require newer Windows). Enumeration + needs Vista-era APIs; hotplug notifications need Windows 8. This is not a + blanket XP support promise. In non-DDK builds these newer APIs are resolved + dynamically (see lookup_functions, hid_internal_hotplug_resolve_threadpool + and hid_internal_hotplug_resolve_cm_notification). + The regions guarded by this lock are kept deliberately small - pointer swaps + and flag/counter updates; the one-time bootstrap of the hotplug machinery is + the largest. */ +typedef volatile LONG hid_internal_lock; + +static void hid_internal_lock_acquire(hid_internal_lock *lock) +{ + unsigned int attempts = 0; + + while (InterlockedCompareExchange(lock, 1, 0) != 0) { + /* Sleep(0) yields the rest of the quantum, but only to threads of equal + or higher priority: if the holder is lower-priority (or starved on a + single-CPU system), spinning on it can burn quanta without any + progress. After a few attempts, Sleep(1) instead: it yields to any + ready thread. */ + if (++attempts < 16) { + Sleep(0); + } else { + Sleep(1); + } + } +} + +static void hid_internal_lock_release(hid_internal_lock *lock) +{ + InterlockedExchange(lock, 0); +} + static wchar_t *last_global_error_str = NULL; +/* Callbacks may run on a CM or threadpool thread, but all invocations hold the + same critical section. Only one thread can therefore own this marker at a + time. Set it immediately around each invocation and restore it for nesting; + zero means inactive. Interlocked access needs no allocation or bootstrap and + remains valid when leaked notifications retain the machinery. */ +static volatile LONG hid_hotplug_callback_thread = 0; + +static int hid_internal_in_hotplug_callback(void) +{ + DWORD error = GetLastError(); + DWORD callback_thread = (DWORD)InterlockedCompareExchange(&hid_hotplug_callback_thread, 0, 0); + int in_callback; + + in_callback = (callback_thread != 0 && callback_thread == GetCurrentThreadId()); + SetLastError(error); + + return in_callback; +} + +/* Serializes mutations of last_global_error_str: the hotplug API is + thread-safe and its failure paths may write the global error concurrently. + Note that this only protects writers against each other: hid_error(NULL) + hands the raw string pointer out to the application without any lock, which + is why the header requires the application to serialize hid_error(NULL) + against the hotplug API. HIDAPI's own code on the internal event context + never writes the global error - not even a user callback that re-enters the + public hotplug API from that context: register_global_error_message() drops + those writes (see hid_internal_in_hotplug_callback). An application therefore never + has to serialize hid_error(NULL) against a write it could not see coming. */ +static hid_internal_lock global_error_lock = 0; + +/* Publishes a message (built by the caller, ownership taken) as the global + error string. Only the pointer swap is under the lock: it is a spinlock, and + building or freeing a message is far too heavy for a spin-guarded region. */ +static void register_global_error_message(wchar_t *msg) +{ + wchar_t *old_msg; + + /* A user callback runs on HIDAPI's internal event context (a threadpool + work item or the CM notification callback). A nested public hotplug call + made from such a callback must not touch last_global_error_str - success + clear included: the write happens on the event context, and the + application cannot serialize its lock-free hid_error(NULL) read against it. + The marker identifies only the executing callback thread, so a concurrent + application thread still records its own errors. */ + if (hid_internal_in_hotplug_callback()) { + free(msg); + return; + } + + hid_internal_lock_acquire(&global_error_lock); + old_msg = last_global_error_str; + last_global_error_str = msg; + hid_internal_lock_release(&global_error_lock); + + free(old_msg); +} + +static void register_global_winapi_error_code(DWORD error_code, const WCHAR *op) +{ + wchar_t *msg = NULL; + + register_winapi_error_code_to_buffer(&msg, op, error_code); + register_global_error_message(msg); +} + static void register_global_winapi_error(const WCHAR *op) { - register_winapi_error_to_buffer(&last_global_error_str, op); + /* Capture the error code first: building the message may clobber it */ + register_global_winapi_error_code(GetLastError(), op); } static void register_global_error(const WCHAR *string_error) { - register_string_error_to_buffer(&last_global_error_str, string_error); + register_global_error_message(string_error ? _wcsdup(string_error) : NULL); } static HANDLE open_device(const wchar_t *path, BOOL open_rw) @@ -412,19 +592,232 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) return HID_API_VERSION_STR; } -static void hid_internal_hotplug_init() +/* Serializes the bootstrap and the teardown of the hotplug machinery (and the + flag and counter below): two racing first registrations must not both + initialize the critical section, and every read of mutex_ready that is not + already made under the critical section is made under this lock (it is what + publishes the critical section to other threads). Statically initialized: + guarding state with it costs no OS object. */ +static hid_internal_lock hotplug_init_lock = 0; + +/* Set while hid_exit() is tearing the hotplug machinery down (and unloading the + resolved libraries): hotplug registration fails instead of arming a context + being destroyed or calling into libraries being unloaded. Deregistration may + still disarm a callback while mutex_ready remains published, but leaves OS + cleanup to hid_exit. Guarded by hotplug_init_lock, NOT by the critical section: + hid_exit() must be able to raise it before it can know whether the machinery + (and with it the critical section) even exists - and without creating it, as + a program that never uses hotplug must not have hid_exit() create OS objects + on its behalf. */ +static LONG hotplug_exiting = 0; + +/* Every public caller admitted by hid_internal_hotplug_enter: inside or waiting + for the critical section, waiting on quiescence, or completing detached OS + unregistration. Guarded by hotplug_init_lock. At zero, hid_exit atomically + unpublishes mutex_ready under this lock before destroying synchronization + objects; until then, non-bootstrap deregistration can still be admitted. */ +static LONG hotplug_machinery_users = 0; + +/* Resolves the threadpool API used as the hotplug event context. + Always called inside the critical section. Returns -1 when it is unavailable + (the OS predates it), in which case hotplug is not available either. */ +static int hid_internal_hotplug_resolve_threadpool(void) +{ + HMODULE kernel32; + + if (hid_internal_CreateThreadpoolWork != NULL) { + /* Already resolved: resolved last, so it doubles as the "all set" flag */ + return 0; + } + + /* kernel32.dll is mapped into every process and is never unloaded, so its + handle needs neither LoadLibrary nor FreeLibrary */ + kernel32 = GetModuleHandleW(L"kernel32.dll"); + if (kernel32 == NULL) { + return -1; + } + +/* Avoid direct function-pointer cast from FARPROC to typed callback pointer. + Using memcpy keeps this warning-free regardless of the compiler and compiler settings. */ +#define RESOLVE_TP(x) do { \ + FARPROC proc_addr = GetProcAddress(kernel32, #x); \ + if (!proc_addr) return -1; \ + memcpy(&hid_internal_##x, &proc_addr, sizeof(hid_internal_##x)); \ +} while (0) + + RESOLVE_TP(SubmitThreadpoolWork); + RESOLVE_TP(CloseThreadpoolWork); + RESOLVE_TP(WaitForThreadpoolWorkCallbacks); + RESOLVE_TP(CreateThreadpoolWork); + +#undef RESOLVE_TP + + return 0; +} + +/* Resolves the PnP notification API that delivers the hotplug events. Windows 8 + and up, so - like the threadpool API above - it is resolved here and not in + the mandatory lookup_functions(): a failure there would fail hid_init() as a + whole on older Windows. The critical section orders setup against teardown; + hotplug_exiting gates registration, the user count retains synchronization + objects, and OS unregistration plus the work wait drain internal contexts + before hid_exit unloads cfgmgr32.dll. + Returns -1 when the API is unavailable (the OS predates it), in which case + hotplug is not available either. */ +static int hid_internal_hotplug_resolve_cm_notification(void) +{ +#ifdef HIDAPI_USE_DDK + /* Statically imported from cfgmgr32.lib in this build */ + return 0; +#else + if (CM_Unregister_Notification != NULL) { + /* Already resolved: resolved last, so it doubles as the "all set" flag */ + return 0; + } + + if (cfgmgr32_lib_handle == NULL) { + /* hid_init() has not run (or hid_exit() unloaded the library again) */ + return -1; + } + +/* Avoid direct function-pointer cast from FARPROC to typed callback pointer. + Using memcpy keeps this warning-free regardless of the compiler and compiler settings. */ +#define RESOLVE_CM(x) do { \ + FARPROC proc_addr = GetProcAddress(cfgmgr32_lib_handle, #x); \ + if (!proc_addr) return -1; \ + memcpy(&x, &proc_addr, sizeof(x)); \ +} while (0) + + RESOLVE_CM(CM_Register_Notification); + RESOLVE_CM(CM_Unregister_Notification); + +#undef RESOLVE_CM + + return 0; +#endif +} + +/* Bootstraps the hotplug machinery: the critical section that guards all + hotplug state and the manual-reset quiescence event. + Must be called with hotplug_init_lock held. On failure the machinery is not + created and *create_error / *create_op receive the error code and the name of + the call that failed (reporting them is left to the caller: this runs under a + spinlock). + Once created, the machinery stays valid for as long as anything can enter it: + hid_exit() destroys it only after proving nothing can (see + hid_internal_hotplug_enter and hid_internal_hotplug_exit), and when a + notification could not be unregistered it is never destroyed at all, so that + a live OS callback can never enter a deleted critical section. */ +static int hid_internal_hotplug_init_under_lock(DWORD *create_error, const WCHAR **create_op) +{ + if (hid_hotplug_context.mutex_ready) { + return 0; + } + + /* Manual reset, initially signaled: nothing is pending yet */ + hid_hotplug_context.quiescent_event = CreateEvent(NULL, TRUE, TRUE, NULL); + if (hid_hotplug_context.quiescent_event == NULL) { + *create_error = GetLastError(); + *create_op = L"hid_hotplug_register_callback/CreateEvent"; + return -1; + } + + if (!InitializeCriticalSectionAndSpinCount(&hid_hotplug_context.critical_section, 0)) { + *create_error = GetLastError(); + *create_op = L"hid_hotplug_register_callback/InitializeCriticalSectionAndSpinCount"; + CloseHandle(hid_hotplug_context.quiescent_event); + hid_hotplug_context.quiescent_event = NULL; + return -1; + } + + hid_hotplug_context.mutex_in_use = 0; + hid_hotplug_context.cb_list_dirty = 0; + hid_hotplug_context.pending_unregistrations = 0; + if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) + hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + + /* Set state to Ready. Published last: a thread that observes this + under hotplug_init_lock also observes everything above. */ + hid_hotplug_context.mutex_ready = 1; + + return 0; +} + +/* Result codes of hid_internal_hotplug_enter (0 is success) */ +#define HID_HOTPLUG_ENTER_EXITING 1 /* hid_exit() is in progress */ +#define HID_HOTPLUG_ENTER_NOT_READY 2 /* no machinery and bootstrap not requested */ +#define HID_HOTPLUG_ENTER_FAILED 3 /* bootstrap failed (global error registered) */ + +/* Counts the calling thread into the hotplug machinery, bootstrapping it first + when `bootstrap` is set. While a thread is counted in, the critical section + and the quiescence event exist and stay valid: hid_exit() destroys them only + after the count reaches zero and mutex_ready is unpublished atomically under + hotplug_init_lock. Exit rejects bootstrap entry but permits deregistration + while the machinery is published. A successful call MUST be balanced with + hid_internal_hotplug_leave(). */ +static int hid_internal_hotplug_enter(int bootstrap) { - if (!hid_hotplug_context.mutex_ready) { - InitializeCriticalSection(&hid_hotplug_context.critical_section); + int result = 0; + DWORD create_error = 0; + const WCHAR *create_op = NULL; + + hid_internal_lock_acquire(&hotplug_init_lock); + if (hotplug_exiting && bootstrap) { + result = HID_HOTPLUG_ENTER_EXITING; + } else if (!bootstrap && !hid_hotplug_context.mutex_ready) { + result = HID_HOTPLUG_ENTER_NOT_READY; + } else if (hid_internal_hotplug_init_under_lock(&create_error, &create_op) < 0) { + result = HID_HOTPLUG_ENTER_FAILED; + } else { + hotplug_machinery_users++; + } + hid_internal_lock_release(&hotplug_init_lock); - /* Set state to Ready */ - hid_hotplug_context.mutex_ready = 1; - hid_hotplug_context.mutex_in_use = 0; - hid_hotplug_context.cb_list_dirty = 0; - if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) - hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + if (result == HID_HOTPLUG_ENTER_FAILED) { + register_global_winapi_error_code(create_error, create_op); } + + return result; +} + +static void hid_internal_hotplug_leave(void) +{ + hid_internal_lock_acquire(&hotplug_init_lock); + hotplug_machinery_users--; + hid_internal_lock_release(&hotplug_init_lock); +} + +/* Whether hid_exit() is currently tearing the machinery down. Re-checked under + the critical section by callers that were already counted in when hid_exit() + started: `hotplug_exiting` may be raised while they hold - or wait on - the + critical section, and they must not arm anything behind the teardown. */ +static int hid_internal_hotplug_exiting(void) +{ + int exiting; + + hid_internal_lock_acquire(&hotplug_init_lock); + exiting = (hotplug_exiting != 0); + hid_internal_lock_release(&hotplug_init_lock); + + return exiting; +} + +#ifndef HIDAPI_USE_DDK +/* Whether the critical section exists and may be entered. Only used by + hid_exit() itself (via hid_internal_hotplug_notification_leaked), on the same + thread that is the only one allowed to destroy the machinery, so the answer + cannot go stale between the check and the EnterCriticalSection. */ +static int hid_internal_hotplug_ready(void) +{ + int ready; + + hid_internal_lock_acquire(&hotplug_init_lock); + ready = hid_hotplug_context.mutex_ready; + hid_internal_lock_release(&hotplug_init_lock); + + return ready; } +#endif int HID_API_EXPORT hid_init(void) { @@ -451,15 +844,221 @@ struct hid_hotplug_callback { void *user_data; hid_hotplug_callback_fn callback; + /* Registration-time HID_API_HOTPLUG_ENUMERATE snapshot, + still to be replayed to this callback as synthetic + HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED events on the event context */ + struct hid_device_info *replay; + /* Pointer to the next notification */ struct hid_hotplug_callback *next; }; -static void hid_internal_hotplug_remove_postponed() +struct hid_hotplug_recovered_event { + struct hid_device_info *device; + /* Handles increase monotonically: only earlier registrations receive this + arrival. The repairing registration gets it through its own snapshot. */ + hid_hotplug_callback_handle before_handle; + struct hid_hotplug_recovered_event *next; +}; + +static void hid_internal_hotplug_free_recovered(struct hid_hotplug_recovered_event *events) +{ + while (events != NULL) { + struct hid_hotplug_recovered_event *next = events->next; + hid_free_enumeration(events->device); + free(events); + events = next; + } +} + +static struct hid_device_info *hid_internal_copy_device_info(const struct hid_device_info *src) +{ + struct hid_device_info *dst = (struct hid_device_info *)calloc(1, sizeof(struct hid_device_info)); + + if (dst == NULL) { + return NULL; + } + + *dst = *src; + dst->next = NULL; + dst->path = NULL; + dst->serial_number = NULL; + dst->manufacturer_string = NULL; + dst->product_string = NULL; + + if ((src->path && (dst->path = _strdup(src->path)) == NULL) + || (src->serial_number && (dst->serial_number = _wcsdup(src->serial_number)) == NULL) + || (src->manufacturer_string && (dst->manufacturer_string = _wcsdup(src->manufacturer_string)) == NULL) + || (src->product_string && (dst->product_string = _wcsdup(src->product_string)) == NULL)) { + hid_free_enumeration(dst); + return NULL; + } + + return dst; +} + +/* Attempts to pin the image this code lives in, so it stays mapped even if the + application unloads hidapi: a notification that could not be unregistered is + still live at the OS level and will call hid_internal_notify_callback. + FROM_ADDRESS identifies whichever image contains hidapi, including the host + executable or DLL in a static build. hid_hotplug_context is in the same + translation unit as the callback. This is best-effort containment: the BOOL + result is unchecked, so retention of the image is not guaranteed on failure. */ +static void hid_internal_hotplug_pin_module(void) +{ + HMODULE module = NULL; + + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (LPCWSTR)(void *)&hid_hotplug_context, &module); +} + +/* ASCII case folding: locale-independent, and all an interface path needs + (this is also all the _stricmp() below used to do, in the C locale) */ +static unsigned char hid_internal_ascii_tolower(unsigned char c) +{ + return (c >= 'A' && c <= 'Z') ? (unsigned char)(c - 'A' + 'a') : c; +} + +/* Compares a cached (UTF-8) interface path with the (UTF-16) one a notification + carries, WITHOUT ALLOCATING: it encodes the UTF-16 path to UTF-8 on the fly, one + code point at a time, and matches it against the bytes of the cached one. + + Both hotplug cache lookups need this comparison, and both used to convert the + notification's symbolic link to UTF-8 first - which allocates. That allocation + failing in the REMOVAL lookup would process the removal but leave its cache + record behind, and a stale record is unrecoverable: it is the arrival dedupe, so + the next connection on that interface path (paths are reused when a device is + replugged into the same port) would be classified as a duplicate and suppressed + forever, for every callback, including later HID_API_HOTPLUG_ENUMERATE passes. + Allocation-free removal lookup cannot strand a cached record. Describing an + arrival still allocates: failure drops that arrival and marks the cache + incomplete for repair before a later ENUMERATE snapshot succeeds. + + The comparison is case-independent for ASCII, exactly like the _stricmp() it + replaces. A cached path is always a WC_ERR_INVALID_CHARS conversion of an + interface path (see hid_internal_UTF16toUTF8 and hid_internal_get_device_info), + so it always encodes well-formed UTF-16: an ill-formed symbolic link cannot be in + the cache, and comparing unequal is the correct answer for it - which is what the + failing conversion used to yield as well. */ +static int hid_internal_path_equals(const char *cached_path, const wchar_t *interface_path) +{ + const unsigned char *cached = (const unsigned char *)cached_path; + + while (*interface_path != L'\0') { + unsigned long code_point = (unsigned long)*interface_path++; + unsigned char utf8[4]; + size_t len, i; + + if (code_point >= 0xD800UL && code_point <= 0xDBFFUL) { + /* A high surrogate must be followed by a low one */ + if (*interface_path < 0xDC00 || *interface_path > 0xDFFF) { + return 0; + } + code_point = 0x10000UL + ((code_point - 0xD800UL) << 10) + (unsigned long)(*interface_path++ - 0xDC00); + } else if (code_point >= 0xDC00UL && code_point <= 0xDFFFUL) { + /* An unpaired low surrogate */ + return 0; + } + + if (code_point < 0x80UL) { + utf8[0] = (unsigned char)code_point; + len = 1; + } else if (code_point < 0x800UL) { + utf8[0] = (unsigned char)(0xC0UL | (code_point >> 6)); + utf8[1] = (unsigned char)(0x80UL | (code_point & 0x3FUL)); + len = 2; + } else if (code_point < 0x10000UL) { + utf8[0] = (unsigned char)(0xE0UL | (code_point >> 12)); + utf8[1] = (unsigned char)(0x80UL | ((code_point >> 6) & 0x3FUL)); + utf8[2] = (unsigned char)(0x80UL | (code_point & 0x3FUL)); + len = 3; + } else { + utf8[0] = (unsigned char)(0xF0UL | (code_point >> 18)); + utf8[1] = (unsigned char)(0x80UL | ((code_point >> 12) & 0x3FUL)); + utf8[2] = (unsigned char)(0x80UL | ((code_point >> 6) & 0x3FUL)); + utf8[3] = (unsigned char)(0x80UL | (code_point & 0x3FUL)); + len = 4; + } + + /* No byte of an encoded code point is ever '\0', so a cached path that ends + early simply compares unequal here: the walk cannot run past its end */ + for (i = 0; i < len; ++i) { + if (hid_internal_ascii_tolower(*cached) != hid_internal_ascii_tolower(utf8[i])) { + return 0; + } + ++cached; + } + } + + return *cached == '\0'; +} + +/* Tells whether an interface path is already in the device cache - i.e. whether + its connection has already been reported. Always called inside a locked mutex. + + This is the whole arrival dedupe. The notification is armed BEFORE the + registration-time enumeration runs (a device connecting in between must not be + missed by both), so a device that arrives in that window is captured by the + enumeration AND has an arrival notification in flight; that notification must + not report - or cache - the same connection a second time. + + The cache suppresses that overlap (and arrivals already recovered by an OOM + repair). Removal drops the entry so a later re-plug can be reported again. + There is a separate arm-window ambiguity: an old removal can remain queued + while enumeration records a same-path replacement, yielding replay ARRIVED, + LEFT with replacement metadata, then live ARRIVED for the replacement. + Path-only notification data cannot reliably distinguish these connection + instances, even with sequential dispatch. A deterministic regression needs + a pause/injection seam between arming notifications and enumeration. + + That last step does assume the OS dispatches ONE registration's notifications + sequentially, i.e. in delivery order. If a removal and the arrival of a re-plug + onto the same path were ever dispatched concurrently, the arrival could observe + the cache entry the removal has not dropped yet, be mistaken for the arm-window + duplicate above and be dropped whole - after which the removal drops the stale + entry, and the new connection is reported neither ARRIVED nor LEFT. MSDN + documents no such ordering guarantee for CM_Register_Notification, but the CM + machinery does dispatch a registration's events sequentially in practice. + A raw-event FIFO would need a serialized consumer: here the critical section, + not the identity of one TP_WORK object, serializes execution. Enqueue or + critical-section entry order cannot recover missing PnP ordering information, + so a FIFO alone would not remove that sequential ordered-delivery assumption. */ +static int hid_internal_hotplug_is_cached(const wchar_t *interface_path) +{ + for (struct hid_device_info *device = hid_hotplug_context.devs; device != NULL; device = device->next) { + /* Case-independent path comparison is mandatory */ + if (device->path != NULL && hid_internal_path_equals(device->path, interface_path)) { + return 1; + } + } + + return 0; +} + +/* Unlinks the cached device with this interface path, if there is one, and hands + it to the caller (who owns it). Always called inside a locked mutex. Allocates + nothing: see hid_internal_path_equals. */ +static struct hid_device_info *hid_internal_hotplug_take_cached_device(const wchar_t *interface_path) +{ + for (struct hid_device_info **current = &hid_hotplug_context.devs; *current != NULL; current = &(*current)->next) { + /* Case-independent path comparison is mandatory */ + if ((*current)->path != NULL && hid_internal_path_equals((*current)->path, interface_path)) { + struct hid_device_info *device = *current; + *current = device->next; + device->next = NULL; + return device; + } + } + + return NULL; +} + +static void hid_internal_hotplug_remove_postponed(void) { /* Unregister the callbacks whose removal was postponed */ /* This function is always called inside a locked mutex */ /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ + /* mutex_ready is defensive: the caller already holds a live critical section. */ if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { return; } @@ -470,75 +1069,408 @@ static void hid_internal_hotplug_remove_postponed() struct hid_hotplug_callback *callback = *current; if (!callback->events) { *current = (*current)->next; + hid_free_enumeration(callback->replay); free(callback); continue; } current = &callback->next; } - + /* Clear the flag so we don't start the cycle unless necessary */ hid_hotplug_context.cb_list_dirty = 0; } -static void hid_internal_hotplug_cleanup() +/* Completes the unregistration of a notification handle detached by + hid_internal_hotplug_cleanup. Must be called OUTSIDE the critical section: + CM_Unregister_Notification waits for in-progress notification callbacks, + which may themselves be blocked on the critical section. */ +static CONFIGRET hid_internal_hotplug_finish_unregistration(HCMNOTIFICATION notify_handle) +{ + CONFIGRET cr; + + if (notify_handle == NULL) { + /* Nothing was detached - and on a teardown that never armed a notification + CM_Unregister_Notification is not even resolved: a handle can only exist + once it is (see hid_internal_hotplug_resolve_cm_notification) */ + return CR_SUCCESS; + } + + cr = CM_Unregister_Notification(notify_handle); + + EnterCriticalSection(&hid_hotplug_context.critical_section); + if (cr != CR_SUCCESS) { + /* Setting notification_leaked makes hid_exit retain the critical section + and quiescence event for late notifications; normal teardown destroys + them (see keep_machinery in hid_internal_hotplug_exit). The resolved + libraries also survive, module pinning is attempted, and no second + notification is armed. Record the failure for hid_exit, including when + this runs on the internal event context, which must remain quiet. An + application-thread deregistration also reports the returned CONFIGRET; + failed registration unwind preserves its original error. */ + hid_hotplug_context.notification_leaked = 1; + hid_hotplug_context.unregistration_error = cr; + hid_internal_hotplug_pin_module(); + } + hid_hotplug_context.pending_unregistrations--; + if (hid_hotplug_context.pending_unregistrations == 0) { + SetEvent(hid_hotplug_context.quiescent_event); + } + LeaveCriticalSection(&hid_hotplug_context.critical_section); + return cr; +} + +/* Waits until every detached notification handle has been unregistered. + Must be called WITHOUT the critical section: the unregistration completes on + another thread, which needs it. */ +static DWORD hid_internal_hotplug_wait_quiescent(void) +{ + DWORD wait_error = ERROR_SUCCESS; + int wait_failed = 0; + + for (;;) { + int pending; + + EnterCriticalSection(&hid_hotplug_context.critical_section); + pending = (hid_hotplug_context.pending_unregistrations > 0); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + if (!pending) { + return wait_error; + } + + if (wait_failed) { + /* Retain reachable objects until the owner finishes even if the event + wait failed. Poll outside the section so the owner can complete. */ + Sleep(1); + } else if (WaitForSingleObject(hid_hotplug_context.quiescent_event, INFINITE) == WAIT_FAILED) { + wait_error = GetLastError(); + wait_failed = 1; + } + } +} + +/* Always called inside a locked mutex. + When the last callback is gone, tears the machinery down and returns the + Win32 notification handle, which the caller MUST hand to + hid_internal_hotplug_finish_unregistration after leaving the critical + section (never under it, and never from the notification callback itself: + the notification callback defers the teardown to the threadpool work item). */ +static HCMNOTIFICATION hid_internal_hotplug_cleanup(void) { + HCMNOTIFICATION notify_handle; + + /* mutex_ready is defensive: the caller already holds a live critical section. */ if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { - return; + return NULL; } /* Before checking if the list is empty, clear any entries whose removal was postponed first */ hid_internal_hotplug_remove_postponed(); /* Unregister the HID device connection notification when removing the last callback */ - /* This function is always called inside a locked mutex */ if (hid_hotplug_context.hotplug_cbs != NULL) { - return; + return NULL; } + hid_internal_hotplug_free_recovered(hid_hotplug_context.recovered_events); + hid_hotplug_context.recovered_events = NULL; + if (hid_hotplug_context.devs) { /* Cleanup connected device list */ hid_free_enumeration(hid_hotplug_context.devs); hid_hotplug_context.devs = NULL; } - if (hid_hotplug_context.notify_handle) { - if (CM_Unregister_Notification(hid_hotplug_context.notify_handle) != CR_SUCCESS) { - /* We mark an error, but we proceed with the cleanup */ - register_global_error(L"CM_Unregister_Notification failed for Hotplug notification"); + notify_handle = hid_hotplug_context.notify_handle; + hid_hotplug_context.notify_handle = NULL; + if (notify_handle != NULL) { + /* Balanced by hid_internal_hotplug_finish_unregistration */ + hid_hotplug_context.pending_unregistrations++; + ResetEvent(hid_hotplug_context.quiescent_event); + } + return notify_handle; +} + +/* Deliver (and consume) a callback's pending snapshot. + Always called inside a locked mutex, with mutex_in_use set. */ +static void hid_internal_hotplug_replay_flush(struct hid_hotplug_callback *callback) +{ + while (callback->replay != NULL) { + struct hid_device_info *device = callback->replay; + callback->replay = device->next; + device->next = NULL; + + if (!callback->events) { + /* The callback was deregistered while the pass was pending */ + hid_free_enumeration(device); + continue; + } + + /* Mark this thread as the event context for the duration of the call, so a + nested public hotplug call does not write the global error (save/restore + keeps nested callbacks composing correctly). */ + LONG prev_in_cb = InterlockedExchange(&hid_hotplug_callback_thread, (LONG)GetCurrentThreadId()); + int result = (*callback->callback)(callback->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, callback->user_data); + InterlockedExchange(&hid_hotplug_callback_thread, prev_in_cb); + hid_free_enumeration(device); + + /* A non-zero result stops the remainder of the pass and deregisters the callback */ + if (result) { + callback->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + hid_free_enumeration(callback->replay); + callback->replay = NULL; + } + } +} + +static void hid_internal_hotplug_recovered_flush(void); + +/* Threadpool work item: delivers pending snapshots and recovered arrivals + (unless a live event got to them first) and performs the cleanup the + notification callback is not allowed to perform itself. */ +static VOID WINAPI hid_internal_hotplug_event_work(PVOID instance, PVOID context, PVOID work) +{ + HCMNOTIFICATION notify_handle; + + (void)instance; + (void)context; + (void)work; + + EnterCriticalSection(&hid_hotplug_context.critical_section); + hid_hotplug_context.work_submitted = 0; + + hid_internal_hotplug_recovered_flush(); + hid_hotplug_context.mutex_in_use = 1; + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; callback != NULL; callback = callback->next) { + hid_internal_hotplug_replay_flush(callback); + } + hid_hotplug_context.mutex_in_use = 0; + hid_internal_hotplug_recovered_flush(); + + notify_handle = hid_internal_hotplug_cleanup(); + + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + hid_internal_hotplug_finish_unregistration(notify_handle); +} + +/* Called with the critical section held; at most one pass waits to acquire it. */ +static void hid_internal_hotplug_submit_work(void) +{ + if (!hid_hotplug_context.work_submitted) { + hid_hotplug_context.work_submitted = 1; + hid_internal_SubmitThreadpoolWork(hid_hotplug_context.event_work); + } +} + +#ifndef HIDAPI_USE_DDK +/* Whether a notification could not be unregistered at some point in this process + and may still be live at the OS level. Sticky: see hid_hotplug_context. */ +static int hid_internal_hotplug_notification_leaked(void) +{ + int leaked; + + if (!hid_internal_hotplug_ready()) { + return 0; + } + + EnterCriticalSection(&hid_hotplug_context.critical_section); + leaked = (hid_hotplug_context.notification_leaked != 0); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + return leaked; +} +#endif + +/* Ends the teardown started by hid_internal_hotplug_exit: see `hotplug_exiting`. + Called unconditionally by hid_exit() once the resolved libraries are gone. */ +static void hid_internal_hotplug_exit_done(void) +{ + hid_internal_lock_acquire(&hotplug_init_lock); + hotplug_exiting = 0; + hid_internal_lock_release(&hotplug_init_lock); +} + +static int hid_internal_hotplug_exit(void) +{ + HCMNOTIFICATION notify_handle; + PVOID event_work; + CONFIGRET unregistration_error; + DWORD wait_error; + int keep_machinery; + int machinery_ready; + + /* Nothing may bootstrap or arm the machinery from here on. Deregistration + may still disarm callbacks while the critical section exists. Without this, a + registration waiting for a pending unregistration to complete could wake up + behind this teardown, arm a fresh notification and append a callback to a + context that is being dismantled - leaving a live notification and a + "registered" callback behind hid_exit(). It is also what keeps a concurrent + hid_hotplug_register_callback() - which is thread-safe, and initializes the + library implicitly - from calling into hid.dll/cfgmgr32.dll while hid_exit() + is unloading them, and what makes the destruction at the end of this + function safe. Lowered again by hid_internal_hotplug_exit_done(). */ + hid_internal_lock_acquire(&hotplug_init_lock); + hotplug_exiting = 1; + machinery_ready = (hid_hotplug_context.mutex_ready != 0); + hid_internal_lock_release(&hotplug_init_lock); + + if (!machinery_ready) { + /* Hotplug was never used (or a previous hid_exit() already destroyed the + machinery): nothing can be armed, and nothing was created that would + have to be freed. A registration bootstrapping the machinery right now + fails on `hotplug_exiting` before it arms anything or calls into the + libraries hid_exit() is about to unload. */ + return 0; + } + + EnterCriticalSection(&hid_hotplug_context.critical_section); + + /* Remove all callbacks from the list, including their undelivered HID_API_HOTPLUG_ENUMERATE snapshots */ + { + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current) { + struct hid_hotplug_callback *next = (*current)->next; + hid_free_enumeration((*current)->replay); + free(*current); + *current = next; + } + } + notify_handle = hid_internal_hotplug_cleanup(); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + hid_internal_hotplug_finish_unregistration(notify_handle); + + /* Quiescence: unregistrations started by concurrent (contract-legal) + hid_hotplug_deregister_callback calls must complete before anything a + notification can still reach is released */ + wait_error = hid_internal_hotplug_wait_quiescent(); + + EnterCriticalSection(&hid_hotplug_context.critical_section); + /* Consume failures since the previous exit, including earlier deregistration, + event work and registration unwind, rather than only this teardown. */ + unregistration_error = hid_hotplug_context.unregistration_error; + hid_hotplug_context.unregistration_error = CR_SUCCESS; + + event_work = hid_hotplug_context.event_work; + /* An unregistration error implies notification_leaked. + Stable at this point: no unregistration is pending anymore, and nothing + that could start one can enter behind `hotplug_exiting`. */ + keep_machinery = (hid_hotplug_context.notification_leaked != 0); + if (!keep_machinery) { + hid_hotplug_context.event_work = NULL; + } else { + /* A notification may still fire and submit to the work item: keep it + (deliberately leaked), along with the critical section, the quiescence + event and the device cache it works on */ + event_work = NULL; + } + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + if (event_work != NULL) { + /* No notification is registered anymore (CM_Unregister_Notification only + returns once its callbacks have finished) and nothing can submit new work + while `exiting` is set, so the work item can be drained and closed. + Not under the critical section: the work item takes it. + hid_exit() must not be called from a hotplug callback, so the wait cannot + deadlock on the calling thread itself. */ + hid_internal_WaitForThreadpoolWorkCallbacks(event_work, FALSE); + hid_internal_CloseThreadpoolWork(event_work); + } + + EnterCriticalSection(&hid_hotplug_context.critical_section); + /* A last-gasp notification callback may have re-added a device between the + cleanup and the completion of the unregistration */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + if (!keep_machinery) { + /* hid_exit() frees what the machinery created - one kernel event and one + critical section: a plugin-style host that repeatedly loads, uses and + unloads the library must not accumulate OS objects. Destroying them is + safe only once nothing can be inside the critical section: every + notification is unregistered and the work item is drained and closed + (established above). `hotplug_exiting` rejects new registrations; + deregistration may still enter but cannot start OS cleanup. Wait until + all admitted callers finish and atomically stop further admission. */ + for (;;) { + int busy; + + hid_internal_lock_acquire(&hotplug_init_lock); + busy = (hotplug_machinery_users != 0); + if (!busy) { + /* The same lock acquisition proves idle and stops non-bootstrap + admission; hotplug_exiting still rejects bootstrap admission. */ + hid_hotplug_context.mutex_ready = 0; + } + hid_internal_lock_release(&hotplug_init_lock); + + if (!busy) { + break; + } + Sleep(1); } + + DeleteCriticalSection(&hid_hotplug_context.critical_section); + CloseHandle(hid_hotplug_context.quiescent_event); + hid_hotplug_context.quiescent_event = NULL; } + /* else: the machinery outlives hid_exit() on purpose - a leaked notification + may still enter the critical section at any time + (see hid_internal_hotplug_init_under_lock) */ - hid_hotplug_context.notify_handle = NULL; -} + /* `hotplug_exiting` stays raised until hid_exit() is done unloading the + libraries: see hid_internal_hotplug_exit_done */ -static void hid_internal_hotplug_exit() -{ - if (!hid_hotplug_context.mutex_ready) { - /* If the critical section is not initialized, we are safe to assume nothing else is */ - return; + if (unregistration_error != CR_SUCCESS) { + wchar_t msg[160]; + HIDAPI_SWPRINTF(msg, sizeof(msg) / sizeof(msg[0]), + L"hid_exit: CM_Unregister_Notification failed (CR 0x%08X)", (unsigned int)unregistration_error); + register_global_error_message(_wcsdup(msg)); + return -1; } - EnterCriticalSection(&hid_hotplug_context.critical_section); - struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - /* Remove all callbacks from the list */ - while (*current) { - struct hid_hotplug_callback *next = (*current)->next; - free(*current); - *current = next; + if (wait_error != ERROR_SUCCESS) { + /* Report the OS synchronization failure with -1 even when the polling + fallback completed teardown without leaking an object or notification. + The wait anomaly is worth surfacing; callers can re-initialize normally. */ + register_global_winapi_error_code(wait_error, L"hid_exit/WaitForSingleObject"); + return -1; } - hid_internal_hotplug_cleanup(); - LeaveCriticalSection(&hid_hotplug_context.critical_section); - hid_hotplug_context.mutex_ready = 0; - DeleteCriticalSection(&hid_hotplug_context.critical_section); + + return 0; } int HID_API_EXPORT hid_exit(void) { - hid_internal_hotplug_exit(); + int result = hid_internal_hotplug_exit(); #ifndef HIDAPI_USE_DDK - free_library_handles(); - hidapi_initialized = FALSE; + if (!hid_internal_hotplug_notification_leaked()) { + /* Still under `exiting`: a concurrent (thread-safe) hotplug registration + fails instead of calling into libraries that are being unloaded here. + The critical section ordered setup against teardown; the user count kept + synchronization objects alive through all admitted public calls, and CM + unregistration plus the work wait drained internal contexts before those + objects were destroyed. Other APIs follow the application's documented + serialization rule. */ + free_library_handles(); + hidapi_initialized = FALSE; + } + /* else: a hotplug notification is still registered at the OS level and its + callback calls into hid.dll/cfgmgr32.dll through these handles: they stay + loaded (deliberately leaked, with module pinning attempted) so that a late + notification never calls into unloaded code */ #endif + + hid_internal_hotplug_exit_done(); + + if (result < 0) { + /* register_global_error: set by hid_internal_hotplug_exit */ + return -1; + } + register_global_error(NULL); return 0; @@ -774,7 +1706,9 @@ static hid_internal_detect_bus_type_result hid_internal_detect_bus_type(const wc wchar_t *device_id = NULL, *compatible_ids = NULL; CONFIGRET cr; DEVINST dev_node; - hid_internal_detect_bus_type_result result = { 0 }; + hid_internal_detect_bus_type_result result; + + memset(&result, 0, sizeof(result)); /* Get the device id from interface path */ device_id = (wchar_t *)hid_internal_get_device_interface_property(interface_path, &DEVPKEY_Device_InstanceId, DEVPROP_TYPE_STRING); @@ -846,13 +1780,20 @@ static hid_internal_detect_bus_type_result hid_internal_detect_bus_type(const wc return result; } -static char *hid_internal_UTF16toUTF8(const wchar_t *src) +/* Returns NULL both when `src` is not valid UTF-16 and on allocation failure. + Callers that need to tell the two apart (an invalid string is the string's + problem; running out of memory is a library failure) pass `oom`, which is set + to 1 on allocation failure and left untouched otherwise. */ +static char *hid_internal_UTF16toUTF8(const wchar_t *src, int *oom) { char *dst = NULL; int len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, NULL, 0, NULL, NULL); if (len) { dst = (char*)calloc(len, sizeof(char)); if (dst == NULL) { + if (oom) { + *oom = 1; + } return NULL; } WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, src, -1, dst, len, NULL, NULL); @@ -876,7 +1817,11 @@ static wchar_t *hid_internal_UTF8toUTF16(const char *src) return dst; } -static struct hid_device_info *hid_internal_get_device_info(const wchar_t *path, HANDLE handle) +/* Returns NULL when the device cannot be described: because the process is out + of memory (`oom` - when passed - is set to 1; a library failure) or because + `path` is not valid UTF-16 (`oom` is left untouched; the device is simply not + representable and callers skip it). */ +static struct hid_device_info *hid_internal_get_device_info(const wchar_t *path, HANDLE handle, int *oom) { struct hid_device_info *dev = NULL; /* return object */ HIDD_ATTRIBUTES attrib; @@ -891,12 +1836,21 @@ static struct hid_device_info *hid_internal_get_device_info(const wchar_t *path, dev = (struct hid_device_info*)calloc(1, sizeof(struct hid_device_info)); if (dev == NULL) { + if (oom) { + *oom = 1; + } return NULL; } /* Fill out the record */ dev->next = NULL; - dev->path = hid_internal_UTF16toUTF8(path); + dev->path = hid_internal_UTF16toUTF8(path, oom); + if (dev->path == NULL) { + /* A record without a path is useless to the caller and unusable as the key + of the hotplug device cache (where it would crash the removal lookup) */ + free(dev); + return NULL; + } dev->interface_number = -1; attrib.Size = sizeof(HIDD_ATTRIBUTES); @@ -942,6 +1896,16 @@ static struct hid_device_info *hid_internal_get_device_info(const wchar_t *path, HidD_GetProductString(handle, string, size); dev->product_string = _wcsdup(string); + if (dev->serial_number == NULL || dev->manufacturer_string == NULL || dev->product_string == NULL) { + /* Out of memory. A half-built record is not a device (and the bus-specific + fixups right below dereference these strings) */ + if (oom) { + *oom = 1; + } + hid_free_enumeration(dev); + return NULL; + } + /* now, the portion that depends on string descriptors */ switch (dev->bus_type) { case HID_API_BUS_USB: @@ -964,7 +1928,9 @@ static struct hid_device_info *hid_internal_get_device_info(const wchar_t *path, return dev; } -struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +/* Same as hid_enumerate, but distinguishes a genuine failure (*failure set + to 1, error registered) from an empty result (NULL with *failure left 0) */ +static struct hid_device_info *hid_internal_enumerate(unsigned short vendor_id, unsigned short product_id, int *failure) { struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; @@ -973,6 +1939,8 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor wchar_t* device_interface_list = NULL; DWORD len; + *failure = 1; + if (hid_init() < 0) { /* register_global_error: global error is reset by hid_init */ return NULL; @@ -1011,6 +1979,8 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor goto end_of_function; } + *failure = 0; + /* Iterate over each device interface in the HID class, looking for the right one. */ for (wchar_t* device_interface = device_interface_list; *device_interface; device_interface += wcslen(device_interface) + 1) { HANDLE device_handle = INVALID_HANDLE_VALUE; @@ -1035,10 +2005,28 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor device to the enumeration list. */ if (hid_internal_match_device_id(attrib.VendorID, attrib.ProductID, vendor_id, product_id)) { /* VID/PID match. Create the record. */ - struct hid_device_info *tmp = hid_internal_get_device_info(device_interface, device_handle); + int oom = 0; + struct hid_device_info *tmp = hid_internal_get_device_info(device_interface, device_handle, &oom); + + if (tmp == NULL && !oom) { + /* The interface path is not valid UTF-16: the device cannot be + represented to the caller. Skip it - consistently with the + hotplug notifications, which cannot report (or cache) such a + device either, so the device cache stays in step with this list. */ + goto cont_close; + } if (tmp == NULL) { - goto cont_close; + /* Out of memory. Report a failure rather than a partial list: a + list that silently misses devices is indistinguishable from the + system not having them, and the hotplug device cache and the + HID_API_HOTPLUG_ENUMERATE snapshot are built from it. */ + register_global_error(L"Failed to allocate memory for a device info"); + *failure = 1; + CloseHandle(device_handle); + hid_free_enumeration(root); + root = NULL; + goto end_of_function; } if (cur_dev) { @@ -1068,6 +2056,12 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor return root; } +struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + int failure = 0; + return hid_internal_enumerate(vendor_id, product_id, &failure); +} + void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs) { /* TODO: Merge this with the Linux version. This function is platform-independent. */ @@ -1083,7 +2077,182 @@ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *d } } -DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, CM_NOTIFY_ACTION action, PCM_NOTIFY_EVENT_DATA event_data, DWORD event_data_size) +/* Append matching copies to a staged replay. The caller owns partial results. */ +static int hid_internal_hotplug_copy_replay(const struct hid_hotplug_callback *callback, + struct hid_device_info *devs, struct hid_device_info **replay) +{ + while (*replay != NULL) { + replay = &(*replay)->next; + } + for (; devs != NULL; devs = devs->next) { + if (!hid_internal_match_device_id(devs->vendor_id, devs->product_id, callback->vendor_id, callback->product_id)) { + continue; + } + *replay = hid_internal_copy_device_info(devs); + if (*replay == NULL) { + return -1; + } + replay = &(*replay)->next; + } + return 0; +} + +/* Stage the new snapshot and any OOM repair under the critical section. No + cache or event queue changes until every allocation has succeeded. Recovered + arrivals wait for a dispatch boundary, including after reentrant repair. + + The repairing callback receives the recovered device through its own snapshot; + earlier callbacks receive it through the recovered-event queue drained at the + next dispatch boundary. Which sees it first depends on whether repair was + triggered from a replay callback, a live-dispatch callback or an application + thread. All orders satisfy the contract: the snapshot is delivered later, + and each recovered event's recipients are called in registration order. + + Recovery reports ARRIVED to every earlier matching callback armed at repair + time, even one registered without HID_API_HOTPLUG_ENUMERATE after the lost + arrival, provided it remains armed for ARRIVED at delivery. The lost arrival + cannot be ordered against intervening registrations, so consistent pairing + with the cached device's later LEFT is preferred over precision. */ +static int hid_internal_hotplug_snapshot(struct hid_hotplug_callback *callback) +{ + struct hid_hotplug_recovered_event *repairs = NULL; + struct hid_hotplug_recovered_event **tail = &repairs; + struct hid_device_info *recovered = NULL; + int enumerate_failure = 0; + + if (hid_hotplug_context.devs_incomplete) { + recovered = hid_internal_enumerate(0, 0, &enumerate_failure); + if (enumerate_failure) { + /* register_global_error: set by hid_internal_enumerate */ + return -1; + } + /* Keep only paths missing from the cache; cached metadata belongs to the + connection whose arrival has already been delivered. */ + for (struct hid_device_info **current = &recovered; *current != NULL;) { + struct hid_device_info *cached; + for (cached = hid_hotplug_context.devs; cached != NULL; cached = cached->next) { + /* Match the notification lookup's locale-independent ASCII folding. */ + const unsigned char *left = (const unsigned char *)cached->path; + const unsigned char *right = (const unsigned char *)(*current)->path; + while (*left && hid_internal_ascii_tolower(*left) == hid_internal_ascii_tolower(*right)) { + left++; + right++; + } + if (*left == *right) { + break; + } + } + if (cached != NULL) { + struct hid_device_info *duplicate = *current; + *current = duplicate->next; + duplicate->next = NULL; + hid_free_enumeration(duplicate); + } else { + current = &(*current)->next; + } + } + } + + if (hid_internal_hotplug_copy_replay(callback, hid_hotplug_context.devs, &callback->replay) < 0 + || hid_internal_hotplug_copy_replay(callback, recovered, &callback->replay) < 0) { + goto fail; + } + for (struct hid_device_info *device = recovered; device != NULL; device = device->next) { + struct hid_hotplug_recovered_event *repair = (struct hid_hotplug_recovered_event *)calloc(1, sizeof(struct hid_hotplug_recovered_event)); + if (repair == NULL) { + goto fail; + } + *tail = repair; + tail = &repair->next; + repair->before_handle = callback->handle; + repair->device = hid_internal_copy_device_info(device); + if (repair->device == NULL) { + goto fail; + } + } + + tail = &hid_hotplug_context.recovered_events; + while (*tail != NULL) { + tail = &(*tail)->next; + } + *tail = repairs; + if (recovered != NULL) { + struct hid_device_info *last = recovered; + while (last->next != NULL) { + last = last->next; + } + /* Prepend so a live arrival's device->next stays NULL even when this + registration was made from that arrival's callback. */ + last->next = hid_hotplug_context.devs; + hid_hotplug_context.devs = recovered; + hid_internal_hotplug_submit_work(); + } + hid_hotplug_context.devs_incomplete = 0; + return 0; + +fail: + register_global_error(L"Failed to allocate memory for a device info snapshot"); + hid_internal_hotplug_free_recovered(repairs); + hid_free_enumeration(recovered); + hid_free_enumeration(callback->replay); + callback->replay = NULL; + return -1; +} + +/* Delivers one event to matching callbacks registered before `before_handle`. + Always called inside a locked mutex. Does not consume `device`. */ +static void hid_internal_hotplug_dispatch(struct hid_device_info *device, hid_hotplug_event hotplug_event, + hid_hotplug_callback_handle before_handle) +{ + /* Mark the critical section as IN USE, to prevent callback removal from inside a callback */ + hid_hotplug_context.mutex_in_use = 1; + + /* Callbacks registered from inside a callback are appended to the list + and see this device in their registration-time HID_API_HOTPLUG_ENUMERATE + snapshot (or don't, for a removal): the handle cutoff binds dispatch to + the original recipients, so the connection is reported exactly once */ + + /* Call the notifications for the device */ + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; + callback != NULL && callback->handle < before_handle; callback = callback->next) { + /* The registration-time enumeration pass is always delivered + before any live events for the callback */ + hid_internal_hotplug_replay_flush(callback); + + if ((callback->events & hotplug_event) && hid_internal_match_device_id(device->vendor_id, device->product_id, callback->vendor_id, callback->product_id)) { + /* Mark this thread as the event context for the duration of the call + (see hid_internal_in_hotplug_callback); save/restore for nested callbacks. */ + LONG prev_in_cb = InterlockedExchange(&hid_hotplug_callback_thread, (LONG)GetCurrentThreadId()); + int result = (callback->callback)(callback->handle, device, hotplug_event, callback->user_data); + InterlockedExchange(&hid_hotplug_callback_thread, prev_in_cb); + + /* If the result is non-zero, we MARK the callback for future removal and proceed */ + /* We avoid changing the list until we are done calling the callbacks to simplify the process */ + if (result) { + callback->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } + } + } + + hid_hotplug_context.mutex_in_use = 0; +} + +/* Drain whole recovered events only between dispatches, never from a replay + flush or registration. Pop before dispatch so reentrant repair appends safely. + Always called inside a locked mutex, with mutex_in_use clear. */ +static void hid_internal_hotplug_recovered_flush(void) +{ + while (hid_hotplug_context.recovered_events != NULL) { + struct hid_hotplug_recovered_event *event = hid_hotplug_context.recovered_events; + hid_hotplug_context.recovered_events = event->next; + hid_internal_hotplug_dispatch(event->device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, event->before_handle); + hid_free_enumeration(event->device); + free(event); + } +} + +static DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, CM_NOTIFY_ACTION action, PCM_NOTIFY_EVENT_DATA event_data, DWORD event_data_size) { struct hid_device_info *device = NULL; hid_hotplug_event hotplug_event = (hid_hotplug_event)0; @@ -1099,85 +2268,84 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, /* Lock the mutex to avoid race conditions */ EnterCriticalSection(&hid_hotplug_context.critical_section); - if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL) { - HANDLE read_handle; + /* Drain before updating the cache: registrations made by recovered callbacks + must snapshot the state preceding this live event. */ + hid_internal_hotplug_recovered_flush(); + if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL) { hotplug_event = HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED; - /* Open read-only handle to the device */ - read_handle = open_device(event_data->u.DeviceInterface.SymbolicLink, FALSE); - - /* Check validity of read_handle. */ - if (read_handle != INVALID_HANDLE_VALUE) { - device = hid_internal_get_device_info(event_data->u.DeviceInterface.SymbolicLink, read_handle); - - /* Append to the end of the device list */ - if (hid_hotplug_context.devs != NULL) { - struct hid_device_info *last = hid_hotplug_context.devs; - while (last->next != NULL) { - last = last->next; + if (hid_internal_hotplug_is_cached(event_data->u.DeviceInterface.SymbolicLink)) { + /* This connection is already known: registration enumeration or OOM + repair cached it while the OS had an arrival queued (see + hid_internal_hotplug_is_cached). Drop it whole: no second cache + entry, no second dispatch. OOM repair queues ARRIVED for every earlier + matching callback armed at repair time, including registrations without + HID_API_HOTPLUG_ENUMERATE made after the lost arrival; see snapshot above + for the delivery conditions and pairing rationale. */ + } else { + /* Device description stays locked to preserve cache/delivery ordering. + Registration, deregistration and hid_exit latency can include driver + and string queries, complete replay passes and user callback time. */ + HANDLE read_handle = open_device(event_data->u.DeviceInterface.SymbolicLink, FALSE); + + /* Check validity of read_handle. */ + if (read_handle != INVALID_HANDLE_VALUE) { + int oom = 0; + device = hid_internal_get_device_info(event_data->u.DeviceInterface.SymbolicLink, read_handle, &oom); + if (oom) { + hid_hotplug_context.devs_incomplete = 1; } - last->next = device; - } else { - hid_hotplug_context.devs = device; + CloseHandle(read_handle); } - CloseHandle(read_handle); + if (device != NULL) { + /* Append to the end of the device list */ + if (hid_hotplug_context.devs != NULL) { + struct hid_device_info *last = hid_hotplug_context.devs; + while (last->next != NULL) { + last = last->next; + } + last->next = device; + } else { + hid_hotplug_context.devs = device; + } + } + /* else: nothing can be reported or cached for this arrival. Allocation + failure marks the cache incomplete so a later ENUMERATE snapshot + repairs missing paths and queues arrivals before the next live dispatch. + A removal still safely drops any record that was actually cached. */ } } else if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL) { - char *path; - hotplug_event = HID_API_HOTPLUG_EVENT_DEVICE_LEFT; - path = hid_internal_UTF16toUTF8(event_data->u.DeviceInterface.SymbolicLink); - - if (path != NULL) { - /* Get and remove this device from the device list */ - for (struct hid_device_info **current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { - /* Case-independent path comparison is mandatory */ - if (_stricmp((*current)->path, path) == 0) { - struct hid_device_info *next = (*current)->next; - device = *current; - device->next = NULL; - *current = next; - break; - } - } - - free(path); - } + /* Get and remove this device from the device list. Dropping the entry is + what makes a later arrival on the same path (an interface path is reused + when a device is replugged into the same port) a new connection rather + than a duplicate - see hid_internal_hotplug_is_cached. The lookup cannot + fail for lack of memory (it allocates nothing), so a removal can never + leave its record behind. */ + device = hid_internal_hotplug_take_cached_device(event_data->u.DeviceInterface.SymbolicLink); } if (device) { - /* Mark the critical section as IN USE, to prevent callback removal from inside a callback */ - hid_hotplug_context.mutex_in_use = 1; - - /* Call the notifications for the device */ - struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - while (*current) { - struct hid_hotplug_callback *callback = *current; - if ((callback->events & hotplug_event) && hid_internal_match_device_id(device->vendor_id, device->product_id, callback->vendor_id, callback->product_id)) { - int result = (callback->callback)(callback->handle, device, hotplug_event, callback->user_data); - - /* If the result is non-zero, we MARK the callback for future removal and proceed */ - /* We avoid changing the list until we are done calling the callbacks to simplify the process */ - if (result) { - callback->events = 0; - hid_hotplug_context.cb_list_dirty = 1; - } - } - current = &callback->next; - } - - hid_hotplug_context.mutex_in_use = 0; + hid_internal_hotplug_dispatch(device, hotplug_event, hid_hotplug_context.next_handle); /* Free removed device */ if (hotplug_event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT) { hid_free_enumeration(device); } - - /* Remove any callbacks that were marked for removal and stop the notification if none are left */ - hid_internal_hotplug_cleanup(); + hid_internal_hotplug_recovered_flush(); + + /* Remove any callbacks that were marked for removal; if none are left, + defer the teardown to the threadpool work item: unregistering the + notification from its own callback is not allowed (deadlock) */ + hid_internal_hotplug_remove_postponed(); + /* Defensive invariant: work exists before CM registration and is retained + until its notifications are drained; NULL is not a normal live state. */ + if (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.event_work != NULL) { + hid_internal_hotplug_submit_work(); + } } LeaveCriticalSection(&hid_hotplug_context.critical_section); @@ -1185,52 +2353,178 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, return ERROR_SUCCESS; } -int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void* user_data, hid_hotplug_callback_handle* callback_handle) +/* The registration steps that run inside the machinery. The caller has already + counted itself in with hid_internal_hotplug_enter - which is what keeps the + critical section alive for the whole call - and balances that with + hid_internal_hotplug_leave afterwards. Takes ownership of hotplug_cb: it is + freed on failure. */ +static int hid_internal_hotplug_register_counted(struct hid_hotplug_callback *hotplug_cb, int flags, hid_hotplug_callback_handle *callback_handle) { - struct hid_hotplug_callback* hotplug_cb; + /* Lock the mutex to avoid race conditions */ + EnterCriticalSection(&hid_hotplug_context.critical_section); - /* Check params */ - if (events == 0 - || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT)) - || (flags & ~(HID_API_HOTPLUG_ENUMERATE)) - || callback == NULL) { - return -1; + for (;;) { + if (hid_internal_hotplug_exiting()) { + /* hid_exit() started tearing the machinery down after this call was + counted in: arming it again behind its back would leave a live + notification and a registered callback with no context to run in */ + register_global_error(L"hid_exit() is in progress"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); + return -1; + } + + /* A notification detached by a concurrent deregistration may still be live + at the OS until its unregistration completes; arming a replacement in + that window would deliver every event twice. */ + if (hid_hotplug_context.hotplug_cbs != NULL || hid_hotplug_context.notify_handle != NULL + || hid_hotplug_context.pending_unregistrations == 0) { + break; + } + + /* The unregistration completes on another thread (or on the event context), + which needs the critical section, so the wait must not hold it. On wake the + state is re-evaluated from scratch. + This LeaveCriticalSection releases the critical section completely only + because it cannot be held recursively here: on the event context (the one + place this function runs with the critical section already held, via a + user callback registering a callback) hotplug_cbs is never NULL, so the + loop has already exited above. */ + LeaveCriticalSection(&hid_hotplug_context.critical_section); + if (WaitForSingleObject(hid_hotplug_context.quiescent_event, INFINITE) == WAIT_FAILED) { + register_global_winapi_error(L"hid_hotplug_register_callback/WaitForSingleObject"); + free(hotplug_cb); + return -1; + } + EnterCriticalSection(&hid_hotplug_context.critical_section); } - hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); + if (hid_hotplug_context.notification_leaked) { + /* A notification could not be unregistered and may still be live at the OS + level: a second one would deliver every event twice, to every callback. + The condition is sticky (and unreachable in practice), so hotplug stays + unavailable for the rest of the process. */ + register_global_error(L"A hotplug notification could not be unregistered: hotplug is no longer available"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); + return -1; + } - if (hotplug_cb == NULL) { + /* Handle values are never reused while the library remains initialized */ + if (hid_hotplug_context.next_handle >= INT_MAX) { + register_global_error(L"Hotplug callback handles exhausted"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); return -1; } + hotplug_cb->handle = hid_hotplug_context.next_handle++; - /* Fill out the record */ - hotplug_cb->next = NULL; - hotplug_cb->vendor_id = vendor_id; - hotplug_cb->product_id = product_id; - hotplug_cb->events = events; - hotplug_cb->user_data = user_data; - hotplug_cb->callback = callback; + /* Start the machinery with the first callback */ + if (hid_hotplug_context.hotplug_cbs == NULL) { + if (hid_init() < 0) { + /* register_global_error: global error is already set by hid_init */ + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); + return -1; + } - /* Ensure we are ready to actually use the mutex */ - hid_internal_hotplug_init(); + if (hid_internal_hotplug_resolve_threadpool() < 0) { + register_global_error(L"Hotplug is not supported: the threadpool API is unavailable"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); + return -1; + } - /* Lock the mutex to avoid race conditions */ - EnterCriticalSection(&hid_hotplug_context.critical_section); + if (hid_internal_hotplug_resolve_cm_notification() < 0) { + register_global_error(L"Hotplug is not supported: the PnP notification API is unavailable"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); + return -1; + } - hotplug_cb->handle = hid_hotplug_context.next_handle++; + if (hid_hotplug_context.event_work == NULL) { + hid_hotplug_context.work_submitted = 0; + hid_hotplug_context.event_work = hid_internal_CreateThreadpoolWork(hid_internal_hotplug_event_work, NULL, NULL); + if (hid_hotplug_context.event_work == NULL) { + register_global_winapi_error(L"hid_hotplug_register_callback/CreateThreadpoolWork"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); + return -1; + } + } - /* handle the unlikely case of handle overflow */ - if (hid_hotplug_context.next_handle < 0) - { - hid_hotplug_context.next_handle = 1; + if (hid_hotplug_context.notify_handle == NULL) { + GUID interface_class_guid; + CM_NOTIFY_FILTER notify_filter; + CONFIGRET cr; + int enumerate_failure = 0; + + memset(¬ify_filter, 0, sizeof(notify_filter)); + + /* Retrieve HID Interface Class GUID + https://docs.microsoft.com/windows-hardware/drivers/install/guid-devinterface-hid */ + HidD_GetHidGuid(&interface_class_guid); + + notify_filter.cbSize = sizeof(notify_filter); + notify_filter.FilterType = CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE; + notify_filter.u.DeviceInterface.ClassGuid = interface_class_guid; + + /* Register for a HID device notification when adding the first callback. + Armed BEFORE the device cache is filled: a device connecting in between + is then caught by the notification instead of being missed by both, and + the duplicate that this creates is suppressed by the cache itself + (see hid_internal_hotplug_is_cached). */ + cr = CM_Register_Notification(¬ify_filter, NULL, hid_internal_notify_callback, &hid_hotplug_context.notify_handle); + if (cr != CR_SUCCESS) { + wchar_t msg[160]; + HIDAPI_SWPRINTF(msg, sizeof(msg) / sizeof(msg[0]), + L"hid_hotplug_register_callback/CM_Register_Notification: (CR 0x%08X)", (unsigned int)cr); + register_global_error_message(_wcsdup(msg)); + hid_hotplug_context.notify_handle = NULL; + LeaveCriticalSection(&hid_hotplug_context.critical_section); + free(hotplug_cb); + return -1; + } + + /* Normally empty already; an old notification may have appended entries + between its detachment and the completion of its unregistration */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + + /* Fill already connected devices so we can use this info in disconnection + notifications and HID_API_HOTPLUG_ENUMERATE passes */ + hid_hotplug_context.devs = hid_internal_enumerate(0, 0, &enumerate_failure); + if (enumerate_failure) { + /* An empty system is fine; a failed enumeration is not: the device + cache and the ENUMERATE snapshot would misrepresent the system. + register_global_error: set above or by hid_internal_enumerate */ + HCMNOTIFICATION notify_handle = hid_internal_hotplug_cleanup(); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + hid_internal_hotplug_finish_unregistration(notify_handle); + free(hotplug_cb); + return -1; + } + hid_hotplug_context.devs_incomplete = 0; + } + /* else: reuse the still-attached notification (its deferred teardown has + not run yet); the device cache is kept current by that notification */ } - /* Return allocated handle */ - if (callback_handle != NULL) { - *callback_handle = hotplug_cb->handle; + /* Take the registration-time snapshot to be replayed asynchronously + on the event context, one exact copy per matching connected device */ + if ((flags & HID_API_HOTPLUG_ENUMERATE) && (hotplug_cb->events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { + if (hid_internal_hotplug_snapshot(hotplug_cb) < 0) { + /* Tear the machinery down if this would-be-first callback was starting it. + register_global_error: set by hid_internal_hotplug_snapshot */ + HCMNOTIFICATION notify_handle = hid_internal_hotplug_cleanup(); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + hid_internal_hotplug_finish_unregistration(notify_handle); + free(hotplug_cb); + return -1; + } } - /* Append a new callback to the end */ + /* Append the new callback to the end of the list */ if (hid_hotplug_context.hotplug_cbs != NULL) { struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; while (last->next != NULL) { @@ -1239,81 +2533,132 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven last->next = hotplug_cb; } else { - GUID interface_class_guid; - CM_NOTIFY_FILTER notify_filter = { 0 }; + hid_hotplug_context.hotplug_cbs = hotplug_cb; + } - /* Fill already connected devices so we can use this info in disconnection notification */ - hid_hotplug_context.devs = hid_enumerate(0, 0); + /* Return allocated handle */ + if (callback_handle != NULL) { + *callback_handle = hotplug_cb->handle; + } - hid_hotplug_context.hotplug_cbs = hotplug_cb; + /* Have the snapshot delivered on the event context; never from within this call */ + if (hotplug_cb->replay != NULL) { + hid_internal_hotplug_submit_work(); + } - if (hid_hotplug_context.notify_handle != NULL) { - register_global_error(L"Device notification have already been registered"); - LeaveCriticalSection(&hid_hotplug_context.critical_section); - return -1; - } + /* A successful registration leaves no stale error behind (the internal + enumeration of an empty system registers "No HID devices found"). + The critical section orders machinery-held writes and this call's internal + enumeration errors; early failures and WAIT_FAILED above may report outside + it. global_error_lock protects the string swap, not concurrent last-writer + order. Applications must serialize hid_error(NULL) as documented. */ + register_global_error(NULL); - /* Retrieve HID Interface Class GUID - https://docs.microsoft.com/windows-hardware/drivers/install/guid-devinterface-hid */ - HidD_GetHidGuid(&interface_class_guid); + LeaveCriticalSection(&hid_hotplug_context.critical_section); - notify_filter.cbSize = sizeof(notify_filter); - notify_filter.FilterType = CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE; - notify_filter.u.DeviceInterface.ClassGuid = interface_class_guid; + return 0; +} - /* Register for a HID device notification when adding the first callback */ - if (CM_Register_Notification(¬ify_filter, NULL, hid_internal_notify_callback, &hid_hotplug_context.notify_handle) != CR_SUCCESS) { - register_global_error(L"hid_hotplug_register_callback/CM_Register_Notification"); - LeaveCriticalSection(&hid_hotplug_context.critical_section); - return -1; - } +int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void* user_data, hid_hotplug_callback_handle* callback_handle) +{ + struct hid_hotplug_callback* hotplug_cb; + int result; + + /* No events can be delivered before the handle is written */ + if (callback_handle != NULL) { + *callback_handle = 0; } - /* Mark the critical section as IN USE, to prevent callback removal from inside a callback */ - unsigned char old_state = hid_hotplug_context.mutex_in_use; - hid_hotplug_context.mutex_in_use = 1; - - if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { - struct hid_device_info* device = hid_hotplug_context.devs; - /* Notify about already connected devices, if asked so */ - while (device != NULL) { - if (hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { - (*hotplug_cb->callback)(hotplug_cb->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, hotplug_cb->user_data); - } + /* Check params */ + if (callback == NULL) { + register_global_error(L"Callback function is NULL"); + return -1; + } + if (events == 0 + || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT))) { + register_global_error(L"Invalid events mask"); + return -1; + } + if (flags & ~(HID_API_HOTPLUG_ENUMERATE)) { + register_global_error(L"Invalid flags"); + return -1; + } - device = device->next; - } + hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); + + if (hotplug_cb == NULL) { + register_global_error(L"Failed to allocate memory for a hotplug callback"); + return -1; } - hid_hotplug_context.mutex_in_use = old_state; + /* Fill out the record */ + hotplug_cb->next = NULL; + hotplug_cb->vendor_id = vendor_id; + hotplug_cb->product_id = product_id; + hotplug_cb->events = events; + hotplug_cb->user_data = user_data; + hotplug_cb->callback = callback; + hotplug_cb->replay = NULL; - /* Remove any callbacks that were marked for removal and stop the notification if none are left */ - hid_internal_hotplug_cleanup(); - - LeaveCriticalSection(&hid_hotplug_context.critical_section); + /* Ensure the machinery is ready to be used, and keep hid_exit() from + destroying it while this call is inside */ + switch (hid_internal_hotplug_enter(1 /* bootstrap on first use */)) { + case 0: + break; + case HID_HOTPLUG_ENTER_EXITING: + /* hid_exit() is tearing the machinery down: arming it again behind its + back would leave a live notification and a registered callback with no + context to run in */ + register_global_error(L"hid_exit() is in progress"); + free(hotplug_cb); + return -1; + default: + /* register_global_error: set by hid_internal_hotplug_enter */ + free(hotplug_cb); + return -1; + } - return 0; + result = hid_internal_hotplug_register_counted(hotplug_cb, flags, callback_handle); + + hid_internal_hotplug_leave(); + + return result; } int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - if (callback_handle <= 0 || !hid_hotplug_context.mutex_ready) { + int result = -1; + HCMNOTIFICATION notify_handle = NULL; + int exiting; + CONFIGRET cr; + + /* Never bootstraps: without machinery this handle cannot exist. During exit, + admission remains possible while the critical section is published so a + callback can still disarm itself without waiting for hid_exit(). */ + if (callback_handle <= 0 || hid_internal_hotplug_enter(0) != 0) { + register_global_error(L"Invalid or unknown hotplug callback handle"); return -1; } /* Lock the mutex to avoid race conditions */ EnterCriticalSection(&hid_hotplug_context.critical_section); - if (!hid_hotplug_context.hotplug_cbs) { - LeaveCriticalSection(&hid_hotplug_context.critical_section); - return -1; - } + exiting = hid_internal_hotplug_exiting(); /* Remove this notification */ for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { if ((*current)->handle == callback_handle) { + /* A callback already marked for removal counts as deregistered */ + if (!(*current)->events) { + break; + } + + /* Undelivered HID_API_HOTPLUG_ENUMERATE events must never fire after deregistration */ + hid_free_enumeration((*current)->replay); + (*current)->replay = NULL; + /* Check if we were already in the critical section, as we are NOT allowed to remove any callbacks if we are */ - if (hid_hotplug_context.mutex_in_use) { + if (hid_hotplug_context.mutex_in_use || exiting) { /* If we are not allowed to remove the callback, we mark it as pending removal */ (*current)->events = 0; hid_hotplug_context.cb_list_dirty = 1; @@ -1322,15 +2667,39 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_call free(*current); *current = next; } + result = 0; break; } } - hid_internal_hotplug_cleanup(); + if (result != 0) { + /* Ordered against machinery-held writes only; early failures can report + outside the section. global_error_lock protects the swap, and callers + must serialize hid_error(NULL) as documented. */ + register_global_error(L"Invalid or unknown hotplug callback handle"); + } else { + register_global_error(NULL); + } + + /* Exit owns notification cleanup: its quiescence wait may have passed. */ + if (!exiting) { + notify_handle = hid_internal_hotplug_cleanup(); + } LeaveCriticalSection(&hid_hotplug_context.critical_section); - return 0; + cr = hid_internal_hotplug_finish_unregistration(notify_handle); + if (result == 0 && cr != CR_SUCCESS && !hid_internal_in_hotplug_callback()) { + wchar_t msg[200]; + HIDAPI_SWPRINTF(msg, sizeof(msg) / sizeof(msg[0]), + L"Callback deregistered, but CM_Unregister_Notification failed (CR 0x%08X); hotplug is no longer available", + (unsigned int)cr); + register_global_error_message(_wcsdup(msg)); + } + + hid_internal_hotplug_leave(); + + return result; } HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) @@ -1446,7 +2815,7 @@ HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path) dev->input_report_length = caps.InputReportByteLength; dev->feature_report_length = caps.FeatureReportByteLength; dev->read_buf = (char*) malloc(dev->input_report_length); - dev->device_info = hid_internal_get_device_info(interface_path, dev->device_handle); + dev->device_info = hid_internal_get_device_info(interface_path, dev->device_handle, NULL); end_of_function: free(interface_path); @@ -1921,6 +3290,11 @@ int HID_API_EXPORT_CALL hid_winapi_get_container_id(hid_device *dev, GUID *conta return -1; } + if (!dev->device_info) { + register_string_error(dev, L"NULL device info"); + return -1; + } + register_string_error(dev, NULL); interface_path = hid_internal_UTF8toUTF16(dev->device_info->path);