diff --git a/.github/workflows/win-vhid-test.yml b/.github/workflows/win-vhid-test.yml index 3d814e758..f4c100e10 100644 --- a/.github/workflows/win-vhid-test.yml +++ b/.github/workflows/win-vhid-test.yml @@ -145,7 +145,7 @@ jobs: shell: pwsh working-directory: build run: | - ctest -C Release -R DeviceIO_winapi --output-on-failure + ctest -C Release -R "_winapi" --output-on-failure - name: Cleanup virtual device if: always() diff --git a/libusb/hid.c b/libusb/hid.c index abf10bf8f..5da521cf8 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -159,9 +160,59 @@ 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 their ordinary paths - hid_init(), which every + * registration calls, resets it even on success, and deregistering an already + * removed handle reports the stale handle through it. 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; }; @@ -185,10 +236,32 @@ static struct hid_hotplug_context { 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 not joined yet */ + 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; + + /* Pending messages for the callback thread; protected by callback_thread's mutex */ struct hid_hotplug_queue* queue; /* Linked list of the hotplug callbacks */ @@ -387,18 +460,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 +753,200 @@ 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; }; +/* 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 (the event threads and wait_shutdown() only ever hold the + * latter, everyone else the former). */ +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; + 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() { /* 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() { - 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 joiner frees it once they are gone. */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + } + /* When the threads are still winding down, the join and the `devs` cleanup + * are performed by hid_internal_hotplug_finish_shutdown() (which releases + * `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, + * frees the device cache 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() +{ + /* 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). */ + /* Both event threads have exited: we have exclusive access to `devs` */ hid_free_enumeration(hid_hotplug_context.devs); hid_hotplug_context.devs = NULL; + hid_hotplug_context.context = NULL; + + /* Announce the completed wind-down to hid_internal_hotplug_wait_shutdown() */ + hid_internal_hotplug_set_shutdown_pending(0); } -static void hid_internal_hotplug_init() +/* 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() { + 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); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + while (hid_hotplug_context.shutdown_pending) { + 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); +} + +/* 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() +{ + hid_internal_hotplug_cleanup(); + + if (hid_hotplug_context.mutex_in_use || hid_hotplug_context.hotplug_cbs != NULL) { + return; + } + + if (hid_hotplug_context.threads_running) { + hid_internal_hotplug_finish_shutdown(); + } + else { + hid_internal_hotplug_wait_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() +{ + 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 +959,92 @@ 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); +} + +/* 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() +{ + 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() { - if (!hid_hotplug_context.mutex_ready) { + if (hid_internal_hotplug_lock() < 0) { + /* The hotplug machinery was never initialized, so nothing can race us for + * `usb_context`: a registration is the only path that would, and it would + * have initialized the machinery (and taken `mutex`) first. Destroy the + * main context a plain hid_init() may have created and return. */ + if (usb_context) { + libusb_exit(usb_context); + usb_context = NULL; + } return; } - pthread_mutex_lock(&hid_hotplug_context.mutex); 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 and complete the join */ + hid_internal_hotplug_set_shutdown_pending(1); + hid_internal_hotplug_finish_shutdown(); + } + else { + /* Another thread may still be completing a wind-down it has claimed */ + hid_internal_hotplug_wait_shutdown(); + + hid_free_enumeration(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); + /* Destroy the main libusb context while STILL HOLDING `mutex`, so the whole + * teardown is a single transaction. Doing it as a separate step - releasing + * `mutex` here and re-acquiring it just to destroy the context - would open a + * window in which a concurrent hid_hotplug_register_callback() could observe a + * non-NULL usb_context with the machinery already gone, spin up a fresh + * hotplug context and threads, and be orphaned when we then destroy the main + * context: hid_exit() would return with those hotplug threads still running. + * libusb_exit() does not re-enter HIDAPI, so it cannot take `mutex` again. */ + 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 +1072,19 @@ 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(); - } + /* A single transaction under the hotplug `mutex`: the hotplug machinery is + * torn down - and its event threads joined - and the main usb_context is + * destroyed without ever releasing the mutex in between. That keeps a + * concurrent hid_hotplug_register_callback() from either resurrecting + * usb_context behind our back or slipping into a window where the machinery + * is gone but usb_context is not, leaving live hotplug threads orphaned. */ + 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; } @@ -1164,7 +1468,12 @@ 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. */ +static struct hid_device_info *hid_internal_enumerate(unsigned short vendor_id, unsigned short product_id, int *failed) { libusb_device **devs; libusb_device *dev; @@ -1174,6 +1483,8 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, u struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; + *failed = 1; + if (hid_init() < 0) /* register_global_error: global error is set by hid_init */ return NULL; @@ -1202,7 +1513,17 @@ 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 = hid_internal_enumerate(vendor_id, product_id, &failed); + + 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 +1548,155 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } +/* Does this HID device sit on that libusb device? Compares the device part of + * the path - "-:" - INCLUDING the ':' that separates it from the + * "." part get_path() has zeroed out. Dropping the ':' would + * make "1-2" a prefix of "1-20:0.0" and confuse a device on port 2 with one on + * port 20 (routine on a hub with 10 ports or more). */ static int match_libusb_to_info(libusb_device *device, struct hid_device_info* info) { /* make a path from this libusb device, but leave the last 2 fields as 0 */ char pseudo_path[64]; + int len; + + if (info->path == NULL) { + return 0; + } + get_path(&pseudo_path, device, 0, 0); - int len = strlen(pseudo_path) - sizeof("0.0"); + + /* everything but the trailing ".", which get_path() left + * as "0.0" - signed, as get_path() yields "" when it fails */ + len = (int) strlen(pseudo_path) - (int) strlen("0.0"); + if (len <= 0) { + /* get_path() failed: match nothing */ + return 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); + return !strncmp(info->path, pseudo_path, (size_t) len); +} + +/* 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) +{ + 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; } - - hid_hotplug_context.mutex_in_use = 0; - hid_internal_hotplug_remove_postponed(); - pthread_mutex_unlock(&hid_hotplug_context.mutex); } -static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void * user_data) +/* Is this libusb device already represented in the device cache? Uses the same + * identity as the removal path (match_libusb_to_info), so an arrival is only + * ever deduplicated against the very entries a later removal would match. */ +static int hid_internal_hotplug_is_known_device(libusb_device *device) { - (void)ctx; - (void)user_data; + for (struct hid_device_info *info = hid_hotplug_context.devs; info != NULL; info = info->next) { + if (match_libusb_to_info(device, info)) { + return 1; + } + } - /* 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); + 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,60 +1722,141 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic return 0; } +static int 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) { + /* Out of memory: the event is dropped. There is nothing better to be + * done here - libusb gives us no way to postpone or retry it, and the + * callback must not block the event thread. + * A dropped arrival is simply never reported; a dropped removal is worse, + * as the stale entry it leaves in `devs` makes the next arrival on that + * port look like a device we already know (see + * hid_internal_hotplug_is_known_device) and hides it for as long as the + * hotplug machinery keeps running. */ + 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 */ + if (!hid_internal_hotplug_is_known_device(msg->device)) { + struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0); + + if (info) { + /* 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_device_info** tail = &hid_hotplug_context.devs; + while (*tail != NULL) { + tail = &(*tail)->next; + } + *tail = info; + + 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; } } } else if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { + struct hid_device_info *removed = NULL; + struct hid_device_info **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_device_info **current = &hid_hotplug_context.devs; *current;) { struct hid_device_info* info = *current; - if (match_libusb_to_info(msg->device, *current)) { + if (match_libusb_to_info(msg->device, info)) { /* If the libusb 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); - /* Free every removed device (and its internal allocations) */ - hid_free_enumeration(info); + *removed_tail = info; + removed_tail = &info->next; } else { current = &info->next; } } + + while (removed != NULL) { + struct hid_device_info* info = removed; + removed = info->next; + /* Each invocation describes exactly one device: `device->next` is + * NULL by contract */ + info->next = NULL; + hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, last); + /* Free every removed device (and its internal allocations) */ + hid_free_enumeration(info); + } } + 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 */ + /* Any callbacks that await removal are removed above */ /* No further cleaning is needed */ } @@ -1361,12 +1864,18 @@ 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); } @@ -1382,66 +1891,141 @@ static void* callback_thread(void* user_data) 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); - /* 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 */ struct timeval tv; 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); } - /* 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); - libusb_exit(hid_hotplug_context.context); - /* 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. */ + /* Signal callback_thread under the mutex so it can observe shutdown_pending + * and exit cleanly, rather than waiting indefinitely in cond_wait */ hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); 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); + } + + libusb_exit(hid_hotplug_context.context); + 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 and no event thread running + * (the caller must have joined the callback thread if it managed to start it). + * 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; + } + + /* Drop anything that was enqueued (at most the replay marker of this + * registration: the libusb thread, the only other producer, never ran) */ + hidapi_thread_mutex_lock(&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); + + libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); + libusb_exit(hid_hotplug_context.context); + hid_hotplug_context.context = NULL; + + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = 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 +2036,205 @@ 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(); + /* Ensure we are ready to actually use the mutex, and lock it to avoid race conditions */ + hid_internal_hotplug_init_and_lock(); - /* 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; + /* 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 */ + while (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.shutdown_pending) { + if (hid_hotplug_context.threads_running) { + hid_internal_hotplug_finish_shutdown(); + } + else { + /* Another thread has claimed the join: wait for it to be done */ + hid_internal_hotplug_wait_shutdown(); + } } - /* Return allocated handle */ - if (callback_handle != NULL) { - *callback_handle = hotplug_cb->handle; + /* Registration implicitly initializes HIDAPI (as if by hid_init()); done + * under the mutex so concurrent registrations do not race in it. + * + * Deliberately AFTER the wind-down loop above: both finish_shutdown() and + * wait_shutdown() release `mutex` while a concurrent hid_exit() runs, and that + * teardown is a single transaction that NULLs usb_context before it hands the + * mutex back. A registration that parked in the loop therefore resumes with + * usb_context possibly already destroyed; (re)creating it here - after the wait + * has returned, still under `mutex`, and before hid_internal_enumerate() + * dereferences it through libusb_get_device_list(usb_context, ...) - + * re-establishes a consistent context instead of proceeding against a NULL one. + * hid_init() is idempotent, so this is a no-op on the common path where no + * teardown intervened. */ + if (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; } - /* 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, + /* Arm a global callback to receive ALL notifications for HID class + * devices BEFORE taking the snapshot of the connected ones: 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_hotplug_context.devs = hid_internal_enumerate(0, 0, &enumeration_failed); + if (enumeration_failed) { + /* register_global_error: global error is already set by hid_internal_enumerate */ + libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); + libusb_exit(hid_hotplug_context.context); + hid_hotplug_context.context = NULL; + free(hotplug_cb); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + register_libusb_error(&last_global_error, LIBUSB_SUCCESS, NULL); } - /* 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_device_info *device = hid_hotplug_context.devs; 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; + if (hotplug_cb->replay != NULL) { + /* Wake the callback thread up so the snapshot is delivered promptly even + * with no live event traffic. 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; } } - hid_hotplug_context.mutex_in_use = old_state; + 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. */ + hid_internal_hotplug_set_shutdown_pending(0); - hid_internal_hotplug_cleanup(); + 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; + } + + 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); + + 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 +2243,20 @@ 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 when the hotplug machinery was never initialized (or has been shut + * down by hid_exit()); 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 +2264,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 +2287,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 +2553,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 +2598,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 +2660,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 +3338,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 +3404,26 @@ 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), so + * nothing internal can be blocked on this lock either. */ + 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_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..be9b5dc29 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -26,6 +26,7 @@ #include #include #include +#include /* Unix */ #include #include @@ -135,14 +136,24 @@ static wchar_t *utf8_to_wchar_t(const char *utf8) } +/* Serializes concurrent mutations of the error strings: the hotplug API is + * thread-safe and may set the global error from several threads at once + * (including the hotplug monitor thread). Reading the strings 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. */ @@ -419,15 +430,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 +455,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,8 +465,9 @@ 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, or -1 on 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 */ @@ -459,7 +477,7 @@ static int get_hid_report_descriptor_from_sysfs(const char *sysfs_path, struct h return -1; 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; @@ -641,7 +659,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; @@ -701,8 +730,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; @@ -806,7 +841,7 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device /* 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; @@ -835,8 +870,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; @@ -889,7 +930,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, 0, NULL); } if (!root) { @@ -914,6 +955,54 @@ 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 the monotonic `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; + struct hid_hotplug_monitor_thread *next; +}; + static struct hid_hotplug_context { /* UDEV context that handles the monitor */ struct udev* udev_ctx; @@ -921,18 +1010,55 @@ 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; + + /* Monotonic source of thread_id tokens; never reused, survives hid_exit */ + unsigned long next_thread_id; + + enum hid_hotplug_thread_state thread_state; + + /* The retired-list node the current generation owns, pre-allocated at + pthread_create time and non-NULL exactly while thread_state is RUNNING or + FINISHED. hid_internal_hotplug_retire_current() splices this already-owned + node onto `retired` instead of allocating, so retiring a finished + generation can never fail - which is why hid_exit() needs no in-place-join + backstop. Freed when its thread is finally joined (reaped off `retired`), + or on the register failure path if the thread is never created. */ + 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; + 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; @@ -952,16 +1078,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 +1103,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,62 +1115,326 @@ 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) +{ + 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) { - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + 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(); + /* Splice the generation's own pre-allocated node (never NULL while a + generation exists) onto the retired list: a pointer move, no allocation, + so this can never fail. */ + node = hid_hotplug_context.thread_node; + node->thread = hid_hotplug_context.thread; + node->id = hid_hotplug_context.thread_id; + node->being_joined = 0; + node->next = hid_hotplug_context.retired; + hid_hotplug_context.retired = node; + + /* 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; +} - if (hid_hotplug_context.hotplug_cbs != NULL) { +/* 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: + - Self: a destructor re-entering HIDAPI on the monitor thread reaches here; a + thread cannot join itself. Its entry is skipped (pthread_self() compared + against the still-valid, unjoined handle) and left on the list for 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_equal()/pthread_join() while still unjoined. */ +static void hid_internal_hotplug_reap_thread(void) +{ + /* Retire the finished current generation so it is tracked on the list and + joined below (or by a later reaper / hid_exit). This only splices the + generation's pre-allocated node onto the list, so it cannot fail: a + finished generation is always retired and never left FINISHED here. */ + hid_internal_hotplug_retire_current(); + + for (;;) { + struct hid_hotplug_monitor_thread **link; + struct hid_hotplug_monitor_thread *node = NULL; + + for (link = &hid_hotplug_context.retired; *link != NULL; link = &(*link)->next) { + if (!(*link)->being_joined + && !pthread_equal(pthread_self(), (*link)->thread)) { + node = *link; + break; + } + } + if (node == NULL) { + /* Nothing left that we may join: the list is empty, or the only + entries are ourselves or already being joined by another reaper. */ + 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); + pthread_join(node->thread, NULL); + pthread_mutex_lock(&hid_hotplug_context.mutex); + + 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 exactly once (and + not in use); the mutex is temporarily dropped while waiting for a still-RUNNING + thread to notice the empty list and while joining, so the caller must + re-validate any cached state after this returns. + + This does NOT guarantee every retired generation is joined before it returns: + an entry that is the calling (monitor) thread, or one another reaper is + joining, is left on the list. A plain register/deregister must not block on + another generation's teardown; 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 current generation and join the + retired threads we can (all with the mutex released inside the reap). + With hotplug_cbs still NULL no new generation can appear here, so this + settles - the current slot becomes NONE, or it stays FINISHED only + because it is the calling thread or a retired-node allocation failed, + and in both cases there is nothing more this caller can do. */ + hid_internal_hotplug_reap_thread(); + 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; + /* Monotonic monitor-thread generation tokens; never reused, 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; } -static void hid_internal_hotplug_exit() +/* 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) { - if (!hid_hotplug_context.mutex_ready) { + 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(void) +{ + 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(); + + /* No in-place-join backstop is needed here: retiring a finished + generation only splices its pre-allocated node onto the retired list + and can never fail, so hid_internal_hotplug_cleanup() above always + drove the current slot to NONE (the finished generation is now on the + retired list) rather than leaving a published FINISHED slot to join. */ + + /* EVERY generation must be joined before hid_exit() returns. cleanup's + reap joined every retired thread it could; anything still on the list is + being joined by another (pre-exit) reaper - wait for that join to + broadcast thread_cond, then re-evaluate. (hid_exit() is never the + monitor thread, so no retired entry is ever "self" here.) */ + if (hid_hotplug_context.retired != NULL) { + pthread_cond_wait(&hid_hotplug_context.thread_cond, &hid_hotplug_context.mutex); + continue; + } + + if (hid_hotplug_context.hotplug_cbs == NULL + && hid_hotplug_context.thread_state == HID_HOTPLUG_THREAD_NONE + && hid_hotplug_context.retired == NULL) { + 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; @@ -1044,10 +1442,12 @@ int HID_API_EXPORT hid_init(void) /* indicate no error */ 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; } @@ -1068,7 +1468,12 @@ 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 (with the global error set accordingly). 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. */ +static struct hid_device_info *hid_internal_enumerate(unsigned short vendor_id, unsigned short product_id, int *failure) { struct udev *udev; struct udev_enumerate *enumerate; @@ -1077,6 +1482,10 @@ 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; + if (failure) { + *failure = 0; + } + hid_init(); /* register_global_error: global error is reset by hid_init */ @@ -1084,13 +1493,40 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, udev = udev_new(); if (!udev) { 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); + 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); + 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); + 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 */ @@ -1119,10 +1555,21 @@ 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) { + /* A path the scan just listed no longer resolves to a udev device: + the enumeration is no longer a complete, authoritative snapshot. + Flag it so an all-or-nothing caller (the initial hotplug- + registration snapshot) discards the partial result instead of + mistaking the missing entries for departures. hid_enumerate() + passes failure == NULL and keeps its long-standing best-effort + behavior. */ + if (failure) { + *failure = 1; + } continue; + } - tmp = create_device_info_for_device(raw_dev); + tmp = create_device_info_for_device(raw_dev, 0, failure); if (tmp) { if (cur_dev) { cur_dev->next = tmp; @@ -1155,6 +1602,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); +} + void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) { struct hid_device_info *d = devs; @@ -1169,25 +1621,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,124 +1732,403 @@ 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; + } + for (struct hid_device_info *device = list; device; device = device->next) { + if (device->path && !strcmp(device->path, path)) { + return device; + } + } + return NULL; +} + +static struct hid_device_info *hid_internal_find_device_by_path(const char *path) +{ + return hid_internal_find_device_in_list(hid_hotplug_context.devs, path); +} + +/* 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; + + if (info == NULL) { + return; + } + + /* 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 duplicates to keep arrivals (and later departures) exactly-once. */ + 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; + } + + /* 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; + } + + /* Freeze the dispatch to the callbacks registered up to this point */ + dispatch_bound = hid_hotplug_context.next_handle - 1; + + 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; + } + } + + 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). */ + hid_internal_hotplug_process_arrival(create_device_info_for_device(raw_dev, 1, NULL)); + } 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); + } } - return 0; } +/* 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 irrecoverably dead. Netlink receive-buffer overruns (ENOBUFS) + raise POLLERR but clear once the pending error is consumed by a receive + attempt, so a bounded number of retries filters those out. */ +#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; + (void) user_data; - /* 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 */ + /* The monitor file descriptor is immutable while this thread runs: it is + set up before the thread is started and released either by this thread + itself or after it announced its exit. It is read under the mutex all + the same: an unsynchronized read here would race the setup of the next + monitoring context (whose registration this thread cannot have seen). */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + monitor_fd = hid_hotplug_context.monitor_fd; + pthread_mutex_unlock(&hid_hotplug_context.mutex); - while (hid_hotplug_context.monitor_fd > 0) { - fd_set fds; - struct timeval tv; + /* This thread 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 (;;) { + struct pollfd fds; int ret; + int stop = 0; - /* 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; + 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; } - 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; + 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 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; + } - ret = select(hid_hotplug_context.monitor_fd+1, &fds, NULL, NULL, &tv); + /* 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 */ + for (;;) { + struct udev_device *raw_dev = udev_monitor_receive_device(hid_hotplug_context.mon); + if (raw_dev == NULL) { + break; + } + hid_internal_hotplug_process_event(raw_dev); + udev_device_unref(raw_dev); + } + } + } - /* An extra check, just in case within those 5msec the thread was told to stop */ - if (!hid_hotplug_context.hotplug_cbs) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + if (stop) { break; } - /* 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; - } + if (socket_dead) { + /* No monitor socket to wait on anymore (it has been released + above): keep pacing the loop for the replay flushes and the + exit check */ + poll(NULL, 0, 5); + continue; + } - /* 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; - } - } - } 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; - } - } - } - udev_device_unref(raw_dev); - pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* 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 && !(fds.revents & POLLIN)) { + /* An error condition with no data to read */ + if (fds.revents & (POLLHUP | POLLNVAL)) { + /* Unambiguously dead: the socket got closed or invalidated */ + socket_dead = 1; + } else if (++socket_error_polls >= HID_HOTPLUG_SOCKET_ERROR_POLL_LIMIT) { + /* Persistent POLLERR: an error condition that survives this + many drain attempts is not a recoverable overrun */ + socket_dead = 1; + } + if (!socket_dead) { + /* Without this, the error condition would make poll() return + immediately and busy-spin this loop; keep the 5 msec pace */ + poll(NULL, 0, 5); } + } else { + socket_error_polls = 0; } } - /* 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); - return 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) { 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. */ + quiet = hid_hotplug_context.mutex_in_use; /* 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 +2139,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 +2199,248 @@ 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 + int enumerate_failure = 0; + const char *monitor_error = NULL; + + /* This branch is never taken on the monitor thread: 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). The global error writes below + - including the hid_internal_enumerate() ones - are therefore always + made by an application thread */ + + /* Prepare a UDEV context to run monitoring on */ hid_hotplug_context.udev_ctx = udev_new(); - if (!hid_hotplug_context.udev_ctx) - { - pthread_mutex_unlock(&hid_hotplug_context.mutex); - return -1; + 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"; + } } - 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); + 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); + if (!enumerate_failure) { + 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) { + register_global_error(monitor_error); + } + /* on enumerate_failure the global error is already set by the enumeration */ + 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 thread that will be doing the event scanning. + hid_internal_hotplug_cleanup() above ran with the callback list empty, + so it retired any finished predecessor generation onto the retired list + (joining it there, or leaving it for hid_exit()) and drove the current + slot to HID_HOTPLUG_THREAD_NONE - even in the orphan case where a + thread-specific-data destructor re-registers on the very monitor thread + that just finished: that generation is RETIRED (self-skipped for the + join), never dropped. The mutex has been held continuously since, so the + slot is NONE here. The new generation's retired-list node is + pre-allocated below BEFORE the thread is created, so retiring a finished + generation only splices an already-owned node onto the list and can never + fail - which is why hid_exit() needs no in-place-join backstop. + The thread is JOINABLE. When it winds down (the last callback is gone) + it releases the monitoring context and publishes + HID_HOTPLUG_THREAD_FINISHED under the mutex before it returns; the + pthread_t is then retired and joined by hid_exit() or the next + register/deregister (see hid_internal_hotplug_reap_thread), so no + monitor-thread code is still executing inside the library once + hid_exit() returns - which a detached thread could not guarantee. + Joining is done by ANOTHER thread, never the monitor thread itself, so + it is ThreadSanitizer-clean (unlike pthread_detach(self)). */ + /* Pre-allocate the retired-list node this new generation will own for its + whole life, BEFORE creating the thread. Retiring a finished generation + (hid_internal_hotplug_retire_current) then only splices this + already-owned node onto the retired list and can never fail, so a + finished generation is ALWAYS tracked and joined and hid_exit() needs no + in-place-join backstop. An allocation failure here is handled cleanly by + failing the registration WITHOUT creating the thread: no orphan is + possible, and there is nothing to unwind beyond the normal path. */ + 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); + register_global_error("Couldn't allocate the hotplug monitor thread record"); + return -1; + } + + /* Retire any finished predecessor generation onto the retired list (a + pointer splice of its own pre-allocated node - cannot fail). After the + cleanup above the slot is NONE in the common case, so this usually + no-ops; it never overwrites or drops an unjoined pthread_t. */ + 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); + 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++; + 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 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; + + 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 +2449,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 +2474,10 @@ 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"); + } + return result; } diff --git a/mac/hid.c b/mac/hid.c index 70951da72..09e45b3e2 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,17 @@ 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). Must be called + with global_error_mutex held. */ +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,7 +280,16 @@ 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) { - register_error_str(&last_global_error_str, msg); + pthread_mutex_lock(&global_error_mutex); + /* 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()) + 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. */ @@ -276,7 +297,11 @@ static void register_global_error_format(const char *format, ...) { va_list args; va_start(args, format); - register_error_str_vformat(&last_global_error_str, format, args); + pthread_mutex_lock(&global_error_mutex); + /* See register_global_error(): suppressed on the internal event thread. */ + if (!hid_internal_on_event_thread()) + register_error_str_vformat(&last_global_error_str, format, args); + pthread_mutex_unlock(&global_error_mutex); va_end(args); } @@ -438,7 +463,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 +502,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,6 +518,48 @@ 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 ALL of the hotplug + context: the callback list, the device cache and every lifecycle flag + (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, held only while the global error + string is replaced. 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 only exception to "all context state is accessed under the mutex" is the + event thread's startup phase - everything it does before reaching the startup + barrier: the registering thread that started it holds the mutex and is parked + at that barrier, 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. */ + static struct hid_hotplug_context { /* MacOS specific notification handles */ IOHIDManagerRef manager; @@ -495,26 +568,79 @@ static struct hid_hotplug_context { pthread_t thread; 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 collected yet */ + 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; + + /* Identity of the running hotplug event thread. Published by that thread as + its first action and cleared in its epilogue, so it is valid exactly while + an event thread exists. Guarded by global_error_mutex (a leaf mutex), NOT + the hotplug mutex: the global-error writer consults it while holding + global_error_mutex and must never take the hotplug mutex it may already + hold. Read only via hid_internal_on_event_thread(). */ + pthread_t event_thread_id; + unsigned char event_thread_id_valid; /* 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; + +/* 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. This mirrors the libusb and linux + backends, which likewise suppress such writes. A callback may re-enter the + public hid_hotplug_register_callback()/hid_hotplug_deregister_callback(), + whose success and failure paths both write the global error; those writes are + suppressed via this check in register_global_error()[_format](). + Returns non-zero when the caller is the hotplug event thread. Must be called + with global_error_mutex held (the event_thread_id* fields are guarded by it), + which the global-error writer already holds. */ +static int hid_internal_on_event_thread(void) +{ + return hid_hotplug_context.event_thread_id_valid + && pthread_equal(pthread_self(), hid_hotplug_context.event_thread_id); +} static void hid_internal_hotplug_remove_postponed(void) { @@ -531,6 +657,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,6 +668,78 @@ static void hid_internal_hotplug_remove_postponed(void) hid_hotplug_context.cb_list_dirty = 0; } +/* Releases everything that only the collector of the event thread may release. + Called with the hotplug mutex held, either by the thread that has just joined + the event thread, or by the event thread itself when nobody is joining it (it + then detaches itself - see hid_internal_hotplug_thread_epilogue()). + 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) +{ + hid_hotplug_context.thread_needs_join = 0; + + pthread_barrier_destroy(&hid_hotplug_context.startup_barrier); + + /* The run loop sources are created by the event thread, but the thread does + not release them while it winds down: the references must stay valid so + that the run loop can still be woken up until the thread is collected. */ + 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 releases + what only the collector may release. 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 + pthread_equal() check below) - that is what keeps pthread_cond_wait() from + ever being reached with the recursive mutex locked more than once. */ +static void hid_internal_hotplug_collect_thread(void) +{ + 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 + && !pthread_equal(pthread_self(), hid_hotplug_context.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. */ + pthread_join(hid_hotplug_context.thread, NULL); + + pthread_mutex_lock(&hid_hotplug_context.mutex); + hid_hotplug_context.join_in_progress = 0; + hid_internal_hotplug_release_thread(); + + /* Wake the threads waiting for this join to complete */ + pthread_cond_broadcast(&hid_hotplug_context.join_done); + } + + pthread_mutex_unlock(&hid_hotplug_context.mutex); +} + +/* Must be called with the hotplug mutex held */ static void hid_internal_hotplug_cleanup(void) { if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { @@ -558,55 +757,136 @@ 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: they are only released once the + thread has been collected, which cannot happen while this thread + holds the mutex. */ + 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; + } + + 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; - if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) - hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + 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; } +/* 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) +{ + 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 void hid_internal_hotplug_exit(void) { - if (!hid_hotplug_context.mutex_ready) { + 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; } 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 */ + hid_internal_hotplug_collect_thread(); + + /* 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. */ +} + +/* 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 collected by now, so it no longer uses the mode */ + 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 +908,27 @@ int HID_API_EXPORT hid_init(void) int HID_API_EXPORT hid_exit(void) { + /* The hotplug thread and the callbacks are stopped/freed unconditionally: + hid_hotplug_register_callback() may have initialized the library implicitly + without ever creating hid_mgr. + This leaves the hotplug API closed (`exiting`), so that a concurrent + registration cannot re-enter hid_init() while hid_mgr is being destroyed + below */ + hid_internal_hotplug_exit(); + 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; } @@ -967,110 +1257,281 @@ 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. The list is append-only while mutex_in_use is set. */ + 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 */ + /* If the result is non-zero, we mark the callback for removal */ /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ 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()). */ +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) { - 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 re-reports every device that was + already connected when it was opened. 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); + if (!info) { + 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 +1539,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 +1552,347 @@ 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); +} + +/* Lets the hotplug IOHIDManager process the device-matching events it queued for + the already connected devices, exactly 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 can + only ADD devices to the cache, never move one from the initial snapshot to the + live events. It runs on the event thread during its startup phase, 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) +{ + SInt32 res; + do { + res = CFRunLoopRunInMode(hid_hotplug_context.run_loop_mode, 0.001, FALSE); + } while (res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut && res != kCFRunLoopRunStopped); +} + +/* 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": IOHIDManagerCopyDevices() answers synchronously - which is + exactly what 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; + } + + /* 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]); + 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; +} + +/* Runs at the very end of the event thread. If no other thread is joining it, + the thread detaches itself and releases its own resources here: otherwise a + callback that deregisters the last callback from within a callback (including + by returning non-zero) would leave an unjoined thread, two run loop sources + and the run loop behind until the next register/deregister/hid_exit() - which + may never come. */ +static void hid_internal_hotplug_thread_epilogue(void) +{ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + /* The event thread is exiting: stop suppressing global-error writes for its + pthread id. Cleared under the hotplug mutex - before the thread is detached + or collected, and thus before any replacement event thread can be started + and publish its own id - so a later thread's id can never be clobbered. + Ordering is hotplug mutex -> global_error_mutex, the same order the + global-error writer uses when it is called under the hotplug mutex. */ + pthread_mutex_lock(&global_error_mutex); + hid_hotplug_context.event_thread_id_valid = 0; + pthread_mutex_unlock(&global_error_mutex); + + if (hid_hotplug_context.thread_needs_join && !hid_hotplug_context.join_in_progress) { + /* Nobody is inside pthread_join() on this thread, and nobody can enter + it any more: the decision is taken under the mutex on both sides (see + hid_internal_hotplug_collect_thread()), so there is no double join and + no join of a detached thread. */ + pthread_detach(pthread_self()); + hid_internal_hotplug_release_thread(); + pthread_cond_broadcast(&hid_hotplug_context.join_done); + } + + /* Past this point the thread must not touch the context any more: as soon as + the mutex is released, a new event thread may be started */ + pthread_mutex_unlock(&hid_hotplug_context.mutex); +} + static void* hotplug_thread(void* user_data) { + int manager_opened = 0; + (void) user_data; - hid_hotplug_context.thread_state = 0; - hid_hotplug_context.manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + /* Publish this thread's identity 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()). Uses global_error_mutex only: the hotplug + mutex must not be taken during the startup phase (the registrant holds it, + parked at the startup barrier). */ + pthread_mutex_lock(&global_error_mutex); + hid_hotplug_context.event_thread_id = pthread_self(); + hid_hotplug_context.event_thread_id_valid = 1; + pthread_mutex_unlock(&global_error_mutex); + + /* 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 device cache is empty at this point: the event thread is only ever + started with no callbacks registered, which is exactly when the previous + cache was freed by hid_internal_hotplug_cleanup() */ + 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.run_loop_mode) { + hid_hotplug_context.manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + } + + 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); + + /* Opening the manager enqueues the device-matching events for all + the devices that are already connected */ + if (IOHIDManagerOpen(hid_hotplug_context.manager, kIOHIDOptionsTypeNone) == kIOReturnSuccess) { + manager_opened = 1; + + /* Give the manager a chance to process what it just enqueued + (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; + } + } + } + } - /* 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 || code == kCFRunLoopRunHandledSource) { + continue; + } + + /* The run loop is gone: either the stop source stopped it + (thread_state is already 2), or it exited on its own. Publish the + shutdown under the mutex, so that a concurrent registration cannot + observe a running thread (thread_state 1) and signal-and-wake a run + loop that is winding down; a registration that finds the thread + stopped while callbacks are still registered fails instead of + attaching to a dead thread. */ + 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 collects this thread (or lets it detach itself below); + the run loop sources (if any got created) and the startup barrier are + released by whoever collects it */ + + /* Kill the manager. No mutex is needed (and none may be held across + IOHIDManagerClose()): nothing else ever touches the manager, and no other + thread may start a new event thread or release the run loop mode before + this thread has been collected - which cannot happen before the epilogue + below, i.e. after the last use of the run loop and of its mode here. */ + if (hid_hotplug_context.manager) { + if (manager_opened) { + IOHIDManagerClose(hid_hotplug_context.manager, kIOHIDOptionsTypeNone); + } - /* Kill the manager */ - IOHIDManagerClose(hid_hotplug_context.manager, kIOHIDOptionsTypeNone); + 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 +1901,120 @@ 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. When the implicit initialization happens here, that is the + registering thread rather than the thread that later calls hid_enumerate() + or hid_open() - those pump their own run loop, so they no longer service + the manager's run loop source. They do not depend on it (the manager + answers IOHIDManagerCopyDevices() synchronously), but an application that + wants the classic behavior should call hid_init() explicitly, from the + thread it uses HIDAPI on, before registering a hotplug callback. */ + 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(); - - /* Lock the mutex to avoid race conditions */ - pthread_mutex_lock(&hid_hotplug_context.mutex); + /* 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. + Never entered on the event thread itself: the list cannot be empty + while a callback dispatch is in flight. */ + while (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.thread_needs_join) { + /* Make sure the stop was actually requested (idempotent) */ + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + hid_internal_hotplug_collect_thread(); + 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 +2024,120 @@ 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_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; + } + + 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("hid_hotplug_register_callback: failed to start the device monitoring"); + + /* 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; + } + + if (hotplug_cb->replay != NULL && hid_hotplug_context.thread_state == 1) { + /* Ask the event thread to deliver the initial pass */ + CFRunLoopSourceSignal(hid_hotplug_context.replay_source); + CFRunLoopWakeUp(hid_hotplug_context.run_loop); } } - hid_hotplug_context.mutex_in_use = old_state; + /* 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; + } - hid_internal_hotplug_cleanup(); + /* Clear the stale global error on success, like hid_init()/hid_enumerate() do */ + register_global_error(NULL); pthread_mutex_unlock(&hid_hotplug_context.mutex); @@ -1255,40 +2146,69 @@ 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): the thread then detaches and releases itself in its + epilogue - see hid_internal_hotplug_thread_epilogue() */ + hid_internal_hotplug_collect_thread(); + return result; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index ff17785c6..26e9e6b14 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -48,9 +48,59 @@ 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 +# self-skips (77) when the backend reports hotplug as unsupported at runtime +# (e.g. a libusb without LIBUSB_CAP_HAS_HOTPLUG). +function(hidapi_add_hotplug_api_test name backend) + add_executable(${name} test_hotplug_api.c) + set_target_properties(${name} PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED TRUE + ) + target_link_libraries(${name} PRIVATE ${backend} Threads::Threads) + if(HIDAPI_ENABLE_ASAN AND NOT MSVC) + target_link_options(${name} PRIVATE -fsanitize=address) + endif() + add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES + SKIP_RETURN_CODE 77 + TIMEOUT 120 + ) +endfunction() + +# Define a tier-2 (device-backed) hotplug test 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); +# currently only the uhid provider implements presence toggling. +# is the per-event wait budget inside the test; +# bounds the whole run. +function(hidapi_add_hotplug_test name provider backend event_timeout_ms ctest_timeout) + add_executable(${name} test_hotplug.c ${provider}) + set_target_properties(${name} PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED TRUE + ) + target_compile_definitions(${name} PRIVATE + TEST_HOTPLUG_EVENT_TIMEOUT_MS=${event_timeout_ms}) + target_link_libraries(${name} PRIVATE ${backend} Threads::Threads) + if(HIDAPI_ENABLE_ASAN AND NOT MSVC) + target_link_options(${name} PRIVATE -fsanitize=address) + endif() + add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES + SKIP_RETURN_CODE 77 + TIMEOUT ${ctest_timeout} + ) +endfunction() + # --- Linux: hidraw backend via /dev/uhid ----------------------------------- if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_hidraw) hidapi_add_vdev_test(DeviceIO_hidraw test_virtual_device_uhid.c hidapi_hidraw) + hidapi_add_hotplug_api_test(HotplugAPI_hidraw hidapi_hidraw) + hidapi_add_hotplug_test(Hotplug_hidraw test_virtual_device_uhid.c hidapi_hidraw 10000 120) endif() # --- Linux: libusb backend via /dev/raw-gadget (+ dummy_hcd) ---------------- @@ -58,22 +108,34 @@ 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) + # Self-skips until the rawgadget provider implements unplug/replug; the + # generous budgets anticipate the full (virtual) USB stack round trips. + hidapi_add_hotplug_test(Hotplug_libusb test_virtual_device_rawgadget.c hidapi_libusb 30000 300) endif() # --- Windows: winapi backend via a modified vhidmini2 UMDF driver ----------- if(WIN32 AND TARGET hidapi_winapi) hidapi_add_vdev_test(DeviceIO_winapi test_virtual_device_win.c hidapi_winapi) - # HidD_GetPreparsedData / HidP_GetCaps used by the Windows provider. - target_link_libraries(DeviceIO_winapi PRIVATE hid) + hidapi_add_hotplug_api_test(HotplugAPI_winapi hidapi_winapi) + # Self-skips until the vhidmini2 provider implements unplug/replug. + hidapi_add_hotplug_test(Hotplug_winapi test_virtual_device_win.c hidapi_winapi 30000 300) + # hid: HidD_GetPreparsedData / HidP_GetCaps (device caps). + # cfgmgr32: CM_Locate_DevNodeA / CM_Disable_DevNode / CM_Enable_DevNode, used + # by the provider to toggle the root devnode's presence (unplug/replug). + # Set on the target (not via #pragma comment(lib), which MinGW ignores) so + # every winapi toolchain -- MSVC, clang-cl and MinGW -- links it. + target_link_libraries(DeviceIO_winapi PRIVATE hid cfgmgr32) + target_link_libraries(Hotplug_winapi PRIVATE hid cfgmgr32) # Run from the directory holding the hidapi DLL so a shared build can find # it at launch (there is no rpath on Windows). - set_tests_properties(DeviceIO_winapi PROPERTIES + set_tests_properties(DeviceIO_winapi HotplugAPI_winapi Hotplug_winapi PROPERTIES WORKING_DIRECTORY "$") # 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 +145,11 @@ endif() # available (e.g. hosted CI runners); usable locally / on a self-hosted Mac. if(APPLE AND TARGET hidapi_darwin) hidapi_add_vdev_test(DeviceIO_darwin test_virtual_device_mac.c hidapi_darwin) + hidapi_add_hotplug_api_test(HotplugAPI_darwin hidapi_darwin) + # Self-skips until the IOHIDUserDevice provider implements unplug/replug. + hidapi_add_hotplug_test(Hotplug_darwin test_virtual_device_mac.c hidapi_darwin 30000 300) target_link_libraries(DeviceIO_darwin PRIVATE "-framework IOKit" "-framework CoreFoundation") + target_link_libraries(Hotplug_darwin PRIVATE + "-framework IOKit" "-framework CoreFoundation") endif() diff --git a/src/tests/README.md b/src/tests/README.md index 1b6e08e29..0a33eb943 100644 --- a/src/tests/README.md +++ b/src/tests/README.md @@ -22,6 +22,47 @@ command bytes, expected payloads). | Test | What it exercises | |------|-------------------| | `test_device_io.c` | open → write an output report → trigger+read input reports (Feature-report write, then input-report read-back) → close | +| `test_hotplug_api.c` | tier-1 hotplug API contract, no device needed: argument validation, handle properties, implicit init, `hid_exit()` teardown, register/deregister thread churn | +| `test_hotplug.c` | tier-2 hotplug scenarios against a virtual device whose presence is toggled: async delivery, exactly-once ENUMERATE pass, callback-return deregistration, pass-before-live ordering, payloads, filtering, dispatch order, deregistration post-condition, re-entrant registration | + +## Hotplug tests + +The hotplug tests come in two tiers: + +* **Tier 1 — `HotplugAPI_`** (`test_hotplug_api.c`): everything in the + hotplug contract observable *without* a device event. Needs no virtual + device, no privileges, so it runs against **every** backend in the ordinary + per-push CI matrix. Self-skips (77) when the backend reports hotplug as + unsupported at runtime (e.g. a libusb without `LIBUSB_CAP_HAS_HOTPLUG`). +* **Tier 2 — `Hotplug_`** (`test_hotplug.c`): device-backed hotplug + scenarios. On top of a virtual device, the provider must be able to *toggle + the device's presence* (`test_virtual_device_unplug()` / + `test_virtual_device_replug()` in `test_virtual_device.h`). Currently only + the **uhid** provider implements toggling (a `UHID_DESTROY` / + `UHID_CREATE2` pair on the same open `/dev/uhid` fd), so `Hotplug_hidraw` + is the one tier-2 test that actually runs (in `builds.yml`'s ubuntu-cmake + job, like `DeviceIO_hidraw`); the other providers return + `TEST_VDEV_UNAVAILABLE` from the toggle calls and their `Hotplug_*` tests + self-skip everywhere until presence toggling is implemented for them. + +| Test | Runs per-push in `builds.yml` | Notes | +|------|-------------------------------|-------| +| `HotplugAPI_hidraw` | yes (ubuntu-cmake) | | +| `HotplugAPI_libusb` | yes (ubuntu-cmake) | needs libusb hotplug support at runtime | +| `HotplugAPI_winapi` | yes (windows-cmake, MSVC/NMake/ClangCL/MinGW) | | +| `HotplugAPI_darwin` | yes (macos-cmake) | | +| `Hotplug_hidraw` | yes (ubuntu-cmake, via `uhid`) | the only tier-2 test that runs today | +| `Hotplug_libusb` | builds, self-skips | needs rawgadget unplug/replug (future) | +| `Hotplug_winapi` | builds, self-skips | needs driver-side presence toggling (future) | +| `Hotplug_darwin` | builds, self-skips | needs `IOHIDUserDevice` re-creation (future) | + +The tier-2 test is written against strict synchronization rules (hotplug tests +are notoriously flaky otherwise): callbacks only deep-copy the event into a +log under a lock; every expectation is awaited with a deadline-based predicate +poll (never a bare sleep); the *absence* of an event is asserted behind an +**event barrier** — a later event that is provably ordered after the missing +one — never behind a time window; and a missed event within the (generous) +budget is treated as a bug, not retried. ## Providers @@ -90,6 +131,9 @@ cmake -B build -S . -DHIDAPI_WITH_TESTS=ON cmake --build build sudo modprobe uhid sudo ctest --test-dir build -R DeviceIO_hidraw --output-on-failure +sudo ctest --test-dir build -R Hotplug_hidraw --output-on-failure +# tier-1 hotplug API tests need no device and no root: +ctest --test-dir build -R HotplugAPI --output-on-failure ``` On Windows/macOS configure with `-DHIDAPI_WITH_TESTS=ON` and run `ctest`; the diff --git a/src/tests/test_hotplug.c b/src/tests/test_hotplug.c new file mode 100644 index 000000000..c36da97eb --- /dev/null +++ b/src/tests/test_hotplug.c @@ -0,0 +1,1044 @@ +/******************************************************* + 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): + - callbacks only lock, deep-copy the event into a log, + unlock and return; they never call hid_enumerate/hid_open/ + hid_error(NULL); + - 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 future 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; + +#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); +} + +/* ------------------------------------------------------------------ */ +/* 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 */ + 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; + +/* 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) +{ + test_mutex_lock(&g_log_lock); + 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->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); + if (device->path) + snprintf(e->path, sizeof(e->path), "%s", device->path); + if (device->serial_number) { + size_t i; + for (i = 0; i + 1 < sizeof(e->serial) && device->serial_number[i]; i++) { + wchar_t wc = device->serial_number[i]; + e->serial[i] = (wc > 0 && wc < 128) ? (char)wc : '?'; + } + e->serial[i] = '\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) + 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) + return -1; + test_sleep_ms(WAIT_TICK_MS); + } +} + +/* Start-of-test reset. Also the global sweep for two invariants every event + must satisfy: never delivered on the registering (main) thread, and never + more events than the log can hold (an overflow would silently weaken the + later absence assertions). */ +static void hp_reset_log(const char *test_name) +{ + int i; + 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 registering thread\n", test_name); + fflush(stdout); + g_failures++; + break; + } + } + if (g_event_overflow) { + printf(" INVARIANT failed before %s: event log overflow\n", test_name); + fflush(stdout); + g_failures++; + } + g_event_count = 0; + g_event_overflow = 0; + test_mutex_unlock(&g_log_lock); +} + +/* ------------------------------------------------------------------ */ +/* Callbacks. Per the synchronization discipline they only lock, */ +/* deep-copy, append, unlock and return. */ + +/* 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 1) on the first event for + the test's primary device. */ +static int HID_API_CALL cb_return1_on_ours(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); + if (device && device->vendor_id == TEST_VID && device->product_id == TEST_PID) + return 1; + 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_return1_first(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 1; +} + +/* T14: signals "entered", stays inside the callback for a while, 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; +} 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); + test_sleep_ms(250); + test_mutex_lock(&g_log_lock); + ctx->exited = 1; + 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; + int child_rc; + hid_hotplug_callback_handle child_handle; + int self_dereg_rc; +} parent_ctx; + +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; + + 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 = hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &child); + int dereg_rc = hid_hotplug_deregister_callback(callback_handle); + test_mutex_lock(&g_log_lock); + ctx->child_rc = rc; + ctx->child_handle = child; + ctx->self_dereg_rc = dereg_rc; + 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) + 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 */ +/* hid_hotplug_register_callback() wrote to *callback_handle. */ +static int t6_async_delivery(void) +{ + hid_hotplug_callback_handle h = 0; + hp_event ev; + + CHECK(ensure_present() == 0); + hp_reset_log("T6"); + + step("register with ENUMERATE while the device is present"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_log, NULL, &h) == 0); + CHECK(h > 0); + + step("wait for the synthetic ARRIVED"); + CHECK(hp_wait_count_at_least(0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, + TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) == 0); + + CHECK(hp_find_first(&ev, 0, 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 */ + + 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(hid_hotplug_register_callback(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(void) +{ + hid_hotplug_callback_handle h_ret = 0, h_bar = 0; + + CHECK(ensure_present() == 0); + hp_reset_log("T8a"); + + step("register the returns-1 callback and a barrier callback"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_return1_on_ours, NULL, &h_ret) == 0); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_bar) == 0); + + step("unplug: both callbacks see the LEFT; the first returns 1"); + 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(void) +{ + 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 0; + } + 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 returns-1-immediately callback with ENUMERATE (both devices match)"); + rc = hid_hotplug_register_callback(TEST_VID, 0, ALL_EVENTS, + HID_API_HOTPLUG_ENUMERATE, + cb_return1_first, NULL, &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 returns-1 callback never fired"); + } + + step("barrier: a fresh ENUMERATE registration sees both devices"); + rc = hid_hotplug_register_callback(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 returns-1 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; + } + } + + (void)hid_hotplug_deregister_callback(h_probe); + test_virtual_device_destroy(vdev2); + (void)hp_wait_enumerated(TEST_PID_2, TEST_SERIAL_2, 0, EVENT_TIMEOUT_MS); + 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(hid_hotplug_register_callback(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; + int i, all_next_null = 1; + + CHECK(ensure_absent() == 0); + hp_reset_log("T10"); + + step("register (no ENUMERATE) while the device is absent"); + CHECK(hid_hotplug_register_callback(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); + + test_mutex_lock(&g_log_lock); + for (i = 0; i < g_event_count; i++) + if (g_events[i].handle == h && !g_events[i].next_was_null) + all_next_null = 0; + test_mutex_unlock(&g_log_lock); + CHECK(all_next_null); /* device->next == NULL on EVERY invocation */ + + 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(hid_hotplug_register_callback(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; + + CHECK(ensure_absent() == 0); + hp_reset_log("T12"); + + step("register exact / vid-only / wrong-vid / wildcard callbacks"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_match) == 0); + CHECK(hid_hotplug_register_callback(TEST_VID, 0, ALL_EVENTS, 0, + cb_log, NULL, &h_vid) == 0); + CHECK(hid_hotplug_register_callback(TEST_VID ^ 0x0001, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_wrong) == 0); + CHECK(hid_hotplug_register_callback(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(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_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(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_a) == 0); + CHECK(hid_hotplug_register_callback(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). */ +static int t14_deregister_postcondition(void) +{ + hid_hotplug_callback_handle h_slow = 0, h_bar = 0; + slow_ctx *ctx; + int exited; + + CHECK(ensure_absent() == 0); + hp_reset_log("T14"); + + ctx = (slow_ctx *)calloc(1, sizeof(*ctx)); + CHECK(ctx != NULL); + + step("register the slow callback and a barrier callback"); + if (hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_slow, ctx, &h_slow) != 0) { + free(ctx); + CHECK(!"registration failed"); + } + if (hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &h_bar) != 0) { + (void)hid_hotplug_deregister_callback(h_slow); + free(ctx); + CHECK(!"barrier registration failed"); + } + + step("plug and wait for the slow callback to enter"); + if (test_virtual_device_replug(g_vdev) != TEST_VDEV_OK + || hp_wait_flag(&ctx->entered, EVENT_TIMEOUT_MS) != 0) { + (void)hid_hotplug_deregister_callback(h_slow); + (void)hid_hotplug_deregister_callback(h_bar); + free(ctx); + CHECK(!"the slow callback never entered"); + } + + step("deregister while the callback is (still) inside its invocation"); + CHECK(hid_hotplug_deregister_callback(h_slow) == 0); + test_mutex_lock(&g_log_lock); + exited = ctx->exited; + test_mutex_unlock(&g_log_lock); + CHECK(exited == 1); /* deregistration waited for the invocation */ + + step("free the callback's resources and keep events flowing"); + free(ctx); + 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(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, via its snapshot) 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; + + CHECK(ensure_absent() == 0); + hp_reset_log("T15"); + memset(&ctx, 0, sizeof(ctx)); + + step("register the parent callback"); + CHECK(hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_parent, &ctx, &h_parent) == 0); + + step("plug: the parent registers the child and deregisters itself"); + CHECK(test_virtual_device_replug(g_vdev) == TEST_VDEV_OK); + CHECK(hp_wait_flag(&ctx.acted, EVENT_TIMEOUT_MS) == 0); + + test_mutex_lock(&g_log_lock); + child_rc = ctx.child_rc; + h_child = ctx.child_handle; + self_dereg_rc = ctx.self_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 */ + + step("the child sees the device exactly once (via its snapshot)"); + 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); + + 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; +} + +/* ------------------------------------------------------------------ */ + +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"); + return EXIT_FAILURE; + } + + step("probe hotplug support"); + if (hid_hotplug_register_callback(TEST_VID, TEST_PID, ALL_EVENTS, 0, + cb_log, NULL, &probe) != 0) { + printf("hotplug reported unsupported here - skipping\n"); + hid_exit(); + 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"); + hid_exit(); + return EXIT_SKIP; + } + if (rc != TEST_VDEV_OK || !g_vdev) { + printf("failed to create virtual device (rc=%d)\n", rc); + hid_exit(); + return EXIT_FAILURE; + } + + /* Probed before waiting for enumeration so that providers without + presence toggling skip instantly instead of after a full wait. */ + step("probe unplug/replug 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(); + return EXIT_SKIP; + } + if (rc != TEST_VDEV_OK + || hp_wait_enumerated(TEST_PID, TEST_SERIAL, 0, EVENT_TIMEOUT_MS) != 0 + || test_virtual_device_replug(g_vdev) != TEST_VDEV_OK) { + printf("unplug/replug probe failed\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + return EXIT_FAILURE; + } + + /* The readiness barrier doubling as the presence probe: a virtual + device that never enumerates means this host cannot run the test + (same skip semantics as the device-I/O test). */ + step("wait for the device to enumerate"); + if (hp_wait_enumerated(TEST_PID, TEST_SERIAL, 1, EVENT_TIMEOUT_MS) != 0) { + printf("virtual device did not enumerate - skipping\n"); + test_virtual_device_destroy(g_vdev); + hid_exit(); + return EXIT_SKIP; + } + + printf("running hotplug tests...\n"); + fflush(stdout); + + printf("T6: asynchronous delivery + handle parameter\n"); + report("T6 async_delivery", t6_async_delivery()); + printf("T7: exactly-once (ENUMERATE pass vs live events)\n"); + report("T7 exactly_once", t7_exactly_once()); + printf("T8a: non-zero callback return deregisters\n"); + report("T8a return_deregisters", t8a_return_deregisters()); + printf("T8b: non-zero return stops the rest of the ENUMERATE pass\n"); + report("T8b return_stops_pass", t8b_return_stops_pass()); + printf("T9: ENUMERATE pass delivered before live events\n"); + report("T9 pass_before_live", t9_pass_before_live()); + printf("T10: live ARRIVED/LEFT payloads\n"); + report("T10 live_payloads", t10_live_payloads()); + printf("T11: LEFT without ENUMERATE (zero-window proof)\n"); + report("T11 left_without_enumerate", t11_left_without_enumerate()); + printf("T12: VID/PID filtering\n"); + report("T12 vid_pid_filtering", t12_vid_pid_filtering()); + printf("T13: dispatch in registration order\n"); + report("T13 dispatch_order", t13_dispatch_order()); + printf("T14: deregistration post-condition\n"); + report("T14 deregister_postcondition", t14_deregister_postcondition()); + printf("T15: register/deregister from within a callback\n"); + report("T15 reentrant_registration", t15_reentrant_registration()); + + 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)\n", + g_failures == 0 ? "PASS" : "FAIL", g_failures); + return (g_failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/tests/test_hotplug_api.c b/src/tests/test_hotplug_api.c new file mode 100644 index 000000000..ef47734b7 --- /dev/null +++ b/src/tests/test_hotplug_api.c @@ -0,0 +1,384 @@ +/******************************************************* + 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 "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 fails, this backend/host has no hotplug support + (e.g. a libusb without LIBUSB_CAP_HAS_HOTPLUG) 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; + 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) { + printf(" hotplug reported unsupported here (rc=%d) - skipping\n", rc); + 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 retrievable (non-NULL) global error string. The exact error + text is backend-specific, so only its existence is asserted. */ + +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 */ + int rc = hid_hotplug_register_callback(vid, pid, events, flags, cb, NULL, &handle); + CHECK(rc == -1); + CHECK(handle == 0); + CHECK(hid_error(NULL) != NULL); + 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 larger handle). */ +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, never-issued and already-deregistered + handles fails with -1, sets an error string and leaves a + still-registered callback untouched. */ +static int t3_stale_handles(hid_hotplug_callback_handle stale) +{ + hid_hotplug_callback_handle live = 0; + + step("register a live callback"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_noop, NULL, &live) == 0); + CHECK(live > 0); + + step("deregister invalid handles"); + CHECK(hid_hotplug_deregister_callback(0) == -1); + CHECK(hid_error(NULL) != NULL); + CHECK(hid_hotplug_deregister_callback(-1) == -1); + CHECK(hid_error(NULL) != NULL); + CHECK(hid_hotplug_deregister_callback(live + 1000) == -1); /* never issued */ + CHECK(hid_error(NULL) != NULL); + CHECK(hid_hotplug_deregister_callback(stale) == -1); /* already deregistered */ + CHECK(hid_error(NULL) != NULL); + + step("the live callback is unaffected"); + CHECK(hid_hotplug_deregister_callback(live) == 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"); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_record, NULL, &ha) == 0); + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, 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); + + 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; + 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; + CHECK(hid_hotplug_register_callback(0, 0, ALL_EVENTS, 0, cb_record, NULL, &h) == 0); + CHECK(h > 0); + CHECK(hid_exit() == 0); + } + test_mutex_lock(&g_lock); + g_exit_returned = 0; + test_mutex_unlock(&g_lock); + 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 { + volatile int stop; + long iterations; + long failures; +} churn_ctx; + +static void churn_thread_fn(void *arg) +{ + churn_ctx *ctx = (churn_ctx *)arg; + + while (!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 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) { + ctxs[0].stop = 1; + (void)test_thread_join_timeout(&threads[0], 10000); + CHECK(!"failed to start the second churn thread"); + } + + test_sleep_ms(CHURN_MS); + ctxs[0].stop = 1; + ctxs[1].stop = 1; + + step("join the churn threads"); + CHECK(test_thread_join_timeout(&threads[0], 30000) == 0); + CHECK(test_thread_join_timeout(&threads[1], 30000) == 0); + + 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); + if (!supported) { + test_mutex_destroy(&g_lock); + return EXIT_SKIP; + } + report("T4 implicit_init", rc); + + 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("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_platform.h b/src/tests/test_platform.h index c06a8857b..3c9312193 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,12 @@ #include #else #include + #include #include #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 +37,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 +49,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 +121,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 +138,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 +152,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..ecc9dfaec 100644 --- a/src/tests/test_virtual_device.h +++ b/src/tests/test_virtual_device.h @@ -95,6 +95,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..d5288c65e 100644 --- a/src/tests/test_virtual_device_mac.c +++ b/src/tests/test_virtual_device_mac.c @@ -399,3 +399,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..185e7af13 100644 --- a/src/tests/test_virtual_device_rawgadget.c +++ b/src/tests/test_virtual_device_rawgadget.c @@ -627,39 +627,82 @@ static void *int_in_thread_fn(void *arg) 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; + 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 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); + } + } - dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); - if (!dev) - return TEST_VDEV_ERROR; + 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). */ +static int rg_plug(struct test_virtual_device *dev) +{ + struct usb_raw_init init; + int rc; + + /* Reset every per-plug field so a 2nd/3rd plug behaves exactly like the + first. The signal handler being (re)installed is harmless, and ep0_exited + MUST be 0 here so rg_unplug's SIGUSR1 spin drives the *new* ep0 thread. + The mutex/cond and identity (vendor/product/serial) are intentionally + left untouched -- they persist across the unplug/replug cycle. */ + dev->stop = 0; dev->fd = -1; dev->int_in_ep = -1; dev->int_in_addr = 0x81; + dev->configured = 0; + dev->ep0_started = 0; + dev->int_in_started = 0; + dev->ep0_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); if (dev->fd < 0) { int e = errno; - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + dev->fd = -1; if (e == ENOENT || e == EACCES || e == EPERM || e == ENODEV) return TEST_VDEV_UNAVAILABLE; return TEST_VDEV_ERROR; @@ -672,11 +715,9 @@ int test_virtual_device_create(test_virtual_device **out_dev, 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. */ + /* No dummy_hcd UDC to bind (absent, or already in use) -> skip. */ close(dev->fd); - pthread_cond_destroy(&dev->cond); - pthread_mutex_destroy(&dev->lock); - free(dev); + dev->fd = -1; return TEST_VDEV_UNAVAILABLE; } @@ -692,29 +733,51 @@ int test_virtual_device_create(test_virtual_device **out_dev, 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); 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; + + /* 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 +834,26 @@ 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); 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) +{ + if (!dev) + return TEST_VDEV_ERROR; + return rg_plug(dev); +} 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..e297edea4 100644 --- a/src/tests/test_virtual_device_win.c +++ b/src/tests/test_virtual_device_win.c @@ -31,9 +31,27 @@ #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 contains this token (matched 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 "VHIDMINIUM" struct test_virtual_device { unsigned short vendor_id; @@ -42,6 +60,115 @@ struct test_virtual_device { ULONG feature_len; /* FeatureReportByteLength of the opened device */ }; +/* Case-insensitive: does haystack contain needle (needle already uppercase)? */ +static int contains_ci_upper(const char *haystack, const char *needle_upper) +{ + size_t nlen = strlen(needle_upper); + const char *p; + + if (nlen == 0) + return 1; + for (p = haystack; *p != '\0'; ++p) { + size_t i = 0; + while (i < nlen && p[i] != '\0' && + (char)toupper((unsigned char)p[i]) == needle_upper[i]) + ++i; + if (i == nlen) + return 1; + } + return 0; +} + +/* 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; + char *inst; + + 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) { + free(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[512]; + char *h; + ULONG hwlen = (ULONG)sizeof(hwids); + + if (CM_Locate_DevNodeA(&devinst, inst, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) + continue; + if (CM_Get_DevNode_Registry_PropertyA(devinst, CM_DRP_HARDWAREID, NULL, + hwids, &hwlen, 0) != CR_SUCCESS) + continue; + /* CM_DRP_HARDWAREID is itself a REG_MULTI_SZ; match any of its ids. */ + for (h = hwids; *h != '\0'; h += strlen(h) + 1) { + if (contains_ci_upper(h, VHID_HARDWARE_ID_MATCH)) { + *out_devinst = devinst; + free(list); + return CR_SUCCESS; + } + } + } + + 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, @@ -53,6 +180,18 @@ int test_virtual_device_create(test_virtual_device **out_dev, return TEST_VDEV_ERROR; *out_dev = NULL; + /* Windows cannot create HID devices on the fly; the CI job pre-installs a + single static vhidmini device whose identity is fixed (see + src/tests/windows/driver). Report UNAVAILABLE for any requested device that + is not actually present here - e.g. the mid-pass-stop test's second device - + so such tests skip cleanly instead of waiting for one that can never appear. */ + { + 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) return TEST_VDEV_ERROR; @@ -141,6 +280,100 @@ 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_SUCCESS) + return TEST_VDEV_OK; /* no HID child -> already absent, nothing to disable */ + + cr = CM_Disable_DevNode(child, 0); + 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/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/windows/hid.c b/windows/hid.c index fceaaa05c..44fafbdd4 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 @@ -216,10 +217,57 @@ 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 HID_API_HOTPLUG_ENUMERATE + passes and performs cleanup deferred from the notification callback. + Created with the first callback registration, closed by hid_exit(). */ + PVOID event_work; + + /* 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, the module is pinned, and no + second notification is ever armed next to it (every event would be + delivered twice). */ + unsigned char notification_leaked; + + /* Same failure, scoped to the teardown that has to report it (hid_exit) */ + unsigned char unregistration_failed; + /* Critical section (faster mutex substitute), for both cached device list and callback list changes */ CRITICAL_SECTION critical_section; @@ -234,7 +282,9 @@ 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) */ + /* 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 (see hid_internal_notify_callback). */ struct hid_device_info *devs; } hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ @@ -283,7 +333,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 +344,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 +389,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 +431,117 @@ 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. Everything this file uses above its minimum target is resolved + dynamically instead (see lookup_functions and hid_internal_hotplug_resolve_threadpool). + 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; +/* Thread-local: true only while THIS thread is inside a user hotplug callback, + i.e. it is HIDAPI's internal event context. Thread-specific on purpose - the + event context is not a single fixed thread (a threadpool work item or the CM + notification callback), so a stored thread id would be fragile, and the flag + must be true for the dispatching thread alone, never for a concurrent + application thread. This is how mac/libusb detect the event context (they + compare its thread identity). */ +#if defined(_MSC_VER) +#define HID_THREAD_LOCAL __declspec(thread) +#else +#define HID_THREAD_LOCAL __thread /* GCC/Clang (MinGW, Cygwin): __declspec(thread) is ignored there */ +#endif +static HID_THREAD_LOCAL int hid_in_hotplug_callback = 0; + +/* 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_in_hotplug_callback). An application therefore never + has to serialize hid_error(NULL) against a write it could not see coming, + matching the other backends (libusb, linux, mac). */ +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. + hid_in_hotplug_callback is thread-local and set only around the callback + invocation, so this drops writes on the dispatching thread alone. A + concurrent application thread (its own flag is 0) still records its errors, + and reading a thread-local is not a shared-memory data race. */ + if (hid_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,18 +571,178 @@ 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 and deregistration fail instead of + arming a context that is being destroyed, or calling into libraries that are + being unloaded. 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; + +/* Number of threads currently counted into the hotplug machinery by + hid_internal_hotplug_enter, i.e. inside - or blocked on - the critical + section from a public hotplug call. Guarded by hotplug_init_lock. + hid_exit() destroys the critical section and the quiescence event only after + `hotplug_exiting` has cut off new entries and this count has drained to zero: + deleting a critical section another thread is blocked on (or about to enter) + is undefined behavior. */ +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; +} + +/* 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 nothing is created and *create_error + receives the CreateEvent error code (reporting it 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) +{ + 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(); + return -1; + } + + InitializeCriticalSection(&hid_hotplug_context.critical_section); + + 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 `hotplug_exiting` has cut off new entries AND the count has drained to + zero. A successful call (and only 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; + + hid_internal_lock_acquire(&hotplug_init_lock); + if (hotplug_exiting) { + 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) < 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, L"hid_hotplug_register_callback/CreateEvent"); } + + 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; +} + +/* 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; } int HID_API_EXPORT hid_init(void) @@ -451,10 +770,181 @@ struct hid_hotplug_callback { void *user_data; hid_hotplug_callback_fn callback; + /* HID_API_HOTPLUG_ENUMERATE snapshot taken at registration time, + 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 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; +} + +/* Pins the module this code lives in, so that 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. + Any address inside the module identifies it; hid_hotplug_context is in the very + same translation unit as the callback. */ +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. + With no allocation on either lookup, an out-of-memory condition can no longer + desynchronize the cache from the system: the only allocation left is the one that + describes an arriving device, and failing it simply does not cache the device + (nothing was reported for it either - see hid_internal_notify_callback). + + 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 decides it, with no timing assumption anywhere: the OS delivers + exactly one arrival notification per interface instance, so that overlap is the + only way an arrival can be a duplicate - and in that case the enumeration has, + by construction, already put the device in the cache. Conversely the removal + notification is authoritative and drops the cache entry, so a genuine re-plug + (even onto the same, reused path) is NOT cached and IS reported. */ +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() { /* Unregister the callbacks whose removal was postponed */ @@ -470,29 +960,95 @@ 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 void hid_internal_hotplug_finish_unregistration(HCMNOTIFICATION notify_handle) { - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + CONFIGRET cr; + + if (notify_handle == NULL) { return; } + cr = CM_Unregister_Notification(notify_handle); + + EnterCriticalSection(&hid_hotplug_context.critical_section); + if (cr != CR_SUCCESS) { + /* The OS-side registration may still be live and call into this module at + any time. Everything it can reach has to survive: the critical section and + the quiescence event are never destroyed anyway, the resolved libraries are + never unloaded (see hid_exit), the module is pinned, and no second + notification is ever armed next to this one - the two would deliver every + event twice. hid_exit() reports the failure; this function also runs on the + internal event context, which must never touch the global error string (an + application thread may be reading it through hid_error(NULL)). */ + hid_hotplug_context.notification_leaked = 1; + hid_hotplug_context.unregistration_failed = 1; + 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); +} + +/* 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 void hid_internal_hotplug_wait_quiescent(void) +{ + 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; + } + + if (WaitForSingleObject(hid_hotplug_context.quiescent_event, INFINITE) == WAIT_FAILED) { + /* Should never happen; do not spin forever on a broken event */ + return; + } + } +} + +/* 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() +{ + HCMNOTIFICATION notify_handle; + + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + 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; } if (hid_hotplug_context.devs) { @@ -501,44 +1057,274 @@ static void hid_internal_hotplug_cleanup() 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 HID_API_HOTPLUG_ENUMERATE + 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). */ + int prev_in_cb = hid_in_hotplug_callback; + hid_in_hotplug_callback = 1; + int result = (*callback->callback)(callback->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, callback->user_data); + hid_in_hotplug_callback = 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; } } +} - hid_hotplug_context.notify_handle = NULL; +/* Threadpool work item: delivers pending HID_API_HOTPLUG_ENUMERATE passes + (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.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; + + notify_handle = hid_internal_hotplug_cleanup(); + + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + hid_internal_hotplug_finish_unregistration(notify_handle); } -static void hid_internal_hotplug_exit() +/* 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) { - if (!hid_hotplug_context.mutex_ready) { - /* If the critical section is not initialized, we are safe to assume nothing else is */ - return; + int leaked; + + if (!hid_internal_hotplug_ready()) { + return 0; } + 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; + leaked = (hid_hotplug_context.notification_leaked != 0); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + + return leaked; +} + +/* 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; + int failed; + int keep_machinery; + int machinery_ready; + + /* Nothing may enter or arm the machinery from here on. 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 */ + hid_internal_hotplug_wait_quiescent(); + + EnterCriticalSection(&hid_hotplug_context.critical_section); + if (hid_hotplug_context.pending_unregistrations > 0) { + /* Only reachable if waiting itself failed: treat it as a live notification */ + hid_hotplug_context.notification_leaked = 1; + hid_hotplug_context.unregistration_failed = 1; + hid_internal_hotplug_pin_module(); + } + /* Scoped to this teardown: a later hid_exit() has nothing of its own to report + (a hotplug callback can no longer be registered once a notification leaked) */ + failed = (hid_hotplug_context.unregistration_failed != 0); + hid_hotplug_context.unregistration_failed = 0; + + event_work = hid_hotplug_context.event_work; + /* `failed` implies notification_leaked: they are only ever set together. + 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); } - hid_internal_hotplug_cleanup(); + + 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); - hid_hotplug_context.mutex_ready = 0; - DeleteCriticalSection(&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` (still raised) keeps new callers + out, so only callers already counted in remain - wait those out. They + fail out promptly on their `exiting` checks. */ + for (;;) { + int busy; + + hid_internal_lock_acquire(&hotplug_init_lock); + busy = (hotplug_machinery_users != 0); + if (!busy) { + /* Unpublished under the same acquisition that proved the machinery + idle: with `hotplug_exiting` raised nothing can be counted back + in, so the destruction below cannot race anything. */ + 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) */ + + /* `hotplug_exiting` stays raised until hid_exit() is done unloading the + libraries: see hid_internal_hotplug_exit_done */ + + if (failed) { + register_global_error(L"hid_exit: a hotplug notification could not be unregistered"); + return -1; + } + + 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. + Every other call that goes through them either holds the critical section + or is accounted for by pending_unregistrations, which the teardown above + has already waited out. */ + 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, and the module is pinned) 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 +1560,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 +1634,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 +1671,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 +1690,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 +1750,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 +1782,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 +1793,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 +1833,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 +1859,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 +1910,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,6 +1931,52 @@ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *d } } +/* Delivers one live event to every matching callback registered at this moment. + 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) +{ + /* 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 live dispatch is bound to the + callbacks present at event time, so the connection is reported exactly once */ + struct hid_hotplug_callback *last_at_event = hid_hotplug_context.hotplug_cbs; + while (last_at_event != NULL && last_at_event->next != NULL) { + last_at_event = last_at_event->next; + } + + /* Call the notifications for the device */ + for (struct hid_hotplug_callback *callback = hid_hotplug_context.hotplug_cbs; callback != NULL; 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_in_hotplug_callback); save/restore for nested callbacks. */ + int prev_in_cb = hid_in_hotplug_callback; + hid_in_hotplug_callback = 1; + int result = (callback->callback)(callback->handle, device, hotplug_event, callback->user_data); + hid_in_hotplug_callback = 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; + } + } + + if (callback == last_at_event) { + break; + } + } + + hid_hotplug_context.mutex_in_use = 0; +} + 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; @@ -1100,84 +1994,72 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, EnterCriticalSection(&hid_hotplug_context.critical_section); if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL) { - HANDLE read_handle; - 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); + if (hid_internal_hotplug_is_cached(event_data->u.DeviceInterface.SymbolicLink)) { + /* This connection is already known - and therefore already reported. + The only way that happens is the arm-before-enumerate window at + registration, where the enumeration cached the device and the OS had + already queued this arrival for it (see + hid_internal_hotplug_is_cached). Drop it whole: no second cache + entry, no second dispatch. */ + } else { + /* Open read-only handle to the device */ + HANDLE 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); + /* Check validity of read_handle. */ + if (read_handle != INVALID_HANDLE_VALUE) { + device = hid_internal_get_device_info(event_data->u.DeviceInterface.SymbolicLink, read_handle, NULL); + CloseHandle(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 (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; } - last->next = device; - } else { - hid_hotplug_context.devs = device; } - - CloseHandle(read_handle); + /* else: the interface could not be opened or described. The usual cause + is that it disconnected again before we got to it - no connection is + owed then: nothing is cached, so the removal notification that follows + finds nothing and reports nothing either, which is consistent. An + out-of-memory failure does lose the connection, but there is nothing to + report it with; the cache is left consistent either way, as only fully + described devices are ever 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); /* 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(); + /* 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(); + if (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.event_work != NULL) { + hid_internal_SubmitThreadpoolWork(hid_hotplug_context.event_work); + } } LeaveCriticalSection(&hid_hotplug_context.critical_section); @@ -1185,52 +2067,174 @@ 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_hotplug_context.event_work == NULL) { + 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; + } + } - hotplug_cb->handle = hid_hotplug_context.next_handle++; + if (hid_hotplug_context.notify_handle == NULL) { + GUID interface_class_guid; + CM_NOTIFY_FILTER notify_filter; + 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). */ + 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"); + hid_hotplug_context.notify_handle = NULL; + 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; + /* 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; + } + } + /* 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)) { + 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) { + HCMNOTIFICATION notify_handle; + register_global_error(L"Failed to allocate memory for a device info snapshot"); + hid_free_enumeration(hotplug_cb->replay); + /* Tear the machinery down if this would-be-first callback was starting it */ + notify_handle = hid_internal_hotplug_cleanup(); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + hid_internal_hotplug_finish_unregistration(notify_handle); + free(hotplug_cb); + return -1; + } + replay_tail = &(*replay_tail)->next; + } } - /* 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,79 +2243,134 @@ 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_SubmitThreadpoolWork(hid_hotplug_context.event_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"). + Under the critical section: outside of it this would race with - and wipe - + the error string of a concurrently failing registration. */ + 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; + + /* Never bootstraps the machinery: with no machinery there is nothing this + handle could belong to. And when hid_exit() is in progress, it deregisters + every callback and invalidates every handle - this one is (or is about to + be) one of them. */ + 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) { + if (hid_internal_hotplug_exiting()) { + /* hid_exit() started tearing the machinery down after this call was + counted in: same as above */ + register_global_error(L"Invalid or unknown hotplug callback handle"); LeaveCriticalSection(&hid_hotplug_context.critical_section); + hid_internal_hotplug_leave(); return -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) { + /* 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 we are not allowed to remove the callback, we mark it as pending removal */ @@ -1322,15 +2381,26 @@ 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) { + /* Under the critical section: outside of it this would race with - and wipe - + the error string of a concurrently failing hotplug call */ + register_global_error(L"Invalid or unknown hotplug callback handle"); + } + + notify_handle = hid_internal_hotplug_cleanup(); LeaveCriticalSection(&hid_hotplug_context.critical_section); - return 0; + hid_internal_hotplug_finish_unregistration(notify_handle); + + 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 +2516,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 +2991,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);