diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 4c059aca6..db89e3e44 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -125,7 +125,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install libudev-dev libusb-1.0-0-dev python3-pip ninja-build + sudo apt install autoconf automake libudev-dev libusb-1.0-0-dev pkg-config python3-pip ninja-build sudo -H pip3 install meson - name: Configure CMake run: | @@ -138,6 +138,15 @@ jobs: - name: Build CMake Static working-directory: build/static run: make install + - name: Check pkg-config package + run: | + autoreconf --install --force hidapisrc/tests/pkg-config + mkdir -p build/pkg-config-test + cd build/pkg-config-test + CFLAGS="${GNU_COMPILE_FLAGS}" \ + PKG_CONFIG_PATH="${GITHUB_WORKSPACE}/install/shared/lib/pkgconfig" \ + "${GITHUB_WORKSPACE}/hidapisrc/tests/pkg-config/configure" + make V=1 - name: Run virtual-device tests (uhid -> hidraw; raw-gadget self-skips) working-directory: build/shared run: | @@ -156,6 +165,7 @@ jobs: with: files: "install/shared/lib/libhidapi-libusb.so, \ install/shared/lib/libhidapi-hidraw.so, \ + install/shared/lib/pkgconfig/hidapi.pc, \ install/shared/include/hidapi/hidapi.h, \ install/shared/include/hidapi/hidapi_libusb.h, \ install/static/lib/libhidapi-libusb.a, \ diff --git a/AGENTS.md b/AGENTS.md index 62854f8b2..27c55dc18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,3 +46,41 @@ session itself should be included, as it is not publicly accessible. When a PR fixes an issue or relates to / replaces another issue, the PR description should include a reference to the issue number after the main description but before the `Assisted-by:` attribution, e.g. `Fixes: #123` or `Closes: #124`. + +## HIDAPI API usage contract + +This is the contract HIDAPI expects of its callers. It governs both code review and code +generation: judge the library's behaviour only against usage that respects it, and only +generate example/test code that respects it. The authoritative reference is +[Multi‐threading Notes](https://github.com/libusb/hidapi/wiki/Multi%E2%80%90threading-Notes). + +### Library lifecycle + +- Call `hid_init()` before any other HIDAPI function. Implicit lazy initialization exists, but + must not be relied upon when more than one thread may make the first call. +- Call `hid_exit()` last, after every device has been closed. `hid_exit()` deinitializes the + whole library; no HIDAPI function other than a new `hid_init()` may be called after it, and + device handles obtained before it are invalid. +- `hid_init()`/`hid_exit()` must never run concurrently with any other HIDAPI function. + +### Concurrency + +- HIDAPI v0.x.x is **not thread-safe**. The following must not be called concurrently from + different threads: `hid_init`, `hid_exit`, `hid_enumerate`, `hid_open`, `hid_open_path`, + `hid_close`, `hid_error(NULL)` — the global error string is one reason. +- Functions taking a `hid_device *` are not individually thread-safe, but different devices may + be used from different threads — i.e. a dedicated thread per device. +- `hid_close()` must be serialized against initialization and enumeration calls. +- Since v0.15.0, a dedicated read thread using only `hid_read`, `hid_read_timeout` and + `hid_read_error` is safe alongside other operations on the same device from another thread. + Do not use `hid_error` from such a thread; use `hid_read_error`. +- macOS: `hid_init()` and `hid_exit()` must be called from the same thread, and that thread must + stay alive until all devices are closed and `hid_exit()` has completed. + +### For AI agents and reviewers + +A finding whose trigger requires violating this contract is application misuse, not a library +defect. Examples: calling `hid_exit()` while other threads still use the API, using device +handles after `hid_exit()`, unsynchronized concurrent first-time initialization. Do not report +such scenarios as bugs, and do not add locking or other synchronization to "fix" them. Hardening +the library against misuse is a maintainer design decision, not a review fix. diff --git a/Makefile.am b/Makefile.am index 00bcb73cf..0bde02838 100644 --- a/Makefile.am +++ b/Makefile.am @@ -7,10 +7,30 @@ else pkgconfigdir=$(libdir)/pkgconfig endif -if OS_LINUX -pkgconfig_DATA=pc/hidapi-hidraw.pc pc/hidapi-libusb.pc -else pkgconfig_DATA=pc/hidapi.pc + +if OS_LINUX +pkgconfig_DATA += pc/hidapi-hidraw.pc pc/hidapi-libusb.pc +endif + +if OS_DARWIN +pkgconfig_DATA += pc/hidapi-darwin.pc +endif + +if OS_FREEBSD +pkgconfig_DATA += pc/hidapi-libusb.pc +endif + +if OS_KFREEBSD +pkgconfig_DATA += pc/hidapi-libusb.pc +endif + +if OS_HAIKU +pkgconfig_DATA += pc/hidapi-libusb.pc +endif + +if OS_WINDOWS +pkgconfig_DATA += pc/hidapi-winapi.pc endif SUBDIRS= @@ -45,7 +65,12 @@ if BUILD_TESTGUI SUBDIRS += testgui endif -EXTRA_DIST = udev doxygen +EXTRA_DIST = \ + udev \ + doxygen \ + tests/pkg-config/configure.ac \ + tests/pkg-config/Makefile.am \ + tests/pkg-config/test.c dist_doc_DATA = \ README.md \ diff --git a/README.md b/README.md index cf5e6e02e..5263fee74 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ this is not a problem. This back-end uses libusb-1.0 to communicate directly to a USB device. This back-end will of course not work with Bluetooth devices. +Starting with HIDAPI 0.16, this back-end requires libusb 1.0.16 or newer. + ### Test GUI HIDAPI also comes with a Test GUI. The Test GUI is cross-platform and uses diff --git a/configure.ac b/configure.ac index 1b2051052..de8c133f0 100644 --- a/configure.ac +++ b/configure.ac @@ -63,7 +63,7 @@ case $host in # HIDAPI/libusb libs AC_CHECK_LIB([rt], [clock_gettime], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -lrt"], [hidapi_lib_error librt]) - PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0]) + PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0]) LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS" CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS" ;; @@ -87,7 +87,7 @@ case $host in CFLAGS="$CFLAGS -I/usr/local/include" LDFLAGS="$LDFLAGS -L/usr/local/lib" LIBS="${LIBS}" - PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0]) + PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0]) LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS" CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS" AC_CHECK_LIB([iconv], [iconv_open], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -liconv"], [hidapi_lib_error libiconv]) @@ -100,7 +100,7 @@ case $host in os="kfreebsd" threads="pthreads" - PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0]) + PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0]) LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS" CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS" ;; @@ -112,7 +112,7 @@ case $host in os="haiku" threads="pthreads" - PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.9], true, [hidapi_lib_error libusb-1.0]) + PKG_CHECK_MODULES([libusb], [libusb-1.0 >= 1.0.16], true, [hidapi_lib_error libusb-1.0]) LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} $libusb_LIBS" CFLAGS_LIBUSB="${CFLAGS_LIBUSB} $libusb_CFLAGS" AC_CHECK_LIB([iconv], [libiconv_open], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -liconv"], [hidapi_lib_error libiconv]) @@ -237,12 +237,52 @@ AM_CONDITIONAL(OS_WINDOWS, test "x$os" = xwindows) AC_CONFIG_HEADERS([config.h]) +m4_define([HIDAPI_CONFIGURE_BACKEND_PC], [ + AC_CONFIG_FILES([$1:pc/hidapi-backend-specific.pc.in], [ + sed \ + -e "s|@HIDAPI_BACKEND_NAME@|$2|g" \ + -e "s|@HIDAPI_BACKEND_DESCRIPTION@|$3|g" \ + -e "s|@HIDAPI_BACKEND_LIBRARY_NAME@|$4|g" \ + "$1" > "$1.tmp" && + mv "$1.tmp" "$1" + ], [$5]) +]) + +HIDAPI_LIB_NAME=hidapi +HIDAPI_LIBUSB_LIB_NAME=hidapi if test "x$os" = "xlinux"; then - AC_CONFIG_FILES([pc/hidapi-hidraw.pc]) - AC_CONFIG_FILES([pc/hidapi-libusb.pc]) -else - AC_CONFIG_FILES([pc/hidapi.pc]) + HIDAPI_LIB_NAME=hidapi-hidraw + HIDAPI_LIBUSB_LIB_NAME=hidapi-libusb + HIDAPI_CONFIGURE_BACKEND_PC( + [pc/hidapi-hidraw.pc], + [hidapi-hidraw], + [C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the hidraw implementation.], + [hidapi-hidraw]) +elif test "x$os" = "xdarwin"; then + HIDAPI_CONFIGURE_BACKEND_PC( + [pc/hidapi-darwin.pc], + [hidapi-darwin], + [C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the Darwin implementation.], + [hidapi]) +elif test "x$os" = "xwindows"; then + HIDAPI_CONFIGURE_BACKEND_PC( + [pc/hidapi-winapi.pc], + [hidapi-winapi], + [C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the WinAPI implementation.], + [hidapi]) +fi +if test "x$os" != "xdarwin" && test "x$os" != "xwindows"; then + HIDAPI_CONFIGURE_BACKEND_PC( + [pc/hidapi-libusb.pc], + [hidapi-libusb], + [C Library for USB HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the libusb implementation.], + [$hidapi_libusb_lib_name], + [hidapi_libusb_lib_name="$HIDAPI_LIBUSB_LIB_NAME"]) fi +m4_undefine([HIDAPI_CONFIGURE_BACKEND_PC]) + +AC_SUBST([HIDAPI_LIB_NAME]) +AC_CONFIG_FILES([pc/hidapi.pc]) AC_SUBST(LTLDFLAGS) diff --git a/libusb/CMakeLists.txt b/libusb/CMakeLists.txt index 62efd3c1e..d550828c9 100644 --- a/libusb/CMakeLists.txt +++ b/libusb/CMakeLists.txt @@ -13,7 +13,7 @@ if(TARGET usb-1.0) target_link_libraries(hidapi_libusb PRIVATE usb-1.0) else() include(FindPkgConfig) - pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.9) + pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.16) target_link_libraries(hidapi_libusb PRIVATE PkgConfig::libusb) endif() @@ -105,4 +105,8 @@ if(HIDAPI_INSTALL_TARGETS) ) endif() -hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-libusb.pc.in") +hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-backend-specific.pc.in" + OUTPUT_NAME "hidapi-libusb" + LIBRARY_NAME "hidapi-libusb" + DESCRIPTION "C Library for USB HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the libusb implementation." +) diff --git a/libusb/hid.c b/libusb/hid.c index abf10bf8f..c21106758 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -30,8 +30,10 @@ #include #include #include +#include #include #include +#include /* Unix */ #include @@ -147,6 +149,10 @@ struct hid_device_ { hidapi_error_ctx error; wchar_t *last_read_error_str; + + unsigned int write_timeout_ms; + unsigned int send_output_report_timeout_ms; + unsigned int send_feature_report_timeout_ms; }; static struct hid_api_version api_version = { @@ -159,12 +165,73 @@ static libusb_context *usb_context = NULL; static hidapi_error_ctx last_global_error; +/* Serializes mutations of the global error state: hid_hotplug_register_callback() + * and hid_hotplug_deregister_callback() are thread-safe by contract and their + * failure paths write last_global_error concurrently (an unserialized + * register_string_error() would double-free the stored string). */ +static pthread_mutex_t hid_global_error_mutex = PTHREAD_MUTEX_INITIALIZER; + +/* Identity of the hotplug callback thread - the one HIDAPI-owned thread that can + * reach the public API, as it is the thread the callbacks run on. + * + * The global error state belongs to the application: hid_error(NULL) frees and + * replaces the cached string, and an application cannot serialize its own calls + * against a thread it does not know about. Registering or deregistering a + * callback from within a callback is explicitly allowed (see hidapi.h), and both + * write the global error state on failure, for example when deregistering an + * already removed handle. Those writes are therefore DROPPED on this thread + * (see register_libusb_error()/register_string_error()): + * a call that fails there still reports the failure through its return value, + * only the error string of the process is left alone. + * + * Guarded by hid_global_error_mutex, which is a leaf lock (never held while + * another one is acquired), so this adds no lock-order edge. Only one callback + * thread exists at a time (see threads_running). */ +static pthread_t hid_callback_thread_id; +static unsigned char hid_callback_thread_id_valid; + +/* Called by the callback thread as its first and last action */ +static void hid_internal_callback_thread_enter(void) +{ + pthread_mutex_lock(&hid_global_error_mutex); + hid_callback_thread_id = pthread_self(); + hid_callback_thread_id_valid = 1; + pthread_mutex_unlock(&hid_global_error_mutex); +} + +static void hid_internal_callback_thread_leave(void) +{ + pthread_mutex_lock(&hid_global_error_mutex); + hid_callback_thread_id_valid = 0; + pthread_mutex_unlock(&hid_global_error_mutex); +} + +/* Called with hid_global_error_mutex held */ +static int hid_internal_on_callback_thread(void) +{ + return hid_callback_thread_id_valid && pthread_equal(pthread_self(), hid_callback_thread_id); +} + struct hid_hotplug_queue { + /* The device this message is about; NULL marks a request to flush + * the pending HID_API_HOTPLUG_ENUMERATE snapshots (a "replay marker") */ libusb_device* device; - int event; /* Arrived or removed */ + int event; /* Arrived or removed; unused for replay markers */ struct hid_hotplug_queue* next; }; +struct hid_hotplug_device { + /* NULL after a reconciled removal while a snapshot arrival is pending. */ + struct hid_device_info *info; + libusb_device *device; /* Referenced identity of this connection */ + /* A snapshot entry may still have an unprocessed live arrival. */ + unsigned char arrival_seen; + /* Nonzero once reconciliation finds it gone: hide it from snapshots and + * deliver its removal only to handles below this registration boundary. */ + hid_hotplug_callback_handle removed_before; + struct hid_hotplug_device *next; +}; + static struct hid_hotplug_context { /* A separate libusb context for hotplug events: helps avoid mutual blocking with read_thread's */ libusb_context * context; @@ -178,27 +245,60 @@ static struct hid_hotplug_context { /* A thread that fills the event queue */ hidapi_thread_state libusb_thread; - /* A separate thread which processes hidapi's internal event queue */ + /* A separate thread which processes hidapi's internal event queue. + * Its condition parks the callback thread while !shutdown_pending and + * shutdown waiters while shutdown_pending; flag transitions broadcast. */ hidapi_thread_state callback_thread; /* This mutex prevents changes to the callback list */ pthread_mutex_t mutex; /* Boolean flags */ + /* `mutex` (and the thread states) have been initialized. A one-way latch: + * written once under hid_hotplug_init_mutex and read under it as well; the + * objects it advertises are never destroyed, so once it is set, locking + * `mutex` is safe forever. */ unsigned char mutex_ready; unsigned char mutex_in_use; unsigned char cb_list_dirty; - + /* The event threads have been started and their join not yet claimed */ + unsigned char threads_running; + /* A thread has claimed the join of the event threads (threads_running is + * already cleared) but has not completed it yet. It releases `mutex` while + * joining, so in that window the event threads may still be running - and + * still draining messages into `devs`, which nobody else may free until the + * join is through (see hid_internal_hotplug_finish_shutdown). */ + unsigned char join_claimed; + /* Tells the event threads to wind down; set when the last callback is + * removed and cleared once they have been joined. Always written under BOTH + * `mutex` and callback_thread's mutex (see + * hid_internal_hotplug_set_shutdown_pending), so a reader may hold either. + * The join is performed synchronously on the initiating application thread; + * when the shutdown is initiated from the callback (event) thread itself, + * which cannot join itself, it is deferred to the next registration or + * hid_exit(). */ + unsigned char shutdown_pending; + /* Completed wind-downs; protected by callback_thread's mutex */ + unsigned long shutdown_generation; + + /* Pending messages for the callback thread; protected by callback_thread's mutex */ struct hid_hotplug_queue* queue; + /* A removal was dropped; protected by callback_thread's mutex. Cleared + * before reconciliation, restored on failure or another dropped removal. */ + unsigned char cache_stale; /* Linked list of the hotplug callbacks */ struct hid_hotplug_callback *hotplug_cbs; - /* Linked list of the device infos (mandatory when the device is disconnected). + /* Linked list of the devices and their infos (needed after disconnection). * Protected by `mutex`: all reads, writes and the final free during teardown - * are performed while holding it. The teardown free runs in - * hid_internal_hotplug_cleanup() after the hotplug threads have been joined. */ - struct hid_device_info *devs; + * are performed while holding it. Freed by + * hotplug_thread() after joining the callback thread, + * hid_internal_hotplug_cleanup()/hid_internal_hotplug_exit() when the threads + * never ran or were already joined (and no join is claimed), or + * hid_internal_hotplug_unwind_registration() on a failed first registration. + * Device references are released before their libusb context is destroyed. */ + struct hid_hotplug_device *devs; } hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ uint16_t get_usb_code_for_current_locale(void); @@ -211,6 +311,9 @@ static hid_device *new_hid_device(void) return NULL; dev->blocking = 1; + dev->write_timeout_ms = 1000; + dev->send_output_report_timeout_ms = 1000; + dev->send_feature_report_timeout_ms = 1000; hidapi_thread_state_init(&dev->thread_state); @@ -387,18 +490,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 +783,232 @@ struct hid_hotplug_callback hid_hotplug_callback_fn callback; void* user_data; int events; + /* Deep-copied registration-time snapshot of the matching connected devices + * (HID_API_HOTPLUG_ENUMERATE): delivered asynchronously on the event thread + * as synthetic arrival events, always before any live event for this + * callback. Protected by hid_hotplug_context.mutex. */ + struct hid_device_info* replay; struct hid_hotplug_callback* next; hid_hotplug_callback_handle handle; }; -static void hid_internal_hotplug_remove_postponed() +static void hid_internal_hotplug_free_devices(struct hid_hotplug_device *devs) +{ + while (devs) { + struct hid_hotplug_device *next = devs->next; + hid_free_enumeration(devs->info); + libusb_unref_device(devs->device); + free(devs); + devs = next; + } +} + +/* Takes ownership of info on success only */ +static struct hid_hotplug_device *hid_internal_hotplug_create_device(libusb_device *device, struct hid_device_info *info) +{ + struct hid_hotplug_device *dev = (struct hid_hotplug_device *) calloc(1, sizeof(struct hid_hotplug_device)); + if (dev) { + dev->info = info; + dev->device = libusb_ref_device(device); + } + return dev; +} + +/* Sets shutdown_pending and wakes everyone who may be waiting for the change: + * the event threads, and any thread in hid_internal_hotplug_wait_shutdown(). + * Called with `mutex` held, so every write to shutdown_pending is made under + * BOTH `mutex` and the callback thread's mutex - a reader may therefore hold + * either one of them. */ +static void hid_internal_hotplug_set_shutdown_pending(unsigned char value) +{ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.shutdown_pending = value; + if (!value) { + hid_hotplug_context.shutdown_generation++; + } + hidapi_thread_cond_broadcast(&hid_hotplug_context.callback_thread); + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); +} + +static void hid_internal_hotplug_remove_postponed(void) { /* Unregister the callbacks whose removal was postponed */ - /* This function is always called inside a locked mutex */ - /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { + /* This function is always called with `mutex` held, which implies the + * machinery is initialized: locking it is the only way to get here */ + /* However, any actions are only allowed if the mutex is NOT in use */ + if (hid_hotplug_context.mutex_in_use) { return; } - - /* Traverse the list of callbacks and check if any were marked for removal */ - struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - while (*current) { - struct hid_hotplug_callback *callback = *current; - if (!callback->events) { - *current = (*current)->next; - free(callback); - continue; + + if (hid_hotplug_context.cb_list_dirty) { + /* Traverse the list of callbacks and check if any were marked for removal */ + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current) { + struct hid_hotplug_callback *callback = *current; + if (!callback->events) { + *current = callback->next; + /* An undelivered ENUMERATE snapshot dies with its callback */ + hid_free_enumeration(callback->replay); + free(callback); + continue; + } + current = &callback->next; } - current = &callback->next; + + /* Clear the flag so we don't start the cycle unless necessary */ + hid_hotplug_context.cb_list_dirty = 0; + } + + if (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.threads_running && !hid_hotplug_context.shutdown_pending) { + /* The last callback is gone: ask the event threads to wind down. The + * caller joins them (hid_internal_hotplug_cleanup_sync()), unless this + * runs on the event thread itself - it cannot join itself, so the join + * is then deferred to the next registration or hid_exit(). */ + hid_internal_hotplug_set_shutdown_pending(1); } - - /* Clear the flag so we don't start the cycle unless necessary */ - hid_hotplug_context.cb_list_dirty = 0; } -static void hid_internal_hotplug_cleanup() +static void hid_internal_hotplug_cleanup(void) { - if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + /* Called with `mutex` held, which implies the machinery is initialized */ + if (hid_hotplug_context.mutex_in_use) { return; } - /* Before checking if the list is empty, clear any entries whose removal was postponed first */ + /* Before checking if the list is empty, clear any entries whose removal was + * postponed first; this also winds the event threads down (shutdown_pending) + * once the list becomes empty */ hid_internal_hotplug_remove_postponed(); if (hid_hotplug_context.hotplug_cbs != NULL) { return; } - /* Wait for both threads to stop */ + if (!hid_hotplug_context.threads_running && !hid_hotplug_context.join_claimed) { + /* The event threads either never ran or have already been joined: we + * have exclusive access to `devs` (the caller holds `mutex`). + * A merely CLAIMED join does not qualify: threads_running is already + * cleared, but the threads are still running and may be draining + * messages into `devs` - the pump frees it once the drainer is gone. */ + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + } + /* When the threads are still winding down, the pump frees `devs` and + * hid_internal_hotplug_finish_shutdown() joins it (releasing `mutex` + * while joining): joining here could deadlock, as the callback + * thread locks `mutex` to drain its queue while the caller of this + * function is holding it. */ +} + +/* Completes the wind-down of the event threads (a claimed join): joins them + * and clears/broadcasts shutdown_pending for any other + * thread waiting for the wind-down to finish. + * Called with `mutex` held (recursion level 1) and shutdown_pending set; + * temporarily releases `mutex` while joining so the callback thread can drain + * its queue (process_hotplug_event locks it); on return `mutex` is held again. + * Must not run on the event thread itself. */ +static void hid_internal_hotplug_finish_shutdown(void) +{ + /* Claim the join: other threads now see threads_running == 0 and wait for + * shutdown_pending to be cleared instead of joining a second time. + * join_claimed keeps them from mistaking the cleared threads_running for + * "the threads are gone" while `mutex` is released below - they are not, and + * `devs` stays theirs until the join is through. */ + hid_hotplug_context.threads_running = 0; + hid_hotplug_context.join_claimed = 1; + pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* The libusb thread joins the callback thread on its way out */ hidapi_thread_join(&hid_hotplug_context.libusb_thread); + pthread_mutex_lock(&hid_hotplug_context.mutex); + hid_hotplug_context.join_claimed = 0; - /* Both hotplug threads have exited: we now have exclusive access to `devs` - * (the caller holds `mutex` and no hotplug event can reach process_hotplug_event). */ - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; + /* Announce the completed wind-down to hid_internal_hotplug_wait_shutdown() */ + hid_internal_hotplug_set_shutdown_pending(0); +} + +/* Waits out a wind-down that another thread has already claimed (threads_running + * cleared while shutdown_pending is still set): a busy `unlock`/`lock` spin would + * be unfair and can livelock under a real-time scheduling policy, so we block on + * the callback thread's condition, which the joiner broadcasts once it is done. + * Called with `mutex` held (recursion level 1); releases and re-acquires it while + * waiting. Must not run on the event thread itself. */ +static void hid_internal_hotplug_wait_shutdown(void) +{ + unsigned long generation; + + if (!hid_hotplug_context.shutdown_pending) { + return; + } + + /* Acquire the mutex that guards shutdown_pending (and its condition) BEFORE + * releasing `mutex`, so the joiner cannot complete in between and leave us + * waiting for a broadcast that has already happened */ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + generation = hid_hotplug_context.shutdown_generation; + pthread_mutex_unlock(&hid_hotplug_context.mutex); + while (hid_hotplug_context.shutdown_pending && hid_hotplug_context.shutdown_generation == generation) { + hidapi_thread_cond_wait(&hid_hotplug_context.callback_thread); + } + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + pthread_mutex_lock(&hid_hotplug_context.mutex); +} + +/* Called with `mutex` held. With no callbacks left, the caller must be off + * the event thread at recursion level 1. + * Both join and wait release it: re-check which generation needs settling. */ +static void hid_internal_hotplug_settle_shutdown(void) +{ + while (hid_hotplug_context.hotplug_cbs == NULL && hid_hotplug_context.shutdown_pending) { + if (hid_hotplug_context.threads_running) { + hid_internal_hotplug_finish_shutdown(); + } + else { + hid_internal_hotplug_wait_shutdown(); + } + } } -static void hid_internal_hotplug_init() +/* Runs the postponed-removal cleanup and, unless this is the event thread + * dispatching (mutex_in_use), completes the wind-down of the event threads + * synchronously once the last callback is gone: when this returns on an + * application thread, the machinery is fully stopped, no callback can be + * invoked anymore and the library may be safely unloaded. + * The event thread cannot join itself, so a shutdown initiated from within a + * callback stays deferred to the next registration or hid_exit(). + * Called with `mutex` held (recursion level 1). */ +static void hid_internal_hotplug_cleanup_sync(void) { + hid_internal_hotplug_cleanup(); + + if (hid_hotplug_context.mutex_in_use || hid_hotplug_context.hotplug_cbs != NULL) { + return; + } + + hid_internal_hotplug_settle_shutdown(); +} + +/* Serializes the one-time initialization of `mutex` and publishes the + * mutex_ready flag that advertises it: mutex_ready is read under this mutex, so + * no reader observes it without a happens-before relation to the initialization + * it advertises (a plain read would be a data race, and C11 atomics are not + * available on the C99 baseline). + * + * This mutex is a leaf: it is never held while another one is acquired. Holding + * it across the acquisition of `mutex` would deadlock, as the event thread takes + * them in the opposite order whenever a callback registers another callback + * (it already holds `mutex` and then needs this one). */ +static pthread_mutex_t hid_hotplug_init_mutex = PTHREAD_MUTEX_INITIALIZER; + +/* Initializes the hotplug machinery if needed, then locks `mutex`. + * Once initialized, `mutex` and the thread states live for the rest of the + * process: hid_internal_hotplug_exit() winds the machinery down but does NOT + * destroy them (mutex_ready is a one-way latch). Destroying `mutex` would race + * every thread that is about to lock it - it cannot be done safely without a + * lock, and taking one around it is what the lock-order note above rules out. */ +static void hid_internal_hotplug_init_and_lock(void) +{ + pthread_mutex_lock(&hid_hotplug_init_mutex); if (!hid_hotplug_context.mutex_ready) { hidapi_thread_state_init(&hid_hotplug_context.libusb_thread); hidapi_thread_state_init(&hid_hotplug_context.callback_thread); @@ -716,35 +1021,75 @@ static void hid_internal_hotplug_init() pthread_mutexattr_destroy(&attr); /* Set state to Ready */ - hid_hotplug_context.mutex_ready = 1; hid_hotplug_context.mutex_in_use = 0; hid_hotplug_context.cb_list_dirty = 0; + hid_hotplug_context.threads_running = 0; + hid_hotplug_context.join_claimed = 0; + hid_hotplug_context.shutdown_pending = 0; if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; + + hid_hotplug_context.mutex_ready = 1; } + pthread_mutex_unlock(&hid_hotplug_init_mutex); + + pthread_mutex_lock(&hid_hotplug_context.mutex); } -static void hid_internal_hotplug_exit() +/* Locks `mutex` if the hotplug machinery has ever been initialized; returns -1 + * (without locking anything) if it has not */ +static int hid_internal_hotplug_lock(void) { - if (!hid_hotplug_context.mutex_ready) { - return; + unsigned char ready; + + pthread_mutex_lock(&hid_hotplug_init_mutex); + ready = hid_hotplug_context.mutex_ready; + pthread_mutex_unlock(&hid_hotplug_init_mutex); + + if (!ready) { + return -1; } pthread_mutex_lock(&hid_hotplug_context.mutex); + return 0; +} + +static void hid_internal_hotplug_exit(void) +{ + /* Initialize the machinery if it never was, instead of taking a lock-free + * shortcut for that case: the common path below then handles it naturally + * (empty callback list, no threads to wind down). The application must + * serialize hid_exit() against registration, as it does against hid_init(). */ + hid_internal_hotplug_init_and_lock(); + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; - /* Remove all callbacks from the list */ + /* Remove all callbacks from the list (undelivered ENUMERATE snapshots die with them) */ while (*current) { struct hid_hotplug_callback* next = (*current)->next; + hid_free_enumeration((*current)->replay); free(*current); *current = next; } - hid_internal_hotplug_cleanup(); - pthread_mutex_unlock(&hid_hotplug_context.mutex); - hid_hotplug_context.mutex_ready = 0; - pthread_mutex_destroy(&hid_hotplug_context.mutex); + hid_hotplug_context.cb_list_dirty = 0; + + if (hid_hotplug_context.threads_running) { + /* Request the wind-down */ + hid_internal_hotplug_set_shutdown_pending(1); + } + hid_internal_hotplug_settle_shutdown(); + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + + /* The event threads are gone. Destroy the main context under `mutex` too; + * libusb_exit() does not re-enter HIDAPI. */ + if (usb_context) { + libusb_exit(usb_context); + usb_context = NULL; + } - hidapi_thread_state_destroy(&hid_hotplug_context.callback_thread); - hidapi_thread_state_destroy(&hid_hotplug_context.libusb_thread); + /* `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 +1117,16 @@ int HID_API_EXPORT hid_init(void) int HID_API_EXPORT hid_exit(void) { - if (usb_context) { - libusb_exit(usb_context); - usb_context = NULL; - hid_internal_hotplug_exit(); - } + /* Stop the hotplug machinery before destroying the main usb_context. + * The application must serialize this call against registration and must + * never make it from a hotplug callback (see hidapi.h). */ + hid_internal_hotplug_exit(); /* Free global error state */ + pthread_mutex_lock(&hid_global_error_mutex); free_hidapi_error(&last_global_error); memset(&last_global_error, 0, sizeof(last_global_error)); + pthread_mutex_unlock(&hid_global_error_mutex); return 0; } @@ -1067,7 +1413,7 @@ static int should_enumerate_interface(unsigned short vendor_id, const struct lib return 0; } -static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id) +static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id, int *oom) { struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; @@ -1089,8 +1435,14 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns } res = libusb_get_active_config_descriptor(dev, &conf_desc); - if (res < 0) - libusb_get_config_descriptor(dev, 0, &conf_desc); + if (res < 0 && res != LIBUSB_ERROR_NO_MEM) + res = libusb_get_config_descriptor(dev, 0, &conf_desc); + if (res == LIBUSB_ERROR_NO_MEM) { + if (oom) { + *oom = 1; + } + return NULL; + } if (conf_desc) { for (j = 0; j < conf_desc->bNumInterfaces; j++) { const struct libusb_interface *intf = &conf_desc->interface[j]; @@ -1116,6 +1468,13 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns #endif tmp = create_device_info_for_device(dev, handle, &desc, conf_desc->bConfigurationValue, intf_desc->bInterfaceNumber); + if (!tmp || !tmp->path) { + if (oom) { + *oom = 1; + } + hid_free_enumeration(tmp); + tmp = NULL; + } if (tmp) { #ifdef INVASIVE_GET_USAGE /* TODO: have a runtime check for this section. */ @@ -1164,27 +1523,57 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns return root; } -struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +/* Enumerates the connected HID devices. Unlike hid_enumerate(), it tells a + * genuine failure apart from an empty result: `*failed` is set to 1 only when + * the enumeration itself failed (and the global error describes why), and to 0 + * when the system simply has no matching device (NULL is returned in both + * cases). No error is registered for the empty case. Both public enumeration + * and the hotplug snapshot fail on allocation failure, rather than returning + * a partial list. With `cache` set, stores the infos and connection identities + * there instead of returning a public list. The caller initializes `ctx`. */ +static struct hid_device_info *hid_internal_enumerate(libusb_context *ctx, unsigned short vendor_id, unsigned short product_id, int *failed, struct hid_hotplug_device **cache) { libusb_device **devs; libusb_device *dev; ssize_t num_devs; int i = 0; + int oom = 0; + struct hid_hotplug_device **cache_tail = cache; struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; - if (hid_init() < 0) - /* register_global_error: global error is set by hid_init */ - return NULL; + *failed = 1; + if (cache) { + *cache = NULL; + } - num_devs = libusb_get_device_list(usb_context, &devs); + num_devs = libusb_get_device_list(ctx, &devs); if (num_devs < 0) { register_libusb_error(&last_global_error, num_devs, "libusb_get_device_list"); return NULL; } while ((dev = devs[i++]) != NULL) { - struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id); + struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id, &oom); + if (cache && tmp && !oom) { + *cache_tail = hid_internal_hotplug_create_device(dev, tmp); + if (*cache_tail) { + cache_tail = &(*cache_tail)->next; + continue; + } + oom = 1; + } + if (oom) { + hid_free_enumeration(tmp); + hid_free_enumeration(root); + if (cache) { + hid_internal_hotplug_free_devices(*cache); + *cache = NULL; + } + libusb_free_device_list(devs, 1); + register_string_error(&last_global_error, "Failed to allocate memory for the device enumeration"); + return NULL; + } if (cur_dev) { cur_dev->next = tmp; } @@ -1202,7 +1591,22 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, u libusb_free_device_list(devs, 1); - if (root == NULL) { + *failed = 0; + + return root; +} + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + int failed = 0; + struct hid_device_info *root; + if (hid_init() < 0) { + /* register_global_error: global error is set by hid_init */ + return NULL; + } + root = hid_internal_enumerate(usb_context, vendor_id, product_id, &failed, NULL); + + if (root == NULL && !failed) { if (vendor_id == 0 && product_id == 0) { register_string_error(&last_global_error, "No HID devices found in the system."); } else { @@ -1227,54 +1631,172 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } -static int match_libusb_to_info(libusb_device *device, struct hid_device_info* info) +/* Creates a standalone (next == NULL) deep copy of a single device info entry */ +static struct hid_device_info *hid_internal_copy_device_info(const struct hid_device_info *src) { - /* make a path from this libusb device, but leave the last 2 fields as 0 */ - char pseudo_path[64]; - get_path(&pseudo_path, device, 0, 0); - int len = strlen(pseudo_path) - sizeof("0.0"); - /* If the path on this HID device matches the template, aside from the last 2 fields, */ - /* we assume the HID device is located on this libusb device */ - return !strncmp(info->path, pseudo_path, len); + struct hid_device_info *copy = (struct hid_device_info *) calloc(1, sizeof(struct hid_device_info)); + if (copy == NULL) { + return NULL; + } + + *copy = *src; + copy->next = NULL; + copy->path = src->path ? strdup(src->path) : NULL; + copy->serial_number = src->serial_number ? wcsdup(src->serial_number) : NULL; + copy->manufacturer_string = src->manufacturer_string ? wcsdup(src->manufacturer_string) : NULL; + copy->product_string = src->product_string ? wcsdup(src->product_string) : NULL; + + if ((src->path && !copy->path) + || (src->serial_number && !copy->serial_number) + || (src->manufacturer_string && !copy->manufacturer_string) + || (src->product_string && !copy->product_string)) { + hid_free_enumeration(copy); + return NULL; + } + + return copy; } -static void hid_internal_invoke_callbacks(struct hid_device_info* info, hid_hotplug_event event) +/* 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; + } +} + +/* 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); +/* Record an arrival already represented in the cache, including a retired + * snapshot identity retained only to suppress this delayed arrival. */ +static int hid_internal_hotplug_is_known_device(libusb_device *device) +{ + for (struct hid_hotplug_device **current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { + struct hid_hotplug_device *dev = *current; + if (device == dev->device) { + dev->arrival_seen = 1; + if (dev->info == NULL) { + *current = dev->next; + dev->next = NULL; + hid_internal_hotplug_free_devices(dev); + } + return 1; + } + } + + return 0; } -static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void * user_data) +static int hid_internal_hotplug_cache_stale(void) { - (void)ctx; - (void)user_data; + int stale; + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + stale = hid_hotplug_context.cache_stale; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + return stale; +} - /* Make sure we HOLD the device until we are done with it - otherwise libusb would delete it the moment we exit this function */ - libusb_ref_device(device); +/* Called with `mutex` held. Mark departed entries without freeing infos that + * an active callback may still be using; the callback thread dispatches their + * removals after the current message. */ +static int hid_internal_hotplug_reconcile(void) +{ + libusb_device **devices; + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 0; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + ssize_t count = libusb_get_device_list(hid_hotplug_context.context, &devices); + if (count < 0) { + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 1; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + return (int) count; + } + + for (struct hid_hotplug_device *dev = hid_hotplug_context.devs; dev != NULL; dev = dev->next) { + ssize_t i; + for (i = 0; i < count; i++) { + if (devices[i] == dev->device) { + break; + } + } + if (i == count && !dev->removed_before) { + dev->removed_before = hid_hotplug_context.next_handle; + } + } + libusb_free_device_list(devices, 1); + return 0; +} +/* Appends a message for the callback thread to the queue and wakes it up. + * A NULL device marks a request to flush the pending ENUMERATE snapshots. */ +static int hid_internal_enqueue_hotplug_message(libusb_device *device, int event) +{ struct hid_hotplug_queue* msg = (struct hid_hotplug_queue*) calloc(1, sizeof(struct hid_hotplug_queue)); if (NULL == msg) { - libusb_unref_device(device); - return 0; + return -1; } msg->device = device; @@ -1300,73 +1822,215 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic return 0; } +static int LIBUSB_CALL hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void * user_data) +{ + (void)ctx; + (void)user_data; + + /* Make sure we HOLD the device until we are done with it - otherwise libusb would delete it the moment we exit this function */ + libusb_ref_device(device); + + if (hid_internal_enqueue_hotplug_message(device, event) != 0) { + /* A dropped arrival cannot be retried. A dropped removal requires cache + * reconciliation before another snapshot or an unknown live arrival. */ + if (event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 1; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + } + libusb_unref_device(device); + } + + return 0; +} + static void process_hotplug_event(struct hid_hotplug_queue* msg) { + if (msg->device == NULL) { + /* A replay marker: deliver the pending ENUMERATE snapshots promptly + * even when there is no live event traffic */ + hid_internal_flush_replays(); + return; + } + /* Lock the mutex to avoid race conditions with hid_hotplug_register_callback(), - * which may iterate devs during HID_API_HOTPLUG_ENUMERATE while holding this mutex. - * The mutex is recursive, so hid_internal_invoke_callbacks() can safely re-acquire it. */ + * which iterates devs during HID_API_HOTPLUG_ENUMERATE while holding this mutex. + * The mutex is recursive, so a callback may safely re-enter the API. */ pthread_mutex_lock(&hid_hotplug_context.mutex); + /* Mark the list of callbacks as in use for the WHOLE message: a callback + * deregistered from within a callback (its own or another's) is only marked + * and gets removed by hid_internal_hotplug_remove_postponed() below, which + * is what keeps the `last` boundary below alive across the invocations. */ + hid_hotplug_context.mutex_in_use = 1; + + /* Compute the dispatch boundary ONCE for the whole message, before any + * callback runs: a callback registered from within a callback (i.e. on this + * thread, while this message is being dispatched) must not observe this + * event - it receives an already-arrived device through its own + * registration-time HID_API_HOTPLUG_ENUMERATE snapshot instead. Recomputing + * the boundary per interface would deliver the remaining interfaces of a + * multi-interface device to such a callback a second time. */ + struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; + while (last != NULL && last->next != NULL) { + last = last->next; + } + if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) { - struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0); - struct hid_device_info* info_cur = info; - while (info_cur) { - /* For each device, call all matching callbacks */ - /* TODO: possibly make the `next` field NULL to match the behavior on other systems */ - hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); - info_cur = info_cur->next; - } - - /* Append all we got to the end of the device list */ - if (info) { - if (hid_hotplug_context.devs != NULL) { - struct hid_device_info* last = hid_hotplug_context.devs; - while (last->next != NULL) { - last = last->next; + /* The device may already be in the cache: the libusb listener is armed + * before the initial enumeration, so a device that connects in between + * is reported both by the snapshot and as a live arrival. Match only + * connection identity: an older queued arrival must not evict a newer + * connection already captured at the same port by the snapshot. Suppress + * retired snapshot identities, but deliver an unseen queued arrival even + * if the device has already disconnected. */ + if (!hid_internal_hotplug_is_known_device(msg->device)) { + if (hid_internal_hotplug_cache_stale()) { + hid_internal_hotplug_reconcile(); + } + struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0, NULL); + struct hid_hotplug_device *dev = info ? hid_internal_hotplug_create_device(msg->device, info) : NULL; + + if (dev) { + dev->arrival_seen = 1; + /* Append everything we got to the end of the device list BEFORE + * invoking any callback: a callback registered from within a + * callback takes its HID_API_HOTPLUG_ENUMERATE snapshot from + * `devs`, and this device - which it is excluded from receiving + * as a live event (see `last` above) - must be in it. */ + struct hid_hotplug_device **tail = &hid_hotplug_context.devs; + while (*tail != NULL) { + tail = &(*tail)->next; + } + *tail = dev; + + for (struct hid_device_info* info_cur = info; info_cur != NULL; info_cur = info_cur->next) { + /* Each invocation describes exactly one device: `device->next` + * is NULL by contract. A shallow copy is passed rather than + * unlinking the entry, as `devs` must stay whole: a callback + * may walk it (through a nested registration) while we are + * dispatching. */ + struct hid_device_info single = *info_cur; + single.next = NULL; + hid_internal_invoke_callbacks(&single, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, last); } - last->next = info; } else { - hid_hotplug_context.devs = info; + hid_free_enumeration(info); } } } else if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { - for (struct hid_device_info **current = &hid_hotplug_context.devs; *current;) { - struct hid_device_info* info = *current; - if (match_libusb_to_info(msg->device, *current)) { - /* If the libusb device that's left matches this HID device, we detach it from the list */ - *current = (*current)->next; - info->next = NULL; - hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); - /* Free every removed device (and its internal allocations) */ - hid_free_enumeration(info); + struct hid_hotplug_device *removed = NULL; + struct hid_hotplug_device **removed_tail = &removed; + + /* Detach EVERY interface of the departed device from `devs` BEFORE + * invoking any callback: a callback registered from within this dispatch + * takes its HID_API_HOTPLUG_ENUMERATE snapshot from `devs`, and the + * interfaces that have not been dispatched yet must not be in it - the + * device is physically gone, so it would receive a synthetic arrival for + * them and never a matching removal (this event is excluded from it by + * the `last` boundary above). */ + for (struct hid_hotplug_device **current = &hid_hotplug_context.devs; *current;) { + struct hid_hotplug_device *dev = *current; + if (msg->device == dev->device) { + if (dev->removed_before) { + /* Registrations whose snapshots excluded this connection must + * not receive its delayed removal either. */ + last = NULL; + for (struct hid_hotplug_callback *cb = hid_hotplug_context.hotplug_cbs; + cb != NULL && cb->handle < dev->removed_before; cb = cb->next) { + last = cb; + } + } + /* Detach only this connection, never its same-port replacement */ + *current = dev->next; + dev->next = NULL; + *removed_tail = dev; + removed_tail = &dev->next; + } else { + current = &dev->next; + } + } + + while (removed != NULL) { + struct hid_hotplug_device *dev = removed; + removed = dev->next; + dev->next = NULL; + for (struct hid_device_info *info = dev->info; info != NULL; info = info->next) { + /* Each invocation describes exactly one device */ + struct hid_device_info single = *info; + single.next = NULL; + if (last != NULL) { + hid_internal_invoke_callbacks(&single, HID_API_HOTPLUG_EVENT_DEVICE_LEFT, last); + } + } + if (dev->removed_before && !dev->arrival_seen && dev->info != NULL) { + /* Only snapshot entries can still have an unprocessed arrival. + * Retain their identity until it is drained, a queued LEFT + * confirms no arrival is pending, or the context exits. */ + hid_free_enumeration(dev->info); + dev->info = NULL; + dev->next = hid_hotplug_context.devs; + hid_hotplug_context.devs = dev; } else { - current = &info->next; + hid_internal_hotplug_free_devices(dev); } } } + hid_hotplug_context.mutex_in_use = 0; + /* Remove the callbacks whose removal was postponed during the dispatch; this + * also winds the event threads down once the last one is gone */ + hid_internal_hotplug_remove_postponed(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); /* Release the libusb device - we are done with it */ libusb_unref_device(msg->device); - /* Cleanup note: this function is called inside a thread that the clenup function would be waiting to finish */ - /* Any callbacks that await removal are removed in hid_internal_invoke_callbacks */ + /* Cleanup note: this function is called inside a thread that the cleanup function would be waiting to finish */ + /* Any callbacks that await removal are removed above */ /* No further cleaning is needed */ } +/* Called on the callback thread between messages, never during a callback. */ +static void hid_internal_hotplug_dispatch_removed(void) +{ + pthread_mutex_lock(&hid_hotplug_context.mutex); + while (1) { + struct hid_hotplug_device *dev = hid_hotplug_context.devs; + while (dev != NULL && (!dev->removed_before || dev->info == NULL)) { + dev = dev->next; + } + if (dev == NULL) { + break; + } + struct hid_hotplug_queue msg; + msg.device = libusb_ref_device(dev->device); + msg.event = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT; + msg.next = NULL; + process_hotplug_event(&msg); + } + pthread_mutex_unlock(&hid_hotplug_context.mutex); +} + static void* callback_thread(void* user_data) { (void) user_data; + /* Publish this thread's identity before any callback can run on it: the + * global error state must not be written from here (see + * hid_callback_thread_id) */ + hid_internal_callback_thread_enter(); + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); - /* We stop the thread if by the moment there are no events left in the queue there are no callbacks left */ + /* We stop the thread once the shutdown is requested (the last callback is + * removed) and the queue has been drained */ while (1) { /* Wait for events to arrive or shutdown signal */ - while (!hid_hotplug_context.queue && hid_hotplug_context.hotplug_cbs) { + while (!hid_hotplug_context.queue && !hid_hotplug_context.shutdown_pending) { hidapi_thread_cond_wait(&hid_hotplug_context.callback_thread); } @@ -1379,24 +2043,29 @@ static void* callback_thread(void* user_data) hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); process_hotplug_event(cur_event); free(cur_event); + hid_internal_hotplug_dispatch_removed(); hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); } - if (!hid_hotplug_context.hotplug_cbs) { + if (hid_hotplug_context.shutdown_pending) { break; } } hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + hid_internal_callback_thread_leave(); + return NULL; } +/* The libusb event thread. The callback thread is started by the registration + * (which can report a failure to start it), not from here, and is joined below. */ static void* hotplug_thread(void* user_data) { (void) user_data; - - hidapi_thread_create(&hid_hotplug_context.callback_thread, callback_thread, NULL); + int error_logged = 0; + const struct timespec retry_delay = { 0, 5000000 }; /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ /* This timeout only affects how much time it takes to stop the thread */ @@ -1404,44 +2073,126 @@ static void* hotplug_thread(void* user_data) tv.tv_sec = 0; tv.tv_usec = 5000; - while (hid_hotplug_context.hotplug_cbs) { + while (1) { + unsigned char shutdown; + /* The shutdown flag is set under callback_thread's mutex: read it under + * the same one to synchronize with the writer */ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + shutdown = hid_hotplug_context.shutdown_pending; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + if (shutdown) { + break; + } + /* This will allow libusb to call the callbacks, which will fill up the queue */ - libusb_handle_events_timeout_completed(hid_hotplug_context.context, &tv, NULL); + int res = libusb_handle_events_timeout_completed(hid_hotplug_context.context, &tv, NULL); + if (res < 0 && res != LIBUSB_ERROR_TIMEOUT && res != LIBUSB_ERROR_INTERRUPTED) { + if (!error_logged) { + LOG("Hotplug event handling failed: (%d) %s\n", res, libusb_error_name(res)); + error_logged = 1; + } + nanosleep(&retry_delay, NULL); + } } - /* Disarm the libusb listener */ + /* Disarm the libusb listener: no new messages can be enqueued after this */ libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); + + hidapi_thread_join(&hid_hotplug_context.callback_thread); + + /* Free anything still in the queue (the callback thread may exit before the + * last messages are enqueued); the devices must be unreferenced before their + * libusb context is destroyed. Nothing else can touch the queue anymore. */ + while (hid_hotplug_context.queue) { + struct hid_hotplug_queue *msg = hid_hotplug_context.queue; + hid_hotplug_context.queue = msg->next; + if (msg->device) { + libusb_unref_device(msg->device); + } + free(msg); + } + + /* Self-removal may defer the pump's join indefinitely. Release its context + * now, after all queued and cached device references have been released. */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; libusb_exit(hid_hotplug_context.context); + hid_hotplug_context.context = NULL; + pthread_mutex_unlock(&hid_hotplug_context.mutex); - /* 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. */ + return NULL; +} + +/* Rolls a failed registration back to the state the hotplug machinery was in + * before it: frees the callback that was never added to the list and, when it + * would have been the first one, tears the freshly created libusb context and + * device cache down again. Called with `mutex` held. For a first registration, + * no event thread is running (the caller must have joined the callback thread + * if it managed to start it); otherwise the earlier generation is untouched + * and only the never-added callback is freed. + * The caller registers the error itself, as the roll-back may overwrite it. */ +static void hid_internal_hotplug_unwind_registration(struct hid_hotplug_callback *hotplug_cb, int is_first_callback) +{ + hid_free_enumeration(hotplug_cb->replay); + free(hotplug_cb); + + if (!is_first_callback) { + return; + } + + /* Drop anything enqueued by synchronous libusb I/O during the snapshot, + * or by the replay marker. Neither event thread is running now. */ hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); - hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + while (hid_hotplug_context.queue) { + struct hid_hotplug_queue *msg = hid_hotplug_context.queue; + hid_hotplug_context.queue = msg->next; + if (msg->device) { + libusb_unref_device(msg->device); + } + free(msg); + } hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); - hidapi_thread_join(&hid_hotplug_context.callback_thread); - - return NULL; + libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); + hid_internal_hotplug_free_devices(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + libusb_exit(hid_hotplug_context.context); + hid_hotplug_context.context = NULL; } int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle) { + if (callback_handle != NULL) { + *callback_handle = 0; + } + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + register_string_error(&last_global_error, "Hotplug is not supported by this version of libusb"); return -1; } /* Check params */ if (events == 0 - || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT)) - || (flags & ~(HID_API_HOTPLUG_ENUMERATE)) - || callback == NULL) { + || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT))) { + register_string_error(&last_global_error, "Invalid hotplug events mask"); + return -1; + } + + if (flags & ~(HID_API_HOTPLUG_ENUMERATE)) { + register_string_error(&last_global_error, "Invalid hotplug flags"); + return -1; + } + + if (callback == NULL) { + register_string_error(&last_global_error, "Hotplug callback function is NULL"); return -1; } struct hid_hotplug_callback* hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); if (hotplug_cb == NULL) { + register_string_error(&last_global_error, "Failed to allocate memory for a hotplug callback"); return -1; } @@ -1452,84 +2203,208 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven hotplug_cb->events = events; hotplug_cb->user_data = user_data; hotplug_cb->callback = callback; - - /* Ensure we are ready to actually use the mutex */ - hid_internal_hotplug_init(); - - /* Lock the mutex to avoid race itions */ - pthread_mutex_lock(&hid_hotplug_context.mutex); - - hotplug_cb->handle = hid_hotplug_context.next_handle++; - - /* handle the unlikely case of handle overflow */ - if (hid_hotplug_context.next_handle < 0) - { - hid_hotplug_context.next_handle = 1; + hotplug_cb->replay = NULL; + + /* Ensure we are ready to actually use the mutex, and lock it to avoid race conditions */ + hid_internal_hotplug_init_and_lock(); + + /* If a previous generation of the event threads is still winding down (the + * last callback was removed from the event thread itself, so its join had to + * be deferred), finish it before starting a new one */ + hid_internal_hotplug_settle_shutdown(); + + /* Registration implicitly initializes HIDAPI (as if by hid_init()); done + * under the mutex so concurrent registrations do not race in it. The + * application must serialize hid_exit() against registration. */ + if (!usb_context && hid_init() < 0) { + /* register_global_error: global error is already set by hid_init */ + free(hotplug_cb); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; } - /* Return allocated handle */ - if (callback_handle != NULL) { - *callback_handle = hotplug_cb->handle; - } - /* Append a new callback to the end */ - if (hid_hotplug_context.hotplug_cbs != NULL) { - struct hid_hotplug_callback *last = hid_hotplug_context.hotplug_cbs; - while (last->next != NULL) { - last = last->next; - } - last->next = hotplug_cb; + /* Handles are never reused, as a stale handle must not silently address a + * live callback. Refuse to register rather than to overflow (undefined) or + * to wrap around into the handles still in use. */ + if (hid_hotplug_context.next_handle == INT_MAX) { + register_string_error(&last_global_error, "No hotplug callback handles left"); + free(hotplug_cb); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; } - else { - /* Fill already connected devices so we can use this info in disconnection notification */ - if (libusb_init(&hid_hotplug_context.context)) { + + int is_first_callback = (hid_hotplug_context.hotplug_cbs == NULL); + + if (is_first_callback) { + int res = libusb_init(&hid_hotplug_context.context); + if (res) { + register_libusb_error(&last_global_error, res, "hotplug/libusb_init"); free(hotplug_cb); pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } - - hid_hotplug_context.devs = hid_enumerate(0, 0); - hid_hotplug_context.hotplug_cbs = hotplug_cb; - - /* Arm a global callback to receive ALL notifications for HID class devices */ - if (libusb_hotplug_register_callback(hid_hotplug_context.context, + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + hid_hotplug_context.cache_stale = 0; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + + /* Arm a global callback for ALL USB devices (HID is an interface class; + * hid_enumerate_from_libusb() filters supported interfaces on the callback + * thread) BEFORE taking the snapshot from this same context: libusb does + * not report the devices that are already connected when the listener is + * armed (LIBUSB_HOTPLUG_ENUMERATE is deliberately not used, the snapshot + * takes that role), so a device connecting the other way around - after + * the snapshot but before the listener - would be in neither, and its + * removal would later go unreported as well. The reverse order can only + * report a device twice, which the arrival path deduplicates against + * `devs` (see hid_internal_hotplug_is_known_device). */ + res = libusb_hotplug_register_callback(hid_hotplug_context.context, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, 0, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, &hid_libusb_hotplug_callback, NULL, - &hid_hotplug_context.callback_handle)) { - /* Major malfunction, failed to register a callback. - * No hotplug thread was started, so we must unwind `devs` ourselves - * (hid_internal_hotplug_cleanup() would try to join a non-existent thread). */ + &hid_hotplug_context.callback_handle); + if (res) { + /* Major malfunction, failed to register a callback */ + register_libusb_error(&last_global_error, res, "libusb_hotplug_register_callback"); libusb_exit(hid_hotplug_context.context); - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; + hid_hotplug_context.context = NULL; free(hotplug_cb); - hid_hotplug_context.hotplug_cbs = NULL; pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } - /* Initialization succeeded! We run the threads now */ - hidapi_thread_create(&hid_hotplug_context.libusb_thread, hotplug_thread, NULL); + /* Fill already connected devices so we can use this info in disconnection + * notification. An empty system is not a failure of the registration, but + * a failed enumeration is: silently caching an empty list would make every + * already-connected device invisible to this and all later callbacks. */ + int enumeration_failed = 0; + hid_internal_enumerate(hid_hotplug_context.context, 0, 0, &enumeration_failed, &hid_hotplug_context.devs); + if (enumeration_failed) { + /* register_global_error: global error is already set by hid_internal_enumerate */ + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + } + + if (hid_internal_hotplug_cache_stale()) { + int res = hid_internal_hotplug_reconcile(); + if (res < 0) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_libusb_error(&last_global_error, res, "hotplug/libusb_get_device_list"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } } - /* Mark the mutex as IN USE, to prevent callback removal from inside a callback */ - unsigned char old_state = hid_hotplug_context.mutex_in_use; - hid_hotplug_context.mutex_in_use = 1; - if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { - struct hid_device_info* device = hid_hotplug_context.devs; - /* Notify about already connected devices, if asked so */ - while (device != NULL) { - if (hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { - (*hotplug_cb->callback)(hotplug_cb->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, hotplug_cb->user_data); + /* Take a registration-time snapshot of the matching connected devices: + * it is replayed asynchronously on the event thread as synthetic arrival + * events, never from within this call (see hid_internal_flush_replay). + * All or nothing: a partially copied snapshot would silently hide a + * connected device from the callback forever. */ + struct hid_device_info *replay_tail = NULL; + for (struct hid_hotplug_device *dev = hid_hotplug_context.devs; dev != NULL; dev = dev->next) { + if (dev->removed_before) { + continue; } + for (struct hid_device_info *device = dev->info; device != NULL; device = device->next) { + struct hid_device_info *copy; + if (!hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { + continue; + } + copy = hid_internal_copy_device_info(device); + if (copy == NULL) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_string_error(&last_global_error, "Failed to allocate memory for the hotplug device snapshot"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + if (replay_tail != NULL) { + replay_tail->next = copy; + } + else { + hotplug_cb->replay = copy; + } + replay_tail = copy; + } + } + } + + int removals_pending = 0; + for (struct hid_hotplug_device *dev = hid_hotplug_context.devs; dev != NULL; dev = dev->next) { + if (dev->removed_before && dev->info != NULL) { + removals_pending = 1; + break; + } + } + if (hotplug_cb->replay != NULL || removals_pending || hid_internal_hotplug_cache_stale()) { + /* Wake the callback thread up so the snapshot is delivered promptly even + * with no live event traffic, and reconciled removals are dispatched. + * Enqueued (and, for the first callback, + * before the threads are even started) while holding the mutex the + * delivery needs, so the callback is always in the list by the time the + * marker is acted upon. Without the marker the snapshot would be stuck + * until the next live event, which may never come. */ + if (hid_internal_enqueue_hotplug_message(NULL, 0) != 0) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_string_error(&last_global_error, "Failed to allocate memory for a hotplug message"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + } + + if (is_first_callback) { + /* Initialization succeeded! We run the threads now. The callback thread + * is started here rather than from the libusb thread so that a failure to + * start it can be reported. Neither thread can deliver anything before we + * release the mutex. */ + if (hidapi_thread_create(&hid_hotplug_context.callback_thread, callback_thread, NULL) != 0) { + hid_internal_hotplug_unwind_registration(hotplug_cb, is_first_callback); + register_string_error(&last_global_error, "Failed to start the hotplug callback thread"); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + + 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); - device = device->next; + 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; } - hid_hotplug_context.mutex_in_use = old_state; + /* Commit the registration: from here on nothing can fail */ + hotplug_cb->handle = hid_hotplug_context.next_handle++; - hid_internal_hotplug_cleanup(); + /* 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 +2413,22 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) || !hid_hotplug_context.mutex_ready || callback_handle <= 0) { + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + register_string_error(&last_global_error, "Hotplug is not supported by this version of libusb"); return -1; } - pthread_mutex_lock(&hid_hotplug_context.mutex); + if (callback_handle <= 0) { + register_string_error(&last_global_error, "Invalid or unknown hotplug callback handle"); + return -1; + } - if (hid_hotplug_context.hotplug_cbs == NULL) { - pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* Fails only if the machinery was never initialized in this process. + * mutex_ready is a one-way latch: after hid_exit() the never-destroyed mutex + * is locked normally and the empty list below reports the stale handle. + * On success `mutex` is locked. */ + if (hid_internal_hotplug_lock() < 0) { + register_string_error(&last_global_error, "Invalid or unknown hotplug callback handle"); return -1; } @@ -1553,13 +2436,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 +2459,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 +2725,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 +2770,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 +2832,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); @@ -2102,6 +3024,61 @@ HID_API_EXPORT hid_device * HID_API_CALL hid_libusb_wrap_sys_device(intptr_t sys return NULL; } +void HID_API_EXPORT hid_libusb_set_write_timeout(hid_device *dev, unsigned int timeout) +{ + dev->write_timeout_ms = timeout; +} + +void HID_API_EXPORT hid_libusb_set_send_output_report_timeout(hid_device *dev, unsigned int timeout) +{ + dev->send_output_report_timeout_ms = timeout; +} + +void HID_API_EXPORT hid_libusb_set_send_feature_report_timeout(hid_device *dev, unsigned int timeout) +{ + dev->send_feature_report_timeout_ms = timeout; +} + +static int hidapi_internal_send_output_report(hid_device *dev, const unsigned char *data, size_t length, unsigned int timeout_ms) +{ + int res = -1; + int skipped_report_id = 0; + int report_number; + + if (!data || !length) { + register_string_error(&dev->error, "Zero buffer/length"); + return -1; + } + + register_libusb_error(&dev->error, LIBUSB_SUCCESS, NULL); + + report_number = data[0]; + + if (report_number == 0x0) { + data++; + length--; + skipped_report_id = 1; + } + + res = libusb_control_transfer(dev->device_handle, + LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE|LIBUSB_ENDPOINT_OUT, + 0x09/*HID set_report*/, + (2/*HID output*/ << 8) | report_number, + dev->interface, + (unsigned char *)data, length, + timeout_ms); + + if (res < 0) { + register_libusb_error(&dev->error, res, "hidapi_internal_send_output_report"); + return -1; + } + + /* Account for the report ID */ + if (skipped_report_id) + length++; + + return (int)length; +} int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) { @@ -2111,7 +3088,7 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t if (dev->output_endpoint <= 0) { /* No interrupt out endpoint. Use the Control Endpoint */ - return hid_send_output_report(dev, data, length); + return hidapi_internal_send_output_report(dev, data, length, dev->write_timeout_ms); } if (!data || !length) { @@ -2135,7 +3112,7 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t dev->output_endpoint, (unsigned char*)data, (int)length, - &actual_length, 1000); + &actual_length, dev->write_timeout_ms); if (res < 0) { register_libusb_error(&dev->error, res, "hid_write"); @@ -2320,7 +3297,7 @@ int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char (3/*HID feature*/ << 8) | report_number, dev->interface, (unsigned char *)data, length, - 1000/*timeout millis*/); + dev->send_feature_report_timeout_ms); if (res < 0) { register_libusb_error(&dev->error, res, "hid_send_feature_report"); @@ -2377,43 +3354,7 @@ int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, int HID_API_EXPORT hid_send_output_report(hid_device *dev, const unsigned char *data, size_t length) { - int res = -1; - int skipped_report_id = 0; - int report_number; - - if (!data || !length) { - register_string_error(&dev->error, "Zero buffer/length"); - return -1; - } - - register_libusb_error(&dev->error, LIBUSB_SUCCESS, NULL); - - report_number = data[0]; - - if (report_number == 0x0) { - data++; - length--; - skipped_report_id = 1; - } - - res = libusb_control_transfer(dev->device_handle, - LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE|LIBUSB_ENDPOINT_OUT, - 0x09/*HID set_report*/, - (2/*HID output*/ << 8) | report_number, - dev->interface, - (unsigned char *)data, length, - 1000/*timeout millis*/); - - if (res < 0) { - register_libusb_error(&dev->error, res, "hid_send_output_report"); - return -1; - } - - /* Account for the report ID */ - if (skipped_report_id) - length++; - - return (int)length; + return hidapi_internal_send_output_report(dev, data, length, dev->send_output_report_timeout_ms); } int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) @@ -2588,19 +3529,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 +3595,27 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) } +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + if (!dev) { + /* The global error state is shared by every application thread: this + * function frees and replaces its cached string, so without the lock two + * threads - one here, one in a failing API call - would double-free it. + * HIDAPI's own threads never write it (see hid_callback_thread_id); they + * only take this leaf lock briefly to check their identity, so holding + * it here cannot deadlock anything internal. */ + const wchar_t *res; + + pthread_mutex_lock(&hid_global_error_mutex); + res = hid_internal_error(&last_global_error); + pthread_mutex_unlock(&hid_global_error_mutex); + + return res; + } + + return hid_internal_error(&dev->error); +} + HID_API_EXPORT int HID_API_CALL hid_libusb_error(hid_device *dev) { if (!dev) { diff --git a/libusb/hidapi_libusb.h b/libusb/hidapi_libusb.h index 6e0ded4f7..cc14cba37 100644 --- a/libusb/hidapi_libusb.h +++ b/libusb/hidapi_libusb.h @@ -49,22 +49,80 @@ extern "C" { */ HID_API_EXPORT hid_device * HID_API_CALL hid_libusb_wrap_sys_device(intptr_t sys_dev, int interface_num); - /** @brief Similar to @ref hid_error but gives a libusb error code. + /** @brief Similar to @ref hid_error but gives a libusb error code. - Since version 0.15.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 15, 0) + Since version 0.15.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 15, 0) - If the error occurred is not immediately caused by a libusb function call, - the returned value is 1. @ref hid_error would still contain a valid and meaningful error message. + If the error occurred is not immediately caused by a libusb function call, + the returned value is 1. @ref hid_error would still contain a valid and meaningful error message. - @ingroup API - @param dev A device handle returned from hid_open(), - or NULL to get the last non-device-specific error - (e.g. for errors in hid_open() or hid_enumerate()). + With a NULL device, this function follows the same application + serialization rules as hid_error(NULL), including against hotplug registration/deregistration. + + @ingroup API + @param dev A device handle returned from hid_open(), + or NULL to get the last non-device-specific error + (e.g. for errors in hid_open() or hid_enumerate()). + + @returns + enum libusb_error value representing last error code. + */ + HID_API_EXPORT int HID_API_CALL hid_libusb_error(hid_device *dev); + + /** + * @brief Sets the timeout for hid_write operation. + * + * Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + * + * The default timeout is 1sec for libusb backend. + * + * In case if 1sec is not enough, on in case of multi-platform development, + * the recommended value is 5sec, e.g. to match (unconfigurable) 5sec timeout + * set for hidraw (linux kernel) implementation. + * + * When the timeout is set to 0, the function will not exit, + * until the write operation is performed or an error occurred. + * + * @param timeout New timeout value in milliseconds. + */ + HID_API_EXPORT void HID_API_CALL hid_libusb_set_write_timeout(hid_device *dev, unsigned int timeout); + + /** + * @brief Sets the timeout for hid_send_output_report operation. + * + * Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + * + * The default timeout is 1sec for libusb backend. + * + * In case if 1sec is not enough, on in case of multi-platform development, + * the recommended value is 5sec, e.g. to match (unconfigurable) 5sec timeout + * set for hidraw (linux kernel) implementation. + * + * When the timeout is set to 0, the function will not exit, + * until the write operation is performed or an error occurred. + * + * @param timeout New timeout value in milliseconds. + */ + HID_API_EXPORT void HID_API_CALL hid_libusb_set_send_output_report_timeout(hid_device *dev, unsigned int timeout); + + /** + * @brief Sets the timeout for hid_send_feature_report operation. + * + * Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + * + * The default timeout is 1sec for libusb backend. + * + * In case if 1sec is not enough, on in case of multi-platform development, + * the recommended value is 5sec, e.g. to match (unconfigurable) 5sec timeout + * set for hidraw (linux kernel) implementation. + * + * When the timeout is set to 0, the function will not exit, + * until the write operation is performed or an error occurred. + * + * @param timeout New timeout value in milliseconds. + */ + HID_API_EXPORT void HID_API_CALL hid_libusb_set_send_feature_report_timeout(hid_device *dev, unsigned int timeout); - @returns - enum libusb_error value representing last error code. - */ - HID_API_EXPORT int HID_API_CALL hid_libusb_error(hid_device *dev); #ifdef __cplusplus } 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/CMakeLists.txt b/linux/CMakeLists.txt index 07251a250..1c34e09d2 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -36,4 +36,8 @@ if(HIDAPI_INSTALL_TARGETS) ) endif() -hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-hidraw.pc.in") +hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-backend-specific.pc.in" + OUTPUT_NAME "hidapi-hidraw" + LIBRARY_NAME "hidapi-hidraw" + DESCRIPTION "C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the hidraw implementation." +) diff --git a/mac/CMakeLists.txt b/mac/CMakeLists.txt index 046a6eef4..473e98594 100644 --- a/mac/CMakeLists.txt +++ b/mac/CMakeLists.txt @@ -62,4 +62,8 @@ if(HIDAPI_INSTALL_TARGETS) ) endif() -hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi.pc.in") +hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-backend-specific.pc.in" + OUTPUT_NAME "hidapi-darwin" + LIBRARY_NAME "hidapi" + DESCRIPTION "C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the Darwin implementation." +) diff --git a/netbsd/CMakeLists.txt b/netbsd/CMakeLists.txt index e5688f316..6d6df8000 100644 --- a/netbsd/CMakeLists.txt +++ b/netbsd/CMakeLists.txt @@ -43,4 +43,8 @@ if(HIDAPI_INSTALL_TARGETS) ) endif() -hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-netbsd.pc.in") +hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-backend-specific.pc.in" + OUTPUT_NAME "hidapi-netbsd" + LIBRARY_NAME "hidapi-netbsd" + DESCRIPTION "C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the netbsd implementation." +) diff --git a/pc/.gitignore b/pc/.gitignore index 6fd0ef029..8fb4bc610 100644 --- a/pc/.gitignore +++ b/pc/.gitignore @@ -1 +1,2 @@ *.pc +*.pc.tmp diff --git a/pc/hidapi-backend-specific.pc.in b/pc/hidapi-backend-specific.pc.in new file mode 100644 index 000000000..ecf1b6e7c --- /dev/null +++ b/pc/hidapi-backend-specific.pc.in @@ -0,0 +1,11 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ + +Name: @HIDAPI_BACKEND_NAME@ +Description: @HIDAPI_BACKEND_DESCRIPTION@ +URL: https://github.com/libusb/hidapi +Version: @VERSION@ +Libs: -L${libdir} -l@HIDAPI_BACKEND_LIBRARY_NAME@ +Cflags: -I${includedir}/hidapi diff --git a/pc/hidapi-hidraw.pc.in b/pc/hidapi-hidraw.pc.in deleted file mode 100644 index 28b9fe681..000000000 --- a/pc/hidapi-hidraw.pc.in +++ /dev/null @@ -1,11 +0,0 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -includedir=@includedir@ - -Name: hidapi-hidraw -Description: C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the hidraw implementation. -URL: https://github.com/libusb/hidapi -Version: @VERSION@ -Libs: -L${libdir} -lhidapi-hidraw -Cflags: -I${includedir}/hidapi diff --git a/pc/hidapi-libusb.pc.in b/pc/hidapi-libusb.pc.in deleted file mode 100644 index d51e98f2b..000000000 --- a/pc/hidapi-libusb.pc.in +++ /dev/null @@ -1,11 +0,0 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -includedir=@includedir@ - -Name: hidapi-libusb -Description: C Library for USB HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the libusb implementation. -URL: https://github.com/libusb/hidapi -Version: @VERSION@ -Libs: -L${libdir} -lhidapi-libusb -Cflags: -I${includedir}/hidapi diff --git a/pc/hidapi-netbsd.pc.in b/pc/hidapi-netbsd.pc.in deleted file mode 100644 index fcb5ca215..000000000 --- a/pc/hidapi-netbsd.pc.in +++ /dev/null @@ -1,11 +0,0 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -includedir=@includedir@ - -Name: hidapi-netbsd -Description: C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the netbsd implementation. -URL: https://github.com/libusb/hidapi -Version: @VERSION@ -Libs: -L${libdir} -lhidapi-netbsd -Cflags: -I${includedir}/hidapi diff --git a/pc/hidapi.pc.in b/pc/hidapi.pc.in index 75b11a5dd..f9aaf82c1 100644 --- a/pc/hidapi.pc.in +++ b/pc/hidapi.pc.in @@ -7,5 +7,5 @@ Name: hidapi Description: C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. URL: https://github.com/libusb/hidapi Version: @VERSION@ -Libs: -L${libdir} -lhidapi +Libs: -L${libdir} -l@HIDAPI_LIB_NAME@ Cflags: -I${includedir}/hidapi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c5f4def97..fc804fd54 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,9 +55,16 @@ endif() # Helper(s) function(hidapi_configure_pc PC_IN_FILE) + set(oneValueArgs OUTPUT_NAME LIBRARY_NAME DESCRIPTION) + cmake_parse_arguments(HIDAPI_PC "" "${oneValueArgs}" "" ${ARGN}) file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pc") set(VERSION "${VERSION}${VERSION_SUFFIX}") + if(HIDAPI_PC_LIBRARY_NAME) + set(HIDAPI_LIB_NAME "${HIDAPI_PC_LIBRARY_NAME}") + else() + set(HIDAPI_LIB_NAME "hidapi") + endif() set(prefix "${CMAKE_INSTALL_PREFIX}") set(exec_prefix "\${prefix}") if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") @@ -71,7 +78,14 @@ function(hidapi_configure_pc PC_IN_FILE) set(includedir "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") endif() - get_filename_component(PC_IN_FILENAME "${PC_IN_FILE}" NAME_WE) + if(HIDAPI_PC_OUTPUT_NAME) + set(PC_IN_FILENAME "${HIDAPI_PC_OUTPUT_NAME}") + else() + get_filename_component(PC_IN_FILENAME "${PC_IN_FILE}" NAME_WE) + endif() + set(HIDAPI_BACKEND_NAME "${PC_IN_FILENAME}") + set(HIDAPI_BACKEND_DESCRIPTION "${HIDAPI_PC_DESCRIPTION}") + set(HIDAPI_BACKEND_LIBRARY_NAME "${HIDAPI_LIB_NAME}") set(PC_FILE "${CMAKE_CURRENT_BINARY_DIR}/pc/${PC_IN_FILENAME}.pc") configure_file("${PC_IN_FILE}" "${PC_FILE}" @ONLY) if(HIDAPI_INSTALL_TARGETS) @@ -187,6 +201,11 @@ endif() add_library(hidapi::hidapi ALIAS hidapi_${EXPORT_ALIAS}) +get_target_property(HIDAPI_LIB_NAME hidapi_${EXPORT_ALIAS} OUTPUT_NAME) +hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi.pc.in" + LIBRARY_NAME "${HIDAPI_LIB_NAME}" +) + if(HIDAPI_INSTALL_TARGETS) include(CMakePackageConfigHelpers) set(EXPORT_DENERATED_LOCATION "${CMAKE_BINARY_DIR}/export_generated") diff --git a/tests/pkg-config/Makefile.am b/tests/pkg-config/Makefile.am new file mode 100644 index 000000000..ca003c25f --- /dev/null +++ b/tests/pkg-config/Makefile.am @@ -0,0 +1,5 @@ +noinst_PROGRAMS = hidapi-pkg-config-test + +hidapi_pkg_config_test_SOURCES = test.c +hidapi_pkg_config_test_CPPFLAGS = $(HIDAPI_CFLAGS) +hidapi_pkg_config_test_LDADD = $(HIDAPI_LIBS) diff --git a/tests/pkg-config/configure.ac b/tests/pkg-config/configure.ac new file mode 100644 index 000000000..c3473011b --- /dev/null +++ b/tests/pkg-config/configure.ac @@ -0,0 +1,11 @@ +AC_PREREQ([2.69]) +AC_INIT([hidapi-pkg-config-test], [1.0]) +AC_CONFIG_SRCDIR([test.c]) +AM_INIT_AUTOMAKE([foreign]) + +AC_PROG_CC +PKG_PROG_PKG_CONFIG +PKG_CHECK_MODULES([HIDAPI], [hidapi]) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/tests/pkg-config/test.c b/tests/pkg-config/test.c new file mode 100644 index 000000000..d934ac0e6 --- /dev/null +++ b/tests/pkg-config/test.c @@ -0,0 +1,6 @@ +#include + +int main(void) +{ + return hid_version() ? 0 : 1; +} diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 005978366..d820f1ffb 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -61,7 +61,11 @@ if(HIDAPI_INSTALL_TARGETS) ) endif() -hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi.pc.in") +hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-backend-specific.pc.in" + OUTPUT_NAME "hidapi-winapi" + LIBRARY_NAME "hidapi" + DESCRIPTION "C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the WinAPI implementation." +) if(HIDAPI_WITH_TESTS) add_subdirectory(test)