From 1b0b6acce5505aaa66b550f648c7662a03a53f7e Mon Sep 17 00:00:00 2001 From: Dimitriy Ryazantcev Date: Mon, 1 May 2023 14:20:08 +0300 Subject: [PATCH 01/19] hotplug: Add ability to register device connection/disconnection callback (#299) - initial API; - Windows backend implementation; --- hidapi/hidapi.h | 106 ++++++++++++++ hidtest/test.c | 40 ++++++ libusb/hid.c | 22 +++ linux/hid.c | 22 +++ mac/hid.c | 22 +++ windows/hid.c | 283 +++++++++++++++++++++++++++++++++++++- windows/hidapi_cfgmgr32.h | 67 +++++++++ 7 files changed, 559 insertions(+), 3 deletions(-) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index 744ceb0b0..1e8b5f238 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -254,6 +254,112 @@ extern "C" { */ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs); + /** @brief Callback handle. + + Callbacks handles are generated by hid_hotplug_register_callback() + and can be used to deregister callbacks. Callback handles are unique + and it is safe to call hid_hotplug_deregister_callback() on + an already deregistered callback. + + @ingroup API + */ + typedef int hid_hotplug_callback_handle; + + /** + Hotplug events + + @ingroup API + */ + typedef enum { + /** A device has been plugged in and is ready to use */ + HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED = (1 << 0), + + /** A device has left and is no longer available. + It is the user's responsibility to call hid_close with a disconnected device. + */ + HID_API_HOTPLUG_EVENT_DEVICE_LEFT = (1 << 1) + } hid_hotplug_event; + + /** + Hotplug flags + + @ingroup API + */ + typedef enum { + /** Arm the callback and fire it for all matching currently attached devices. */ + HID_API_HOTPLUG_ENUMERATE = (1 << 0) + } hid_hotplug_flag; + + /** @brief Hotplug callback function type. When requesting hotplug event notifications, + you pass a pointer to a callback function of this type. + + This callback may be called by an internal event thread and as such it is + recommended the callback do minimal processing before returning. + + hidapi will call this function later, when a matching event had happened on + a matching device. + + Note that when callbacks are called from hid_hotplug_register_callback() + because of the \ref HID_API_HOTPLUG_ENUMERATE flag, the callback return + value is ignored. In other words, you cannot cause a callback to be + deregistered by returning 1 when it is called from hid_hotplug_register_callback(). + + @ingroup API + + @param callback_handle The hid_hotplug_callback_handle callback handle. + @param device The hid_device_info of device this event occurred on event that occurred. + @param event Event that occurred. + @param user_data User data provided when this callback was registered. + (Optionally NULL). + + @returns bool + Whether this callback is finished processing events. + Returning non-zero value will cause this callback to be deregistered. + */ + typedef int (HID_API_CALL *hid_hotplug_callback_fn)( + hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, + void *user_data); + + /** @brief Register a HID hotplug callback function. + + If @p vendor_id is set to 0 then any vendor matches. + If @p product_id is set to 0 then any product matches. + If @p vendor_id and @p product_id are both set to 0, then all HID devices will be notified. + + @ingroup API + + @param vendor_id The Vendor ID (VID) of the types of device to notify about. + @param product_id The Product ID (PID) of the types of device to notify about. + @param events Bitwise or of hotplug events that will trigger this callback. + See \ref hid_hotplug_event. + @param flags Bitwise or of hotplug flags that affect registration. + See \ref hid_hotplug_flag. + @param callback The callback function that will be called on device connection/disconnection. + See \ref hid_hotplug_callback_fn. + @param user_data The user data you wanted to provide to your callback function. + @param callback_handle Pointer to store the handle of the allocated callback + (Optionally NULL). + + @returns + This function returns 0 on success or -1 on error. + */ + 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); + + /** @brief Deregister a callback from a HID hotplug. + + This function is safe to call from within a hotplug callback. + + @ingroup API + + @param callback_handle The handle of the callback to deregister. + + @returns + This function returns 0 on success or -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle); + /** @brief Open a HID device using a Vendor ID (VID), Product ID (PID) and optionally a serial number. diff --git a/hidtest/test.c b/hidtest/test.c index 1b6819280..dd11ab9e0 100644 --- a/hidtest/test.c +++ b/hidtest/test.c @@ -109,6 +109,34 @@ void print_devices_with_descriptor(struct hid_device_info *cur_dev) { } } +int device_callback( + hid_hotplug_callback_handle callback_handle, + struct hid_device_info* device, + hid_hotplug_event event, + void* user_data) +{ + (void)user_data; + + if (event & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED) + printf("Handle %d: New device is connected: %s.\n", callback_handle, device->path); + else + printf("Handle %d: Device was disconnected: %s.\n", callback_handle, device->path); + + printf("type: %04hx %04hx\n serial_number: %ls", device->vendor_id, device->product_id, device->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", device->manufacturer_string); + printf(" Product: %ls\n", device->product_string); + printf(" Release: %hx\n", device->release_number); + printf(" Interface: %d\n", device->interface_number); + printf(" Usage (page): 0x%hx (0x%hx)\n", device->usage, device->usage_page); + printf("\n"); + + //if (device->product_id == 0x0ce6) + // return 1; + + return 0; +} + int main(int argc, char* argv[]) { (void)argc; @@ -120,6 +148,7 @@ int main(int argc, char* argv[]) wchar_t wstr[MAX_STR]; hid_device *handle; int i; + hid_hotplug_callback_handle token1, token2; struct hid_device_info *devs; @@ -144,6 +173,17 @@ int main(int argc, char* argv[]) print_devices_with_descriptor(devs); hid_free_enumeration(devs); + hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token1); + hid_hotplug_register_callback(0x054c, 0x0ce6, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token2); + + while (1) + { + + } + + hid_hotplug_deregister_callback(token2); + hid_hotplug_deregister_callback(token1); + // Set up the command buffer. memset(buf,0x00,sizeof(buf)); buf[0] = 0x01; diff --git a/libusb/hid.c b/libusb/hid.c index 188e536d5..057034088 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -863,6 +863,28 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } +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) +{ + /* Stub */ + (void)vendor_id; + (void)product_id; + (void)events; + (void)flags; + (void)callback; + (void)user_data; + (void)callback_handle; + + return -1; +} + +int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) +{ + /* Stub */ + (void)callback_handle; + + return -1; +} + hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { struct hid_device_info *devs, *cur_dev; diff --git a/linux/hid.c b/linux/hid.c index dda9f597a..5338d737e 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -942,6 +942,28 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } +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) +{ + /* Stub */ + (void)vendor_id; + (void)product_id; + (void)events; + (void)flags; + (void)callback; + (void)user_data; + (void)callback_handle; + + return -1; +} + +int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) +{ + /* Stub */ + (void)callback_handle; + + return -1; +} + hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { struct hid_device_info *devs, *cur_dev; diff --git a/mac/hid.c b/mac/hid.c index 39b3c56c0..cc4206141 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -726,6 +726,28 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } +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) +{ + /* Stub */ + (void)vendor_id; + (void)product_id; + (void)events; + (void)flags; + (void)callback; + (void)user_data; + (void)callback_handle; + + return -1; +} + +int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) +{ + /* Stub */ + (void)callback_handle; + + return -1; +} + hid_device * HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { /* This function is identical to the Linux version. Platform independent. */ diff --git a/windows/hid.c b/windows/hid.c index 5b12ad65e..4f251bdbe 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -101,6 +101,8 @@ static CM_Get_DevNode_PropertyW_ CM_Get_DevNode_PropertyW = NULL; static CM_Get_Device_Interface_PropertyW_ CM_Get_Device_Interface_PropertyW = NULL; static CM_Get_Device_Interface_List_SizeW_ CM_Get_Device_Interface_List_SizeW = NULL; static CM_Get_Device_Interface_ListW_ CM_Get_Device_Interface_ListW = NULL; +static CM_Register_Notification_ CM_Register_Notification = NULL; +static CM_Unregister_Notification_ CM_Unregister_Notification = NULL; static HMODULE hid_lib_handle = NULL; static HMODULE cfgmgr32_lib_handle = NULL; @@ -154,6 +156,8 @@ static int lookup_functions() RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_PropertyW); RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_List_SizeW); RESOLVE(cfgmgr32_lib_handle, CM_Get_Device_Interface_ListW); + RESOLVE(cfgmgr32_lib_handle, CM_Register_Notification); + RESOLVE(cfgmgr32_lib_handle, CM_Unregister_Notification); #undef RESOLVE #if defined(__GNUC__) @@ -590,6 +594,12 @@ static void hid_internal_get_ble_info(struct hid_device_info* dev, DEVINST dev_n } } +static int hid_internal_match_device_id(unsigned short vendor_id, unsigned short product_id, unsigned short expected_vendor_id, unsigned short expected_product_id) +{ + return (expected_vendor_id == 0x0 || vendor_id == expected_vendor_id) && (expected_product_id == 0x0 || product_id == expected_product_id); +} + + static void hid_internal_get_info(const wchar_t* interface_path, struct hid_device_info* dev) { wchar_t *device_id = NULL, *compatible_ids = NULL; @@ -825,9 +835,7 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor /* Check the VID/PID to see if we should add this device to the enumeration list. */ - if ((vendor_id == 0x0 || attrib.VendorID == vendor_id) && - (product_id == 0x0 || attrib.ProductID == product_id)) { - + if (hid_internal_match_device_id(attrib.VendorID, attrib.ProductID, vendor_id, product_id)) { /* VID/PID match. Create the record. */ struct hid_device_info *tmp = hid_internal_get_device_info(device_interface, device_handle); @@ -877,6 +885,275 @@ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *d } } +struct hid_hotplug_callback { + hid_hotplug_callback_handle handle; + unsigned short vendor_id; + unsigned short product_id; + hid_hotplug_event events; + void *user_data; + hid_hotplug_callback_fn callback; + + /* Pointer to the next notification */ + struct hid_hotplug_callback *next; +}; + +static struct hid_hotplug_context { + /* Win32 notification handle */ + HCMNOTIFICATION notify_handle; + + /* HIDAPI unique callback handle counter */ + hid_hotplug_callback_handle next_handle; + + /* Linked list of the hotplug callbacks */ + struct hid_hotplug_callback *hotplug_cbs; + + /* Linked list of the device infos (mandatory when the device is disconnected) */ + struct hid_device_info *devs; +} hid_hotplug_context = { + .notify_handle = NULL, + .next_handle = 1, + .hotplug_cbs = NULL, + .devs = NULL +}; + +DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, CM_NOTIFY_ACTION action, PCM_NOTIFY_EVENT_DATA event_data, DWORD event_data_size) +{ + struct hid_device_info* device = NULL; + hid_hotplug_event hotplug_event = 0; + + (void)notify; + (void)context; + (void)event_data_size; + + if (event_data == NULL || event_data->FilterType != CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE) { + return ERROR_SUCCESS; + } + + if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL) { + HANDLE read_handle; + + hotplug_event = HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED; + + /* Open read-only handle to the device */ + read_handle = open_device(event_data->u.DeviceInterface.SymbolicLink, FALSE); + + /* Check validity of read_handle. */ + if (read_handle == INVALID_HANDLE_VALUE) { + /* Unable to open the device. */ + return ERROR_SUCCESS; + } + + device = hid_internal_get_device_info(event_data->u.DeviceInterface.SymbolicLink, read_handle); + + /* Append to the end of the device list */ + if (hid_hotplug_context.devs != NULL) { + struct hid_device_info* last = hid_hotplug_context.devs; + while (last->next != NULL) { + last = last->next; + } + last->next = device; + } + else { + hid_hotplug_context.devs = device; + } + + CloseHandle(read_handle); + } + else if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL) { + char* path; + + hotplug_event = HID_API_HOTPLUG_EVENT_DEVICE_LEFT; + + path = hid_internal_UTF16toUTF8(event_data->u.DeviceInterface.SymbolicLink); + + if (path == NULL) { + return ERROR_SUCCESS; + } + + /* Get and remove this device from the device list */ + for (struct hid_device_info** current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { + /* Case-independent path comparison is mandatory */ + if (_stricmp((*current)->path, path) == 0) { + struct hid_device_info* next = (*current)->next; + device = *current; + *current = next; + break; + } + } + + free(path); + } + + if (device) { + /* Call the notifications for the device */ + struct hid_hotplug_callback *hotplug_cb = hid_hotplug_context.hotplug_cbs; + while (hotplug_cb != NULL) { + if ((hotplug_cb->events & hotplug_event) && + hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { + struct hid_hotplug_callback* cur_hotplug_cb = hotplug_cb; + hotplug_cb = cur_hotplug_cb->next; + + if ((*cur_hotplug_cb->callback)(cur_hotplug_cb->handle, device, hotplug_event, cur_hotplug_cb->user_data)) { + hid_hotplug_deregister_callback(cur_hotplug_cb->handle); + + /* Last callback was unregistered */ + if (hid_hotplug_context.hotplug_cbs == NULL) { + break; + } + } + } + else { + hotplug_cb = hotplug_cb->next; + } + } + + /* Free removed device */ + if (hotplug_event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT) { + free(device); + } + } + + return ERROR_SUCCESS; +} + +int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void* user_data, hid_hotplug_callback_handle* callback_handle) +{ + struct hid_hotplug_callback* hotplug_cb; + + /* Check params */ + if (events == 0 + || (events & ~(HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT)) + || (flags & ~(HID_API_HOTPLUG_ENUMERATE)) + || callback == NULL) { + return -1; + } + + hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); + + if (hotplug_cb == NULL) { + return -1; + } + + /* Fill out the record */ + hotplug_cb->next = NULL; + hotplug_cb->vendor_id = vendor_id; + hotplug_cb->product_id = product_id; + hotplug_cb->events = events; + hotplug_cb->user_data = user_data; + hotplug_cb->callback = callback; + + /* TODO: protect the handle by the context hotplug lock */ + 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; + } + + /* 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; + } + else { + GUID interface_class_guid; + CM_NOTIFY_FILTER notify_filter = { 0 }; + + /* Fill already connected devices so we can use this info in disconnection notification */ + hid_hotplug_context.devs = hid_enumerate(0, 0); + + hid_hotplug_context.hotplug_cbs = hotplug_cb; + + if (hid_hotplug_context.notify_handle != NULL) { + register_global_error(L"Device notification have already been registered"); + return -1; + } + + /* Retrieve HID Interface Class GUID + https://docs.microsoft.com/windows-hardware/drivers/install/guid-devinterface-hid */ + HidD_GetHidGuid(&interface_class_guid); + + notify_filter.cbSize = sizeof(notify_filter); + notify_filter.FilterType = CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE; + notify_filter.u.DeviceInterface.ClassGuid = interface_class_guid; + + /* Register for a HID device notification when adding the first callback */ + if (CM_Register_Notification(¬ify_filter, NULL, hid_internal_notify_callback, &hid_hotplug_context.notify_handle) != CR_SUCCESS) { + register_global_error(L"hid_hotplug_register_callback/CM_Register_Notification"); + return -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); + } + + device = device->next; + } + } + + return 0; +} + +int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) +{ + struct hid_hotplug_callback *hotplug_cb = NULL; + + if (callback_handle <= 0 || hid_hotplug_context.hotplug_cbs == NULL) { + return -1; + } + + /* Remove this notification */ + for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { + if ((*current)->handle == callback_handle) { + struct hid_hotplug_callback *next = (*current)->next; + hotplug_cb = *current; + *current = next; + break; + } + } + + if (hotplug_cb == NULL) { + return -1; + } + + free(hotplug_cb); + + /* Unregister a HID device connection notification when removing the last callback */ + if (hid_hotplug_context.hotplug_cbs == NULL) { + /* Cleanup connected device list */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + + if (hid_hotplug_context.notify_handle == NULL) { + register_global_error(L"Device notification have already been unregistered"); + return -1; + } + + if (CM_Unregister_Notification(hid_hotplug_context.notify_handle) != CR_SUCCESS) { + register_global_error(L"hid_hotplug_deregister_callback/CM_Unregister_Notification"); + return -1; + } + + hid_hotplug_context.notify_handle = NULL; + } + + return 0; +} + HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { /* TODO: Merge this functions with the Linux version. This function should be platform independent. */ diff --git a/windows/hidapi_cfgmgr32.h b/windows/hidapi_cfgmgr32.h index 638512a8b..460b2e583 100644 --- a/windows/hidapi_cfgmgr32.h +++ b/windows/hidapi_cfgmgr32.h @@ -57,6 +57,73 @@ typedef CONFIGRET(__stdcall* CM_Get_Device_Interface_PropertyW_)(LPCWSTR pszDevi typedef CONFIGRET(__stdcall* CM_Get_Device_Interface_List_SizeW_)(PULONG pulLen, LPGUID InterfaceClassGuid, DEVINSTID_W pDeviceID, ULONG ulFlags); typedef CONFIGRET(__stdcall* CM_Get_Device_Interface_ListW_)(LPGUID InterfaceClassGuid, DEVINSTID_W pDeviceID, PZZWSTR Buffer, ULONG BufferLen, ULONG ulFlags); +DECLARE_HANDLE(HCMNOTIFICATION); +typedef HCMNOTIFICATION* PHCMNOTIFICATION; + +typedef enum _CM_NOTIFY_FILTER_TYPE { + CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE = 0, + CM_NOTIFY_FILTER_TYPE_DEVICEHANDLE, + CM_NOTIFY_FILTER_TYPE_DEVICEINSTANCE, + CM_NOTIFY_FILTER_TYPE_MAX +} CM_NOTIFY_FILTER_TYPE, * PCM_NOTIFY_FILTER_TYPE; + +typedef struct _CM_NOTIFY_FILTER { + DWORD cbSize; + DWORD Flags; + CM_NOTIFY_FILTER_TYPE FilterType; + DWORD Reserved; + union { + struct { + GUID ClassGuid; + } DeviceInterface; + struct { + HANDLE hTarget; + } DeviceHandle; + struct { + WCHAR InstanceId[200]; + } DeviceInstance; + } u; +} CM_NOTIFY_FILTER, * PCM_NOTIFY_FILTER; + +typedef enum _CM_NOTIFY_ACTION { + CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL = 0, + CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL, + CM_NOTIFY_ACTION_DEVICEQUERYREMOVE, + CM_NOTIFY_ACTION_DEVICEQUERYREMOVEFAILED, + CM_NOTIFY_ACTION_DEVICEREMOVEPENDING, + CM_NOTIFY_ACTION_DEVICEREMOVECOMPLETE, + CM_NOTIFY_ACTION_DEVICECUSTOMEVENT, + CM_NOTIFY_ACTION_DEVICEINSTANCEENUMERATED, + CM_NOTIFY_ACTION_DEVICEINSTANCESTARTED, + CM_NOTIFY_ACTION_DEVICEINSTANCEREMOVED, + CM_NOTIFY_ACTION_MAX +} CM_NOTIFY_ACTION, * PCM_NOTIFY_ACTION; + +typedef struct _CM_NOTIFY_EVENT_DATA { + CM_NOTIFY_FILTER_TYPE FilterType; + DWORD Reserved; + union { + struct { + GUID ClassGuid; + WCHAR SymbolicLink[ANYSIZE_ARRAY]; + } DeviceInterface; + struct { + GUID EventGuid; + LONG NameOffset; + DWORD DataSize; + BYTE Data[ANYSIZE_ARRAY]; + } DeviceHandle; + struct { + WCHAR InstanceId[ANYSIZE_ARRAY]; + } DeviceInstance; + } u; +} CM_NOTIFY_EVENT_DATA, * PCM_NOTIFY_EVENT_DATA; + +typedef DWORD(CALLBACK* PCM_NOTIFY_CALLBACK)(HCMNOTIFICATION hNotify, PVOID Context, CM_NOTIFY_ACTION Action, PCM_NOTIFY_EVENT_DATA EventData, DWORD EventDataSize); + +typedef CONFIGRET(__stdcall* CM_Register_Notification_)(PCM_NOTIFY_FILTER pFilter, PVOID pContext, PCM_NOTIFY_CALLBACK pCallback, PHCMNOTIFICATION pNotifyContext); +typedef CONFIGRET(__stdcall* CM_Unregister_Notification_)(HCMNOTIFICATION NotifyContext); + // from devpkey.h DEFINE_DEVPROPKEY(DEVPKEY_NAME, 0xb725f130, 0x47ef, 0x101a, 0xa5, 0xf1, 0x02, 0x60, 0x8c, 0x9e, 0xeb, 0xac, 10); // DEVPROP_TYPE_STRING DEFINE_DEVPROPKEY(DEVPKEY_Device_Manufacturer, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 13); // DEVPROP_TYPE_STRING From ce923867993c55bc374ed99a4f384913af6495b4 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 6 Apr 2024 13:56:58 +0400 Subject: [PATCH 02/19] Windows hotplug: Mutex for callback actions (#646) --- hidtest/test.c | 41 ++++---- windows/hid.c | 256 ++++++++++++++++++++++++++++++------------------- 2 files changed, 181 insertions(+), 116 deletions(-) diff --git a/hidtest/test.c b/hidtest/test.c index ec67668f7..71c9b6dbd 100644 --- a/hidtest/test.c +++ b/hidtest/test.c @@ -127,31 +127,34 @@ void print_devices_with_descriptor(struct hid_device_info *cur_dev) { } int device_callback( - hid_hotplug_callback_handle callback_handle, - struct hid_device_info* device, - hid_hotplug_event event, - void* user_data) + hid_hotplug_callback_handle callback_handle, + struct hid_device_info* device, + hid_hotplug_event event, + void* user_data) { (void)user_data; - if (event & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED) - printf("Handle %d: New device is connected: %s.\n", callback_handle, device->path); - else - printf("Handle %d: Device was disconnected: %s.\n", callback_handle, device->path); + if (event & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED) + printf("Handle %d: New device is connected: %s.\n", callback_handle, device->path); + else + printf("Handle %d: Device was disconnected: %s.\n", callback_handle, device->path); - printf("type: %04hx %04hx\n serial_number: %ls", device->vendor_id, device->product_id, device->serial_number); - printf("\n"); - printf(" Manufacturer: %ls\n", device->manufacturer_string); - printf(" Product: %ls\n", device->product_string); - printf(" Release: %hx\n", device->release_number); - printf(" Interface: %d\n", device->interface_number); - printf(" Usage (page): 0x%hx (0x%hx)\n", device->usage, device->usage_page); - printf("\n"); + printf("type: %04hx %04hx\n serial_number: %ls", device->vendor_id, device->product_id, device->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", device->manufacturer_string); + printf(" Product: %ls\n", device->product_string); + printf(" Release: %hx\n", device->release_number); + printf(" Interface: %d\n", device->interface_number); + printf(" Usage (page): 0x%hx (0x%hx)\n", device->usage, device->usage_page); + printf("\n"); - //if (device->product_id == 0x0ce6) - // return 1; + /* Printed data might not show on the screen - force it out */ + fflush(stdout); - return 0; + //if (device->product_id == 0x0ce6) + // return 1; + + return 0; } int main(int argc, char* argv[]) diff --git a/windows/hid.c b/windows/hid.c index 0ae326f77..0a47ca663 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -48,6 +48,7 @@ typedef LONG NTSTATUS; #include #include #define _wcsdup wcsdup +#define _stricmp strcasecmp #endif /*#define HIDAPI_USE_DDK*/ @@ -78,6 +79,10 @@ typedef LONG NTSTATUS; #define MAX_STRING_WCHARS_USB 126 +/* The value of the first callback handle to be given upon registration */ +/* Can be any arbitrary positive integer */ +#define FIRST_HOTPLUG_CALLBACK_HANDLE 1 + static struct hid_api_version api_version = { .major = HID_API_VERSION_MAJOR, .minor = HID_API_VERSION_MINOR, @@ -183,19 +188,44 @@ static int lookup_functions() #endif /* HIDAPI_USE_DDK */ struct hid_device_ { - HANDLE device_handle; - BOOL blocking; - USHORT output_report_length; - unsigned char *write_buf; - size_t input_report_length; - USHORT feature_report_length; - unsigned char *feature_buf; - wchar_t *last_error_str; - BOOL read_pending; - char *read_buf; - OVERLAPPED ol; - OVERLAPPED write_ol; - struct hid_device_info* device_info; + HANDLE device_handle; + BOOL blocking; + USHORT output_report_length; + unsigned char *write_buf; + size_t input_report_length; + USHORT feature_report_length; + unsigned char *feature_buf; + wchar_t *last_error_str; + BOOL read_pending; + char *read_buf; + OVERLAPPED ol; + OVERLAPPED write_ol; + struct hid_device_info *device_info; +}; + +static struct hid_hotplug_context { + /* Win32 notification handle */ + HCMNOTIFICATION notify_handle; + + /* Critical section (faster mutex substitute), for both cached device list and callback list changes */ + CRITICAL_SECTION critical_section; + + BOOL critical_section_ready; + + /* HIDAPI unique callback handle counter */ + hid_hotplug_callback_handle next_handle; + + /* Linked list of the hotplug callbacks */ + struct hid_hotplug_callback *hotplug_cbs; + + /* Linked list of the device infos (mandatory when the device is disconnected) */ + struct hid_device_info *devs; +} hid_hotplug_context = { + .notify_handle = NULL, + .critical_section_ready = 0, + .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, + .hotplug_cbs = NULL, + .devs = NULL }; static hid_device *new_hid_device() @@ -367,9 +397,18 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) return HID_API_VERSION_STR; } +static void hid_internal_hotplug_init() +{ + if (!hid_hotplug_context.critical_section_ready) { + InitializeCriticalSection(&hid_hotplug_context.critical_section); + hid_hotplug_context.critical_section_ready = 1; + } +} + int HID_API_EXPORT hid_init(void) { register_global_error(NULL); + #ifndef HIDAPI_USE_DDK if (!hidapi_initialized) { if (lookup_functions() < 0) { @@ -379,9 +418,66 @@ int HID_API_EXPORT hid_init(void) hidapi_initialized = TRUE; } #endif + return 0; } +static void hid_internal_hotplug_cleanup() +{ + /* Unregister the HID device connection notification when removing the last callback */ + /* This function is always called inside a locked mutex */ + if (hid_hotplug_context.hotplug_cbs != NULL) { + return; + } + + if (hid_hotplug_context.devs) { + /* Cleanup connected device list */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + } + + if (hid_hotplug_context.notify_handle) { + if (CM_Unregister_Notification(hid_hotplug_context.notify_handle) != CR_SUCCESS) { + /* We mark an error, but we proceed with the cleanup */ + register_global_error(L"CM_Unregister_Notification failed for Hotplug notification"); + } + } + + hid_hotplug_context.notify_handle = NULL; +} + +struct hid_hotplug_callback { + hid_hotplug_callback_handle handle; + unsigned short vendor_id; + unsigned short product_id; + hid_hotplug_event events; + void *user_data; + hid_hotplug_callback_fn callback; + + /* Pointer to the next notification */ + struct hid_hotplug_callback *next; +}; + +static void hid_internal_hotplug_exit() +{ + if (!hid_hotplug_context.critical_section_ready) { + /* If the critical section is not initialized, we are safe to assume nothing else is */ + return; + } + EnterCriticalSection(&hid_hotplug_context.critical_section); + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + /* Remove all callbacks from the list */ + while (*current) { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + } + hid_internal_hotplug_cleanup(); + LeaveCriticalSection(&hid_hotplug_context.critical_section); + hid_hotplug_context.critical_section_ready = 0; + DeleteCriticalSection(&hid_hotplug_context.critical_section); +} + int HID_API_EXPORT hid_exit(void) { #ifndef HIDAPI_USE_DDK @@ -389,6 +485,9 @@ int HID_API_EXPORT hid_exit(void) hidapi_initialized = FALSE; #endif register_global_error(NULL); + + hid_internal_hotplug_exit(); + return 0; } @@ -930,40 +1029,9 @@ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *d } } -struct hid_hotplug_callback { - hid_hotplug_callback_handle handle; - unsigned short vendor_id; - unsigned short product_id; - hid_hotplug_event events; - void *user_data; - hid_hotplug_callback_fn callback; - - /* Pointer to the next notification */ - struct hid_hotplug_callback *next; -}; - -static struct hid_hotplug_context { - /* Win32 notification handle */ - HCMNOTIFICATION notify_handle; - - /* HIDAPI unique callback handle counter */ - hid_hotplug_callback_handle next_handle; - - /* Linked list of the hotplug callbacks */ - struct hid_hotplug_callback *hotplug_cbs; - - /* Linked list of the device infos (mandatory when the device is disconnected) */ - struct hid_device_info *devs; -} hid_hotplug_context = { - .notify_handle = NULL, - .next_handle = 1, - .hotplug_cbs = NULL, - .devs = NULL -}; - DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, CM_NOTIFY_ACTION action, PCM_NOTIFY_EVENT_DATA event_data, DWORD event_data_size) { - struct hid_device_info* device = NULL; + struct hid_device_info *device = NULL; hid_hotplug_event hotplug_event = 0; (void)notify; @@ -974,6 +1042,9 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, return ERROR_SUCCESS; } + /* Lock the mutex to avoid race conditions */ + EnterCriticalSection(&hid_hotplug_context.critical_section); + if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL) { HANDLE read_handle; @@ -992,20 +1063,18 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, /* Append to the end of the device list */ if (hid_hotplug_context.devs != NULL) { - struct hid_device_info* last = hid_hotplug_context.devs; + struct hid_device_info *last = hid_hotplug_context.devs; while (last->next != NULL) { last = last->next; } last->next = device; - } - else { + } else { hid_hotplug_context.devs = device; } CloseHandle(read_handle); - } - else if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL) { - char* path; + } else if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL) { + char *path; hotplug_event = HID_API_HOTPLUG_EVENT_DEVICE_LEFT; @@ -1016,10 +1085,10 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, } /* Get and remove this device from the device list */ - for (struct hid_device_info** current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { + for (struct hid_device_info **current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { /* Case-independent path comparison is mandatory */ if (_stricmp((*current)->path, path) == 0) { - struct hid_device_info* next = (*current)->next; + struct hid_device_info *next = (*current)->next; device = *current; *current = next; break; @@ -1031,33 +1100,32 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, if (device) { /* Call the notifications for the device */ - struct hid_hotplug_callback *hotplug_cb = hid_hotplug_context.hotplug_cbs; - while (hotplug_cb != NULL) { - if ((hotplug_cb->events & hotplug_event) && - hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { - struct hid_hotplug_callback* cur_hotplug_cb = hotplug_cb; - hotplug_cb = cur_hotplug_cb->next; - - if ((*cur_hotplug_cb->callback)(cur_hotplug_cb->handle, device, hotplug_event, cur_hotplug_cb->user_data)) { - hid_hotplug_deregister_callback(cur_hotplug_cb->handle); - - /* Last callback was unregistered */ - if (hid_hotplug_context.hotplug_cbs == NULL) { - break; - } + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current) { + struct hid_hotplug_callback *callback = *current; + if ((callback->events & hotplug_event) && hid_internal_match_device_id(device->vendor_id, device->product_id, callback->vendor_id, callback->product_id)) { + int result = (callback->callback)(callback->handle, device, hotplug_event, callback->user_data); + /* If the result is non-zero, we remove the callback and proceed */ + /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + if (result) { + *current = (*current)->next; + free(callback); + continue; } } - else { - hotplug_cb = hotplug_cb->next; - } + current = &callback->next; } /* Free removed device */ if (hotplug_event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT) { free(device); } + + hid_internal_hotplug_cleanup(); } + LeaveCriticalSection(&hid_hotplug_context.critical_section); + return ERROR_SUCCESS; } @@ -1087,7 +1155,12 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven hotplug_cb->user_data = user_data; hotplug_cb->callback = callback; - /* TODO: protect the handle by the context hotplug lock */ + /* Ensure we are ready to actually use the mutex */ + hid_internal_hotplug_init(); + + /* Lock the mutex to avoid race conditions */ + EnterCriticalSection(&hid_hotplug_context.critical_section); + hotplug_cb->handle = hid_hotplug_context.next_handle++; /* handle the unlikely case of handle overflow */ @@ -1120,6 +1193,7 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven if (hid_hotplug_context.notify_handle != NULL) { register_global_error(L"Device notification have already been registered"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); return -1; } @@ -1134,6 +1208,7 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven /* Register for a HID device notification when adding the first callback */ if (CM_Register_Notification(¬ify_filter, NULL, hid_internal_notify_callback, &hid_hotplug_context.notify_handle) != CR_SUCCESS) { register_global_error(L"hid_hotplug_register_callback/CM_Register_Notification"); + LeaveCriticalSection(&hid_hotplug_context.critical_section); return -1; } } @@ -1150,14 +1225,22 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven } } + LeaveCriticalSection(&hid_hotplug_context.critical_section); + return 0; } int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - struct hid_hotplug_callback *hotplug_cb = NULL; + if (callback_handle <= 0 || !hid_hotplug_context.critical_section_ready) { + return -1; + } - if (callback_handle <= 0 || hid_hotplug_context.hotplug_cbs == NULL) { + /* Lock the mutex to avoid race conditions */ + EnterCriticalSection(&hid_hotplug_context.critical_section); + + if (!hid_hotplug_context.hotplug_cbs) { + LeaveCriticalSection(&hid_hotplug_context.critical_section); return -1; } @@ -1165,36 +1248,15 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_call for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { if ((*current)->handle == callback_handle) { struct hid_hotplug_callback *next = (*current)->next; - hotplug_cb = *current; + free(*current); *current = next; break; } } - if (hotplug_cb == NULL) { - return -1; - } - - free(hotplug_cb); - - /* Unregister a HID device connection notification when removing the last callback */ - if (hid_hotplug_context.hotplug_cbs == NULL) { - /* Cleanup connected device list */ - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; + hid_internal_hotplug_cleanup(); - if (hid_hotplug_context.notify_handle == NULL) { - register_global_error(L"Device notification have already been unregistered"); - return -1; - } - - if (CM_Unregister_Notification(hid_hotplug_context.notify_handle) != CR_SUCCESS) { - register_global_error(L"hid_hotplug_deregister_callback/CM_Unregister_Notification"); - return -1; - } - - hid_hotplug_context.notify_handle = NULL; - } + LeaveCriticalSection(&hid_hotplug_context.critical_section); return 0; } From c3a2775f9df3123a7f3aa267dac3fca084106600 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 6 Apr 2024 13:59:53 +0400 Subject: [PATCH 03/19] Connection callback: add stubs for netbsd (#668) * netbsd hotplug stubs * Make cygwin happy (fix copied from libusb) --- netbsd/hid.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/netbsd/hid.c b/netbsd/hid.c index a9b841f81..bd7e75f38 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -742,6 +742,28 @@ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *de } } +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) +{ + /* Stub */ + (void)vendor_id; + (void)product_id; + (void)events; + (void)flags; + (void)callback; + (void)user_data; + (void)callback_handle; + + return -1; +} + +int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) +{ + /* Stub */ + (void)callback_handle; + + return -1; +} + HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) { struct hid_device_info *devs; From 60b40d9423c8d486fc3fad116dcc67082897a8ab Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 6 Apr 2024 14:17:26 +0400 Subject: [PATCH 04/19] Linux Hotplug: connection-callback implementation for libusb backend (#645) --- libusb/hid.c | 646 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 536 insertions(+), 110 deletions(-) diff --git a/libusb/hid.c b/libusb/hid.c index 99d9ea1d6..22e1234fe 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -55,6 +55,10 @@ #endif #include HIDAPI_THREAD_MODEL_INCLUDE +/* The value of the first callback handle to be given upon registration */ +/* Can be any arbitrary positive integer */ +#define FIRST_HOTPLUG_CALLBACK_HANDLE 1 + #ifdef __cplusplus extern "C" { #endif @@ -133,6 +137,51 @@ static struct hid_api_version api_version = { static libusb_context *usb_context = NULL; +struct hid_hotplug_queue { + libusb_device* device; + int event; /* Arrived or removed */ + struct hid_hotplug_queue* next; +}; + +static struct hid_hotplug_context { + /* A separate libusb context for hotplug events: helps avoid mutual blocking with read_thread's */ + libusb_context * context; + + /* libusb callback handle */ + libusb_hotplug_callback_handle callback_handle; + + /* HIDAPI unique callback handle counter */ + hid_hotplug_callback_handle next_handle; + + /* A thread that fills the event queue */ + hidapi_thread_state libusb_thread; + + /* A separate thread which processes hidapi's internal event queue */ + hidapi_thread_state callback_thread; + + /* This mutex prevents changes to the callback list */ + pthread_mutex_t cb_mutex; + + int mutex_ready; + + int thread_running; + + struct hid_hotplug_queue* queue; + + /* Linked list of the hotplug callbacks */ + struct hid_hotplug_callback *hotplug_cbs; + + /* Linked list of the device infos (mandatory when the device is disconnected) */ + struct hid_device_info *devs; +} hid_hotplug_context = { + .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, + .mutex_ready = 0, + .thread_running = 0, + .queue = NULL, + .hotplug_cbs = NULL, + .devs = NULL, +}; + uint16_t get_usb_code_for_current_locale(void); static int return_data(hid_device *dev, unsigned char *data, size_t length); @@ -480,6 +529,75 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) return HID_API_VERSION_STR; } +struct hid_hotplug_callback +{ + unsigned short vendor_id; + unsigned short product_id; + hid_hotplug_callback_fn callback; + void* user_data; + int events; + struct hid_hotplug_callback* next; + + hid_hotplug_callback_handle handle; +}; + +static void hid_internal_hotplug_cleanup() +{ + if (hid_hotplug_context.hotplug_cbs != NULL) { + return; + } + + /* Mark the threads as stopped */ + hid_hotplug_context.thread_running = 0; + + /* Forcibly wake up the thread so it can shut down immediately */ + hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + + /* Wait for both threads to stop */ + hidapi_thread_join(&hid_hotplug_context.libusb_thread); + hidapi_thread_join(&hid_hotplug_context.callback_thread); + + /* Cleanup connected device list */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + /* Disarm the libusb listener */ + libusb_hotplug_deregister_callback(usb_context, hid_hotplug_context.callback_handle); + libusb_exit(hid_hotplug_context.context); +} + +static void hid_internal_hotplug_init() +{ + if (!hid_hotplug_context.mutex_ready) { + hidapi_thread_state_init(&hid_hotplug_context.libusb_thread); + hidapi_thread_state_init(&hid_hotplug_context.callback_thread); + pthread_mutex_init(&hid_hotplug_context.cb_mutex, NULL); + hid_hotplug_context.mutex_ready = 1; + } +} + +static void hid_internal_hotplug_exit() +{ + if (!hid_hotplug_context.mutex_ready) { + return; + } + + pthread_mutex_lock(&hid_hotplug_context.cb_mutex); + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + /* Remove all callbacks from the list */ + while (*current) { + struct hid_hotplug_callback* next = (*current)->next; + free(*current); + *current = next; + } + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + hid_hotplug_context.mutex_ready = 0; + pthread_mutex_destroy(&hid_hotplug_context.cb_mutex); + + hidapi_thread_state_destroy(&hid_hotplug_context.callback_thread); + hidapi_thread_state_destroy(&hid_hotplug_context.libusb_thread); +} + int HID_API_EXPORT hid_init(void) { if (!usb_context) { @@ -503,11 +621,17 @@ int HID_API_EXPORT hid_exit(void) if (usb_context) { libusb_exit(usb_context); usb_context = NULL; + hid_internal_hotplug_exit(); } return 0; } +static int hid_internal_match_device_id(unsigned short vendor_id, unsigned short product_id, unsigned short expected_vendor_id, unsigned short expected_product_id) +{ + return (expected_vendor_id == 0x0 || vendor_id == expected_vendor_id) && (expected_product_id == 0x0 || product_id == expected_product_id); +} + static int hid_get_report_descriptor_libusb(libusb_device_handle *handle, int interface_num, uint16_t expected_report_descriptor_size, unsigned char *buf, size_t buf_size) { unsigned char tmp[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; @@ -784,112 +908,134 @@ static int should_enumerate_interface(unsigned short vendor_id, const struct lib return 0; } -struct hid_device_info HID_API_EXPORT *hid_enumerate(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) { - libusb_device **devs; - libusb_device *dev; - libusb_device_handle *handle = NULL; - ssize_t num_devs; - int i = 0; - struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; + struct libusb_device_descriptor desc; + struct libusb_config_descriptor *conf_desc = NULL; + libusb_device_handle *handle = NULL; + int j, k; - if(hid_init() < 0) - return NULL; - - num_devs = libusb_get_device_list(usb_context, &devs); - if (num_devs < 0) + int res = libusb_get_device_descriptor(dev, &desc); + if (res < 0) return NULL; - while ((dev = devs[i++]) != NULL) { - struct libusb_device_descriptor desc; - struct libusb_config_descriptor *conf_desc = NULL; - int j, k; - int res = libusb_get_device_descriptor(dev, &desc); - if (res < 0) - continue; + unsigned short dev_vid = desc.idVendor; + unsigned short dev_pid = desc.idProduct; - unsigned short dev_vid = desc.idVendor; - unsigned short dev_pid = desc.idProduct; + if ((vendor_id != 0x0 && vendor_id != dev_vid) || + (product_id != 0x0 && product_id != dev_pid)) { + return NULL; + } - if ((vendor_id != 0x0 && vendor_id != dev_vid) || - (product_id != 0x0 && product_id != dev_pid)) { - continue; - } + res = libusb_get_active_config_descriptor(dev, &conf_desc); + if (res < 0) + libusb_get_config_descriptor(dev, 0, &conf_desc); + if (conf_desc) { + for (j = 0; j < conf_desc->bNumInterfaces; j++) { + const struct libusb_interface *intf = &conf_desc->interface[j]; + for (k = 0; k < intf->num_altsetting; k++) { + const struct libusb_interface_descriptor *intf_desc; + intf_desc = &intf->altsetting[k]; + if (should_enumerate_interface(dev_vid, intf_desc)) { + struct hid_device_info *tmp; - res = libusb_get_active_config_descriptor(dev, &conf_desc); - if (res < 0) - libusb_get_config_descriptor(dev, 0, &conf_desc); - if (conf_desc) { - for (j = 0; j < conf_desc->bNumInterfaces; j++) { - const struct libusb_interface *intf = &conf_desc->interface[j]; - for (k = 0; k < intf->num_altsetting; k++) { - const struct libusb_interface_descriptor *intf_desc; - intf_desc = &intf->altsetting[k]; - if (should_enumerate_interface(dev_vid, intf_desc)) { - struct hid_device_info *tmp; - - res = libusb_open(dev, &handle); + res = libusb_open(dev, &handle); #ifdef __ANDROID__ - if (handle) { - /* There is (a potential) libusb Android backend, in which - device descriptor is not accurate up until the device is opened. - https://github.com/libusb/libusb/pull/874#discussion_r632801373 - A workaround is to re-read the descriptor again. - Even if it is not going to be accepted into libusb master, - having it here won't do any harm, since reading the device descriptor - is as cheap as copy 18 bytes of data. */ - libusb_get_device_descriptor(dev, &desc); - } + if (handle) { + /* There is (a potential) libusb Android backend, in which + device descriptor is not accurate up until the device is opened. + https://github.com/libusb/libusb/pull/874#discussion_r632801373 + A workaround is to re-read the descriptor again. + Even if it is not going to be accepted into libusb master, + having it here won't do any harm, since reading the device descriptor + is as cheap as copy 18 bytes of data. */ + libusb_get_device_descriptor(dev, &desc); + } #endif - tmp = create_device_info_for_device(dev, handle, &desc, conf_desc->bConfigurationValue, intf_desc->bInterfaceNumber); - if (tmp) { + tmp = create_device_info_for_device(dev, handle, &desc, conf_desc->bConfigurationValue, intf_desc->bInterfaceNumber); + if (tmp) { #ifdef INVASIVE_GET_USAGE - /* TODO: have a runtime check for this section. */ - - /* - This section is removed because it is too - invasive on the system. Getting a Usage Page - and Usage requires parsing the HID Report - descriptor. Getting a HID Report descriptor - involves claiming the interface. Claiming the - interface involves detaching the kernel driver. - Detaching the kernel driver is hard on the system - because it will unclaim interfaces (if another - app has them claimed) and the re-attachment of - the driver will sometimes change /dev entry names. - It is for these reasons that this section is - optional. For composite devices, use the interface - field in the hid_device_info struct to distinguish - between interfaces. */ - if (handle) { - uint16_t report_descriptor_size = get_report_descriptor_size_from_interface_descriptors(intf_desc); - - invasive_fill_device_info_usage(tmp, handle, intf_desc->bInterfaceNumber, report_descriptor_size); - } -#endif /* INVASIVE_GET_USAGE */ + /* TODO: have a runtime check for this section. */ + + /* + This section is removed because it is too + invasive on the system. Getting a Usage Page + and Usage requires parsing the HID Report + descriptor. Getting a HID Report descriptor + involves claiming the interface. Claiming the + interface involves detaching the kernel driver. + Detaching the kernel driver is hard on the system + because it will unclaim interfaces (if another + app has them claimed) and the re-attachment of + the driver will sometimes change /dev entry names. + It is for these reasons that this section is + optional. For composite devices, use the interface + field in the hid_device_info struct to distinguish + between interfaces. */ + if (handle) { + uint16_t report_descriptor_size = get_report_descriptor_size_from_interface_descriptors(intf_desc); - if (cur_dev) { - cur_dev->next = tmp; - } - else { - root = tmp; - } - cur_dev = tmp; + invasive_fill_device_info_usage(tmp, handle, intf_desc->bInterfaceNumber, report_descriptor_size); } +#endif /* INVASIVE_GET_USAGE */ - if (res >= 0) { - libusb_close(handle); - handle = NULL; + if (cur_dev) { + cur_dev->next = tmp; } - break; + else { + root = tmp; + } + cur_dev = tmp; + } + + if (res >= 0) { + libusb_close(handle); + handle = NULL; } - } /* altsettings */ - } /* interfaces */ - libusb_free_config_descriptor(conf_desc); + break; + } + } /* altsettings */ + } /* interfaces */ + libusb_free_config_descriptor(conf_desc); + } + return root; +} + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + libusb_device **devs; + libusb_device *dev; + ssize_t num_devs; + int i = 0; + + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + + if (hid_init() < 0) + return NULL; + + num_devs = libusb_get_device_list(usb_context, &devs); + if (num_devs < 0) + return NULL; + + while ((dev = devs[i++]) != NULL) { + struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + cur_dev = tmp; + } + /* Traverse to the end of newly attached tail */ + if (cur_dev) { + while (cur_dev->next) { + cur_dev = cur_dev->next; + } } } @@ -912,26 +1058,306 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } -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) +static int match_libusb_to_info(libusb_device *device, struct hid_device_info* info) +{ + /* make a path from this libusb device, but leave the last 2 fields as 0 */ + char pseudo_path[64]; + 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); +} + +static void hid_internal_invoke_callbacks(struct hid_device_info* info, hid_hotplug_event event) +{ + pthread_mutex_lock(&hid_hotplug_context.cb_mutex); + + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current) { + struct hid_hotplug_callback *callback = *current; + 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 remove the callback and proceed */ + /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + if (result) { + struct hid_hotplug_callback *callback = *current; + *current = (*current)->next; + free(callback); + continue; + } + } + current = &callback->next; + } + pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); +} + +static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void * user_data) { - /* Stub */ - (void)vendor_id; - (void)product_id; - (void)events; - (void)flags; - (void)callback; + (void)ctx; (void)user_data; - (void)callback_handle; - return -1; + /* Make sure we HOLD the device until we are done with it - otherwise libusb would delete it the moment we exit this function */ + libusb_ref_device(device); + + struct hid_hotplug_queue* msg = calloc(1, sizeof(struct hid_hotplug_queue)); + if (NULL == msg) { + return 0; + } + + msg->device = device; + msg->event = event; + msg->next = NULL; + + /* We use this thread's mutex to protect the queue */ + hidapi_thread_mutex_lock(&hid_hotplug_context.libusb_thread); + struct hid_hotplug_queue* end = hid_hotplug_context.queue; + if (end) { + while (end->next) { + end = end->next; + } + end->next = msg; + } else { + hid_hotplug_context.queue = msg; + } + hidapi_thread_mutex_unlock(&hid_hotplug_context.libusb_thread); + + /* Wake up the other thread so it can react to the new message immediately */ + hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + + return 0; +} + +static void* hotplug_thread(void* user_data) +{ + (void) user_data; + + /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ + /* This timeout only affects how much time it takes to stop the thread */ + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 5000; + + while (hid_hotplug_context.thread_running) { + /* 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); + } + return NULL; +} + +static void process_hotplug_event(struct hid_hotplug_queue* msg) +{ + 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; + } + last->next = info; + } + else { + hid_hotplug_context.devs = 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 */ + free(info); + } else { + current = &info->next; + } + } + } + + /* Release the libusb device - we are done with it */ + libusb_unref_device(msg->device); + + /* Clean up if the last callback was removed */ + pthread_mutex_lock(&hid_hotplug_context.cb_mutex); + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); +} + +static void* callback_thread(void* user_data) +{ + (void) user_data; + + /* 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 */ + hidapi_timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 5000000; + + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); + while (hid_hotplug_context.thread_running) { + /* We use this thread's mutex to protect the queue */ + hidapi_thread_mutex_lock(&hid_hotplug_context.libusb_thread); + while (hid_hotplug_context.queue) { + process_hotplug_event(hid_hotplug_context.queue); + + /* Empty the queue */ + hid_hotplug_context.queue = hid_hotplug_context.queue->next; + } + hidapi_thread_mutex_unlock(&hid_hotplug_context.libusb_thread); + + /* Make the tread fall asleep and wait for a condition to wake it up */ + hidapi_thread_cond_timedwait(&hid_hotplug_context.callback_thread, &ts); + } + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + + return NULL; +} + +int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle) +{ + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + 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) { + return -1; + } + + struct hid_hotplug_callback* hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); + + if (hotplug_cb == NULL) { + return -1; + } + + /* Fill out the record */ + hotplug_cb->next = NULL; + hotplug_cb->vendor_id = vendor_id; + hotplug_cb->product_id = product_id; + hotplug_cb->events = events; + hotplug_cb->user_data = user_data; + hotplug_cb->callback = callback; + + /* 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.cb_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; + } + + /* 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; + } + else { + /* Fill already connected devices so we can use this info in disconnection notification */ + if (libusb_init(&hid_hotplug_context.context)) { + free(hotplug_cb); + pthread_mutex_unlock(&hid_hotplug_context.cb_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, + 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 */ + libusb_exit(hid_hotplug_context.context); + free(hotplug_cb); + hid_hotplug_context.hotplug_cbs = NULL; + pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + return -1; + } + + /* Initialization succeeded! We run the threads now */ + hid_hotplug_context.thread_running = 1; + hidapi_thread_create(&hid_hotplug_context.libusb_thread, hotplug_thread, NULL); + hidapi_thread_create(&hid_hotplug_context.callback_thread, callback_thread, NULL); + } + + 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); + } + + device = device->next; + } + } + + pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + + return 0; } int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - /* Stub */ - (void)callback_handle; + struct hid_hotplug_callback *hotplug_cb = NULL; + + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) || !hid_hotplug_context.mutex_ready || callback_handle <= 0) { + return -1; + } + + pthread_mutex_lock(&hid_hotplug_context.cb_mutex); - return -1; + if (hid_hotplug_context.hotplug_cbs == NULL) { + pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + return -1; + } + + /* Remove this notification */ + for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { + if ((*current)->handle == callback_handle) { + struct hid_hotplug_callback *next = (*current)->next; + hotplug_cb = *current; + *current = next; + free(hotplug_cb); + break; + } + } + + hid_internal_hotplug_cleanup(); + + pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + + return 0; } hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) @@ -1060,10 +1486,10 @@ static void *read_thread(void *param) /* Make the first submission. Further submissions are made from inside read_callback() */ res = libusb_submit_transfer(dev->transfer); - if(res < 0) { - LOG("libusb_submit_transfer failed: %d %s. Stopping read_thread from running\n", res, libusb_error_name(res)); - dev->shutdown_thread = 1; - dev->transfer_loop_finished = 1; + if (res < 0) { + LOG("libusb_submit_transfer failed: %d %s. Stopping read_thread from running\n", res, libusb_error_name(res)); + dev->shutdown_thread = 1; + dev->transfer_loop_finished = 1; } /* Notify the main thread that the read thread is up and running. */ @@ -1125,7 +1551,7 @@ static void init_xbox360(libusb_device_handle *device_handle, unsigned short idV /* The HORIPAD FPS for Nintendo Switch requires this to enable input reports. This VID/PID is also shared with other HORI controllers, but they all seem to be fine with this as well. - */ + */ memset(data, 0, sizeof(data)); libusb_control_transfer(device_handle, 0xC1, 0x01, 0x100, 0x0, data, sizeof(data), 100); } @@ -1151,7 +1577,7 @@ static void init_xboxone(libusb_device_handle *device_handle, unsigned short idV /* Newer Microsoft Xbox One controllers have a high speed alternate setting */ if (idVendor == vendor_microsoft && - intf_desc->bInterfaceNumber == 0 && intf_desc->bAlternateSetting == 1) { + intf_desc->bInterfaceNumber == 0 && intf_desc->bAlternateSetting == 1) { bSetAlternateSetting = 1; } else if (intf_desc->bInterfaceNumber != 0 && intf_desc->bAlternateSetting == 0) { bSetAlternateSetting = 1; @@ -1250,13 +1676,13 @@ static int hidapi_initialize_device(hid_device *dev, const struct libusb_interfa endpoint. */ int is_interrupt = (ep->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) - == LIBUSB_TRANSFER_TYPE_INTERRUPT; + == LIBUSB_TRANSFER_TYPE_INTERRUPT; int is_output = (ep->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK) - == LIBUSB_ENDPOINT_OUT; + == LIBUSB_ENDPOINT_OUT; int is_input = (ep->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK) - == LIBUSB_ENDPOINT_IN; + == LIBUSB_ENDPOINT_IN; /* Decide whether to use it for input or output. */ if (dev->input_endpoint == 0 && @@ -1290,7 +1716,7 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) int d = 0; int good_open = 0; - if(hid_init() < 0) + if (hid_init() < 0) return NULL; dev = new_hid_device(); @@ -1361,7 +1787,7 @@ HID_API_EXPORT hid_device * HID_API_CALL hid_libusb_wrap_sys_device(intptr_t sys int res = 0; int j = 0, k = 0; - if(hid_init() < 0) + if (hid_init() < 0) return NULL; dev = new_hid_device(); From 4537833f809e022fa9c0ba68f442fbc7279e5386 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 6 Apr 2024 14:19:13 +0400 Subject: [PATCH 05/19] Linux Hotplug: Connection-callback implementation for hidraw (#647) --- linux/hid.c | 326 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 313 insertions(+), 13 deletions(-) diff --git a/linux/hid.c b/linux/hid.c index 1eaaa112a..06b32a60b 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -35,6 +35,7 @@ #include #include #include +#include /* Linux */ #include @@ -68,6 +69,10 @@ #define HIDIOCGINPUT(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x0A, len) #endif +/* The value of the first callback handle to be given upon registration */ +/* Can be any arbitrary positive integer */ +#define FIRST_HOTPLUG_CALLBACK_HANDLE 1 + struct hid_device_ { int device_handle; int blocking; @@ -880,6 +885,96 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) return HID_API_VERSION_STR; } +static struct hid_hotplug_context { + /* UDEV context that handles the monitor */ + struct udev* udev_ctx; + + /* UDEV monitor that receives events */ + struct udev_monitor* mon; + + /* File descriptor for the UDEV monitor that allows to check for new events with select() */ + int monitor_fd; + + /* Thread for the UDEV monitor */ + pthread_t thread; + + pthread_mutex_t mutex; + + int mutex_ready; + + /* HIDAPI unique callback handle counter */ + hid_hotplug_callback_handle next_handle; + + /* Linked list of the hotplug callbacks */ + struct hid_hotplug_callback *hotplug_cbs; + + /* Linked list of the device infos (mandatory when the device is disconnected) */ + struct hid_device_info *devs; +} hid_hotplug_context = { + .udev_ctx = NULL, + .monitor_fd = -1, + .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, + .mutex_ready = 0, + .hotplug_cbs = NULL, + .devs = NULL +}; + +struct hid_hotplug_callback { + hid_hotplug_callback_handle handle; + unsigned short vendor_id; + unsigned short product_id; + hid_hotplug_event events; + void *user_data; + hid_hotplug_callback_fn callback; + + /* Pointer to the next notification */ + struct hid_hotplug_callback *next; +}; + +static void hid_internal_hotplug_cleanup() +{ + if (hid_hotplug_context.hotplug_cbs != NULL) { + return; + } + + pthread_join(hid_hotplug_context.thread, NULL); + + /* Cleanup connected device list */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + /* Disarm the udev monitor */ + udev_monitor_unref(hid_hotplug_context.mon); + udev_unref(hid_hotplug_context.udev_ctx); +} + +static void hid_internal_hotplug_init() +{ + if (!hid_hotplug_context.mutex_ready) { + pthread_mutex_init(&hid_hotplug_context.mutex, NULL); + hid_hotplug_context.mutex_ready = 1; + } +} + +static void hid_internal_hotplug_exit() +{ + if (!hid_hotplug_context.mutex_ready) { + return; + } + + pthread_mutex_lock(&hid_hotplug_context.mutex); + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + /* Remove all callbacks from the list */ + while (*current) { + struct hid_hotplug_callback* next = (*current)->next; + free(*current); + *current = next; + } + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + hid_hotplug_context.mutex_ready = 0; + pthread_mutex_destroy(&hid_hotplug_context.mutex); +} + int HID_API_EXPORT hid_init(void) { const char *locale; @@ -895,14 +990,22 @@ int HID_API_EXPORT hid_init(void) return 0; } + int HID_API_EXPORT hid_exit(void) { /* Free global error message */ register_global_error(NULL); + hid_internal_hotplug_exit(); + return 0; } +static int hid_internal_match_device_id(unsigned short vendor_id, unsigned short product_id, unsigned short expected_vendor_id, unsigned short expected_product_id) +{ + return (expected_vendor_id == 0x0 || vendor_id == expected_vendor_id) && (expected_product_id == 0x0 || product_id == expected_product_id); +} + struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) { struct udev *udev; @@ -1004,26 +1107,224 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } +static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event) +{ + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current) { + struct hid_hotplug_callback *callback = *current; + 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 remove the callback and proceed */ + /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + if (result) { + struct hid_hotplug_callback *callback = *current; + *current = (*current)->next; + free(callback); + continue; + } + } + current = &callback->next; + } +} + +static int match_udev_to_info(struct udev_device* raw_dev, struct hid_device_info *info) +{ + const char *path = udev_device_get_devnode(raw_dev); + if (!strcmp(path, info->path)) { + return 1; + } + return 0; +} + +static void* hotplug_thread(void* user_data) +{ + (void) user_data; + + while (hid_hotplug_context.monitor_fd > 0) { + fd_set fds; + struct timeval tv; + int ret; + + FD_ZERO(&fds); + FD_SET(hid_hotplug_context.monitor_fd, &fds); + /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ + /* This timeout only affects how much time it takes to stop the thread */ + tv.tv_sec = 0; + tv.tv_usec = 5000; + + ret = select(hid_hotplug_context.monitor_fd+1, &fds, NULL, NULL, &tv); + + /* Check if our file descriptor has received data. */ + if (ret > 0 && FD_ISSET(hid_hotplug_context.monitor_fd, &fds)) { + + /* Make the call to receive the device. + select() ensured that this will not block. */ + struct udev_device *raw_dev = udev_monitor_receive_device(hid_hotplug_context.mon); + if (raw_dev) { + /* Lock the mutex so callback/device lists don't change elsewhere from here on */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + const char* action = udev_device_get_action(raw_dev); + if (!strcmp(action, "add")) { + // We create a list of all usages on this UDEV device + struct hid_device_info *info = create_device_info_for_device(raw_dev); + struct hid_device_info *info_cur = info; + while (info_cur) { + /* For each device, call all matching callbacks */ + /* TODO: possibly make the `next` field NULL to match the behavior on other systems */ + hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); + info_cur = info_cur->next; + } + + /* Append all we got to the end of the device list */ + if (info) { + if (hid_hotplug_context.devs != NULL) { + struct hid_device_info *last = hid_hotplug_context.devs; + while (last->next != NULL) { + last = last->next; + } + last->next = info; + } else { + hid_hotplug_context.devs = info; + } + } + } else if (!strcmp(action, "remove")) { + for (struct hid_device_info **current = &hid_hotplug_context.devs; *current;) { + struct hid_device_info* info = *current; + if (match_udev_to_info(raw_dev, *current)) { + /* If the libusb device that's left matches this HID device, we detach it from the list */ + *current = (*current)->next; + info->next = NULL; + hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); + /* Free every removed device */ + free(info); + } else { + current = &info->next; + } + } + } + udev_device_unref(raw_dev); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + } + } + } + return NULL; +} + int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle) { - /* Stub */ - (void)vendor_id; - (void)product_id; - (void)events; - (void)flags; - (void)callback; - (void)user_data; - (void)callback_handle; + struct hid_hotplug_callback* hotplug_cb; - 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) { + return -1; + } + + hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); + + if (hotplug_cb == NULL) { + return -1; + } + + /* Fill out the record */ + hotplug_cb->next = NULL; + hotplug_cb->vendor_id = vendor_id; + hotplug_cb->product_id = product_id; + hotplug_cb->events = events; + hotplug_cb->user_data = user_data; + hotplug_cb->callback = callback; + + /* Ensure we are ready to actually use the mutex */ + hid_internal_hotplug_init(); + + /* Lock the mutex to avoid race conditions */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + 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; + } + + /* 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; + } + else { + // Prepare a UDEV context to run monitoring on + hid_hotplug_context.udev_ctx = udev_new(); + if(!hid_hotplug_context.udev_ctx) + { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + + hid_hotplug_context.mon = udev_monitor_new_from_netlink(hid_hotplug_context.udev_ctx, "udev"); + udev_monitor_filter_add_match_subsystem_devtype(hid_hotplug_context.mon, "hidraw", NULL); + udev_monitor_enable_receiving(hid_hotplug_context.mon); + hid_hotplug_context.monitor_fd = udev_monitor_get_fd(hid_hotplug_context.mon); + + /* After monitoring is all set up, enumerate all devices */ + hid_hotplug_context.devs = hid_enumerate(0, 0); + + /* Don't forget to actually register the callback */ + hid_hotplug_context.hotplug_cbs = hotplug_cb; + + /* Start the thread that will be doing the event scanning */ + pthread_create(&hid_hotplug_context.thread, NULL, &hotplug_thread, NULL); + } + + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + return 0; } int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - /* Stub */ - (void)callback_handle; + if (!hid_hotplug_context.mutex_ready) { + return -1; + } - return -1; + pthread_mutex_lock(&hid_hotplug_context.mutex); + + if (hid_hotplug_context.hotplug_cbs == NULL) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + + int result = -1; + + /* Remove this notification */ + for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { + if ((*current)->handle == callback_handle) { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + result = 0; + break; + } + } + + hid_internal_hotplug_cleanup(); + + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + return result; } hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) @@ -1191,7 +1492,6 @@ int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) return 0; /* Success */ } - int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) { int res; From 4f73d1f5e9fa4aed68e6193189f2fa895c8e1930 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 6 Apr 2024 14:25:09 +0400 Subject: [PATCH 06/19] Hotplug implementation for MacOS (HidManager approach) (#653) --- hidtest/test.c | 3 - mac/hid.c | 430 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 416 insertions(+), 17 deletions(-) diff --git a/hidtest/test.c b/hidtest/test.c index 71c9b6dbd..9fbf2321b 100644 --- a/hidtest/test.c +++ b/hidtest/test.c @@ -151,9 +151,6 @@ int device_callback( /* Printed data might not show on the screen - force it out */ fflush(stdout); - //if (device->product_id == 0x0ce6) - // return 1; - return 0; } diff --git a/mac/hid.c b/mac/hid.c index eca5d6bc4..75038b471 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -38,6 +38,10 @@ #include "hidapi_darwin.h" +/* The value of the first callback handle to be given upon registration */ +/* Can be any arbitrary positive integer */ +#define FIRST_HOTPLUG_CALLBACK_HANDLE 1 + /* Barrier implementation because Mac OSX doesn't have pthread_barrier. It also doesn't have clock_gettime(). So much for POSIX and SUSv2. This implementation came from Brent Priddy and was posted on @@ -457,6 +461,111 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) return HID_API_VERSION_STR; } +struct hid_hotplug_callback { + hid_hotplug_callback_handle handle; + unsigned short vendor_id; + unsigned short product_id; + hid_hotplug_event events; + void *user_data; + hid_hotplug_callback_fn callback; + + /* Pointer to the next notification */ + struct hid_hotplug_callback *next; +}; + +/* When a HID device is removed, we are no longer able to generate a path for it, but we can still match io_service_t */ +struct hid_device_info_ex +{ + struct hid_device_info info; + io_service_t service; +}; + +static struct hid_hotplug_context { + /* MacOS specific notification handles */ + IOHIDManagerRef manager; + + /* Thread and RunLoop for the manager to work in */ + pthread_t thread; + CFRunLoopRef run_loop; + CFRunLoopSourceRef source; + CFStringRef run_loop_mode; + pthread_barrier_t startup_barrier; /* Ensures correct startup sequence */ + int thread_state; + + /* HIDAPI unique callback handle counter */ + hid_hotplug_callback_handle next_handle; + + pthread_mutex_t mutex; + + int mutex_ready; + + /* Linked list of the hotplug callbacks */ + struct hid_hotplug_callback *hotplug_cbs; + + /* Linked list of the device infos (mandatory when the device is disconnected) */ + struct hid_device_info *devs; +} hid_hotplug_context = { + .manager = NULL, + .run_loop = NULL, + .run_loop_mode = NULL, + .source = NULL, + .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, + .mutex_ready = 0, + .thread_state = 0, /* 0 = starting (events ignored), 1 = running (events processed), 2 = shutting down */ + .hotplug_cbs = NULL, + .devs = NULL +}; + +static void hid_internal_hotplug_cleanup() +{ + if (hid_hotplug_context.hotplug_cbs != NULL) { + return; + } + + /* Cleanup connected device list */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + + /* Cause hotplug_thread() to stop. */ + hid_hotplug_context.thread_state = 2; + + /* Wake up the run thread's event loop so that the thread can exit. */ + CFRunLoopSourceSignal(hid_hotplug_context.source); + CFRunLoopWakeUp(hid_hotplug_context.run_loop); + + /* Wait for read_thread() to end. */ + pthread_join(hid_hotplug_context.thread, NULL); +} + +static void hid_internal_hotplug_init() +{ + if (!hid_hotplug_context.mutex_ready) { + pthread_mutex_init(&hid_hotplug_context.mutex, NULL); + hid_hotplug_context.mutex_ready = 1; + } +} + +static void hid_internal_hotplug_exit() +{ + if (!hid_hotplug_context.mutex_ready) { + return; + } + + pthread_mutex_lock(&hid_hotplug_context.mutex); + struct hid_hotplug_callback** current = &hid_hotplug_context.hotplug_cbs; + + /* Remove all callbacks from the list */ + while (*current) { + struct hid_hotplug_callback* next = (*current)->next; + free(*current); + *current = next; + } + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + hid_hotplug_context.mutex_ready = 0; + pthread_mutex_destroy(&hid_hotplug_context.mutex); +} + /* Initialize the IOHIDManager if necessary. This is the public function, and it is safe to call this function repeatedly. Return 0 for success and -1 for failure. */ @@ -467,9 +576,9 @@ int HID_API_EXPORT hid_init(void) if (!hid_mgr) { is_macos_10_10_or_greater = (kCFCoreFoundationVersionNumber >= 1151.16); /* kCFCoreFoundationVersionNumber10_10 */ hid_darwin_set_open_exclusive(1); /* Backward compatibility */ + return init_hid_manager(); } - /* Already initialized. */ return 0; } @@ -481,6 +590,7 @@ int HID_API_EXPORT hid_exit(void) IOHIDManagerClose(hid_mgr, kIOHIDOptionsTypeNone); CFRelease(hid_mgr); hid_mgr = NULL; + hid_internal_hotplug_exit(); } /* Free global error message */ @@ -489,6 +599,11 @@ int HID_API_EXPORT hid_exit(void) return 0; } +static int hid_internal_match_device_id(unsigned short vendor_id, unsigned short product_id, unsigned short expected_vendor_id, unsigned short expected_product_id) +{ + return (expected_vendor_id == 0x0 || vendor_id == expected_vendor_id) && (expected_product_id == 0x0 || product_id == expected_product_id); +} + static void process_pending_events(void) { SInt32 res; do { @@ -544,6 +659,7 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, wchar_t buf[BUF_LEN]; CFTypeRef transport_prop; + struct hid_device_info_ex *cur_dev_ex; struct hid_device_info *cur_dev; io_service_t hid_service; kern_return_t res; @@ -553,7 +669,9 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, return NULL; } - cur_dev = (struct hid_device_info *)calloc(1, sizeof(struct hid_device_info)); + /* A small trick to store an io_service_t tag along with hid_device_info for matching info with unplugged device */ + cur_dev_ex = (struct hid_device_info_ex *)calloc(1, sizeof(struct hid_device_info_ex)); + cur_dev = &(cur_dev_ex->info); if (cur_dev == NULL) { return NULL; } @@ -572,6 +690,7 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, hid_service = IOHIDDeviceGetService(dev); if (hid_service != MACH_PORT_NULL) { res = IORegistryEntryGetRegistryEntryID(hid_service, &entry_id); + cur_dev_ex->service = hid_service; } else { res = KERN_INVALID_ARGUMENT; @@ -804,26 +923,309 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) } } +static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event) +{ + struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; + while (*current) { + struct hid_hotplug_callback *callback = *current; + 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 remove the callback and proceed */ + /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + if (result) { + struct hid_hotplug_callback *callback = *current; + *current = (*current)->next; + free(callback); + continue; + } + } + current = &callback->next; + } +} + +static void hid_internal_hotplug_connect_callback(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + (void) context; + (void) result; + (void) sender; + + struct hid_device_info* info = create_device_info(device); + if(!info) { + return; + } + struct hid_device_info* info_cur = info; + + /* NOTE: we don't call any callbacks and we don't lock the mutex during initialization: the mutex is held by the main thread, but it's waiting by a barrier*/ + if (hid_hotplug_context.thread_state > 0) + { + /* Lock the mutex to avoid race conditions */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + /* Invoke all callbacks */ + while(info_cur) + { + hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); + info_cur = info_cur->next; + } + } + + /* Append all we got to the end of the device list */ + if (info) { + if (hid_hotplug_context.devs != NULL) { + struct hid_device_info* last = hid_hotplug_context.devs; + while (last->next != NULL) { + last = last->next; + } + last->next = info; + } + else { + hid_hotplug_context.devs = info; + } + } + + if (hid_hotplug_context.thread_state > 0) + { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + } +} + +int match_ref_to_info(IOHIDDeviceRef device, struct hid_device_info *info) +{ + if (!device || !info) { + return 0; + } + + struct hid_device_info_ex* ex = (struct hid_device_info_ex*)info; + io_service_t service = IOHIDDeviceGetService(device); + + return (service == ex->service); +} + +static void hid_internal_hotplug_disconnect_callback(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + (void) context; + (void) result; + (void) sender; + + /* NOTE: we don't call any callbacks and we don't lock the mutex during initialization: the mutex is held by the main thread, but it's waiting by a barrier*/ + if (hid_hotplug_context.thread_state > 0) + { + pthread_mutex_lock(&hid_hotplug_context.mutex); + } + + for (struct hid_device_info **current = &hid_hotplug_context.devs; *current;) { + struct hid_device_info* info = *current; + if (match_ref_to_info(device, *current)) { + /* If the IOHIDDeviceRef 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 */ + free(info); + } else { + current = &info->next; + } + } + + if (hid_hotplug_context.thread_state > 0) + { + /* Clean up if the last callback was removed */ + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); + } +} + +static void hotplug_stop_callback(void *context) +{ + hid_device *dev = (hid_device*) context; + CFRunLoopStop(dev->run_loop); /*TODO: CFRunLoopGetCurrent()*/ +} + +static void* hotplug_thread(void* user_data) +{ + (void) user_data; + + hid_hotplug_context.thread_state = 0; + hid_hotplug_context.manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + + if(!hid_hotplug_context.run_loop_mode) { + const char *str = "HIDAPI_hotplug"; + hid_hotplug_context.run_loop_mode = CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); + } + + /* Ensure the manager runs in this thread */ + IOHIDManagerScheduleWithRunLoop(hid_hotplug_context.manager, CFRunLoopGetCurrent(), hid_hotplug_context.run_loop_mode); + /* Store a reference to this runloop if we ever need to stop it - e.g. if we have no callbacks left or hid_exit was called */ + hid_hotplug_context.run_loop = CFRunLoopGetCurrent(); + + /* Create the RunLoopSource which is used to signal the + event loop to stop when hid_close() is called. */ + CFRunLoopSourceContext ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.version = 0; + ctx.perform = &hotplug_stop_callback; + hid_hotplug_context.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0/*order*/, &ctx); + CFRunLoopAddSource(hid_hotplug_context.run_loop, hid_hotplug_context.source, hid_hotplug_context.run_loop_mode); + + /* Set the manager to receive events for ALL HID devices */ + IOHIDManagerSetDeviceMatching(hid_hotplug_context.manager, NULL); + + /* Install callbacks */ + IOHIDManagerRegisterDeviceMatchingCallback(hid_hotplug_context.manager, + hid_internal_hotplug_connect_callback, + NULL); + + IOHIDManagerRegisterDeviceRemovalCallback(hid_hotplug_context.manager, + hid_internal_hotplug_disconnect_callback, + NULL); + + /* After monitoring is all set up, enumerate all devices */ + /* Opening the manager should result in the internal callback being called for all connected devices */ + IOHIDManagerOpen(hid_hotplug_context.manager, kIOHIDOptionsTypeNone); + + /* TODO: We need to flush all events from the runloop to ensure the already connected devices don't send any unwanted events */ + process_pending_events(); + + /* Now that all events are flushed, we are ready to notify the main thread that we are ready */ + hid_hotplug_context.thread_state = 1; + pthread_barrier_wait(&hid_hotplug_context.startup_barrier); + + while (hid_hotplug_context.thread_state != 2) { + int code = CFRunLoopRunInMode(hid_hotplug_context.run_loop_mode, 1000/*sec*/, FALSE); + /* If runloop stopped for whatever reason, exit the thread */ + if (code != kCFRunLoopRunTimedOut && + code != kCFRunLoopRunHandledSource) { + hid_hotplug_context.thread_state = 2; + break; + } + } + + /* Kill the manager */ + IOHIDManagerClose(hid_hotplug_context.manager, kIOHIDOptionsTypeNone); + + IOHIDManagerUnscheduleFromRunLoop(hid_hotplug_context.manager, hid_hotplug_context.run_loop, hid_hotplug_context.run_loop_mode); + + CFRelease(hid_hotplug_context.manager); + hid_hotplug_context.manager = NULL; + + return NULL; +} + int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle) { - /* Stub */ - (void)vendor_id; - (void)product_id; - (void)events; - (void)flags; - (void)callback; - (void)user_data; - (void)callback_handle; + struct hid_hotplug_callback* hotplug_cb; - 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) { + return -1; + } + + hotplug_cb = (struct hid_hotplug_callback*)calloc(1, sizeof(struct hid_hotplug_callback)); + + + if (hotplug_cb == NULL) { + return -1; + } + + /* Fill out the record */ + hotplug_cb->next = NULL; + hotplug_cb->vendor_id = vendor_id; + hotplug_cb->product_id = product_id; + hotplug_cb->events = events; + hotplug_cb->user_data = user_data; + hotplug_cb->callback = callback; + + /* Ensure we are ready to actually use the mutex */ + hid_internal_hotplug_init(); + + /* Lock the mutex to avoid race conditions */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + + 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; + } + + /* 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; + } + else { + pthread_barrier_init(&hid_hotplug_context.startup_barrier, NULL, 2); + pthread_create(&hid_hotplug_context.thread, NULL, hotplug_thread, NULL); + + /* Wait for the thread to finish setting up - without it the callback may be registered too early*/ + + pthread_barrier_wait(&hid_hotplug_context.startup_barrier); + + /* Don't forget to actually register the callback */ + hid_hotplug_context.hotplug_cbs = hotplug_cb; + } + + 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); + } + + device = device->next; + } + } + + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + return 0; } int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - /* Stub */ - (void)callback_handle; + if (!hid_hotplug_context.mutex_ready) { + return -1; + } - return -1; + pthread_mutex_lock(&hid_hotplug_context.mutex); + + if (hid_hotplug_context.hotplug_cbs == NULL) { + pthread_mutex_unlock(&hid_hotplug_context.mutex); + return -1; + } + + int result = -1; + + /* Remove this notification */ + for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { + if ((*current)->handle == callback_handle) { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + result = 0; + break; + } + } + + hid_internal_hotplug_cleanup(); + + pthread_mutex_unlock(&hid_hotplug_context.mutex); + + return result; } hid_device * HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) From b6606ca241e2e500638669141e21249865f8cc4a Mon Sep 17 00:00:00 2001 From: d3xMachina <16732772+d3xMachina@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:34:19 +0000 Subject: [PATCH 07/19] Connection callback: fix LeaveCriticalSection not being called after EnterCriticalSection (#689) --- windows/hid.c | 57 +++++++++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/windows/hid.c b/windows/hid.c index 0a47ca663..f849c3038 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1054,25 +1054,22 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, read_handle = open_device(event_data->u.DeviceInterface.SymbolicLink, FALSE); /* Check validity of read_handle. */ - if (read_handle == INVALID_HANDLE_VALUE) { - /* Unable to open the device. */ - return ERROR_SUCCESS; - } - - device = hid_internal_get_device_info(event_data->u.DeviceInterface.SymbolicLink, read_handle); - - /* Append to the end of the device list */ - if (hid_hotplug_context.devs != NULL) { - struct hid_device_info *last = hid_hotplug_context.devs; - while (last->next != NULL) { - last = last->next; + if (read_handle != INVALID_HANDLE_VALUE) { + device = hid_internal_get_device_info(event_data->u.DeviceInterface.SymbolicLink, read_handle); + + /* Append to the end of the device list */ + if (hid_hotplug_context.devs != NULL) { + struct hid_device_info *last = hid_hotplug_context.devs; + while (last->next != NULL) { + last = last->next; + } + last->next = device; + } else { + hid_hotplug_context.devs = device; } - last->next = device; - } else { - hid_hotplug_context.devs = device; - } - CloseHandle(read_handle); + CloseHandle(read_handle); + } } else if (action == CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL) { char *path; @@ -1080,22 +1077,20 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, path = hid_internal_UTF16toUTF8(event_data->u.DeviceInterface.SymbolicLink); - if (path == NULL) { - return ERROR_SUCCESS; - } - - /* Get and remove this device from the device list */ - for (struct hid_device_info **current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { - /* Case-independent path comparison is mandatory */ - if (_stricmp((*current)->path, path) == 0) { - struct hid_device_info *next = (*current)->next; - device = *current; - *current = next; - break; + if (path != NULL) { + /* Get and remove this device from the device list */ + for (struct hid_device_info **current = &hid_hotplug_context.devs; *current; current = &(*current)->next) { + /* Case-independent path comparison is mandatory */ + if (_stricmp((*current)->path, path) == 0) { + struct hid_device_info *next = (*current)->next; + device = *current; + *current = next; + break; + } } - } - free(path); + free(path); + } } if (device) { From da500c6ccdf0c6498b8aecd30517a53df3533c54 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 6 Sep 2024 13:00:57 +0400 Subject: [PATCH 08/19] Fix possible deadlock in Connection-callback feature (#676) Fix the possible issues that happen when a register or de-register call is made from inside a callback. The way it works is the same for all platforms and is described below. Resolves: #673 1) The mutex is made recursive, so it can be locked by the same thread multiple times 2) `mutex_ready` kept intact, added 2 more flags, `mutex_in_use` and `cb_list_dirty`. 3) When the `mitex_in_use` flag is set, the Deregister call is no longer allowed to immediately remove any callbacks from the list. Instead, the `events` field in the callback is set to 0, which makes it "invalid", as it will no longer match any events, and the `cb_list_dirty` flag is set to 1 to indicate that the list needs to be checked for invalid events later. 4) When a callback returns a non-zero value, indicating that the callback is to be disarmed and removed from the list, it is marked in the same manner until the processing finishes (unless the callback was called directly by the Register call, in which case it's return value is ignored on purpose) 5) After all the callbacks are processed, if `cb_list_dirty` flag is set, the list of callbacks is checked for any callbacks marked for removal (`events` field set to 0), and those are only removed after all the processing is finished. 6) The Register call is allowed to register callbacks, as it causes no issues so long as the mutex it locks is recursive 7) Since the Register call can also call the new callback if `HID_API_HOTPLUG_ENUMERATE` is specified, `mutex_in_use` flag is set to prevent callback removal in that new callback. 8) The return value of any callbacks called for pre-existing devices is still ignored as per documentation and does not mark them invalid. --- hidtest/test.c | 339 ++++++++++++++++++++++++++++++++++++++----------- libusb/hid.c | 210 +++++++++++++++++++----------- linux/hid.c | 131 +++++++++++++++---- mac/hid.c | 111 ++++++++++++---- windows/hid.c | 121 +++++++++++++----- 5 files changed, 694 insertions(+), 218 deletions(-) diff --git a/hidtest/test.c b/hidtest/test.c index 9fbf2321b..3667a5756 100644 --- a/hidtest/test.c +++ b/hidtest/test.c @@ -19,13 +19,17 @@ #include #include #include +#include // for "tolower()" #include -// Headers needed for sleeping. +// Headers needed for sleeping and console management (wait for a keypress) #ifdef _WIN32 #include + #include #else + #include + #include #include #endif @@ -50,8 +54,46 @@ #if defined(USING_HIDAPI_LIBUSB) && HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) #include #endif + // +// A function that waits for a key to be pressed and reports it's code +// Used for immediate response in interactive subroutines +// Taken from: +// https://cboard.cprogramming.com/c-programming/63166-kbhit-linux.html +int waitkey(void) +{ +#ifdef _WIN32 + return _getch(); +#else + struct termios oldt, newt; + int ch; + int oldf; + + tcgetattr(STDIN_FILENO, &oldt); + newt = oldt; + newt.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); + oldf = fcntl(STDIN_FILENO, F_GETFL, 0); + fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); + + do + { + usleep(1); + ch = getchar(); + } + while (EOF == ch); + + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + fcntl(STDIN_FILENO, F_SETFL, oldf); + return ch; +#endif +} + +// + +// +// Report Device info const char *hid_bus_name(hid_bus_type bus_type) { static const char *const HidBusTypeName[] = { "Unknown", @@ -126,80 +168,28 @@ void print_devices_with_descriptor(struct hid_device_info *cur_dev) { } } -int device_callback( - hid_hotplug_callback_handle callback_handle, - struct hid_device_info* device, - hid_hotplug_event event, - void* user_data) +// +// Default static testing +void test_static(void) { - (void)user_data; - - if (event & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED) - printf("Handle %d: New device is connected: %s.\n", callback_handle, device->path); - else - printf("Handle %d: Device was disconnected: %s.\n", callback_handle, device->path); - - printf("type: %04hx %04hx\n serial_number: %ls", device->vendor_id, device->product_id, device->serial_number); - printf("\n"); - printf(" Manufacturer: %ls\n", device->manufacturer_string); - printf(" Product: %ls\n", device->product_string); - printf(" Release: %hx\n", device->release_number); - printf(" Interface: %d\n", device->interface_number); - printf(" Usage (page): 0x%hx (0x%hx)\n", device->usage, device->usage_page); - printf("\n"); - - /* Printed data might not show on the screen - force it out */ - fflush(stdout); + struct hid_device_info *devs; - return 0; + devs = hid_enumerate(0x0, 0x0); + print_devices_with_descriptor(devs); + hid_free_enumeration(devs); } -int main(int argc, char* argv[]) -{ - (void)argc; - (void)argv; +// +// Fixed device testing +void test_device(void) +{ int res; unsigned char buf[256]; - #define MAX_STR 255 +#define MAX_STR 255 wchar_t wstr[MAX_STR]; hid_device *handle; int i; - hid_hotplug_callback_handle token1, token2; - - struct hid_device_info *devs; - - printf("hidapi test/example tool. Compiled with hidapi version %s, runtime version %s.\n", HID_API_VERSION_STR, hid_version_str()); - if (HID_API_VERSION == HID_API_MAKE_VERSION(hid_version()->major, hid_version()->minor, hid_version()->patch)) { - printf("Compile-time version matches runtime version of hidapi.\n\n"); - } - else { - printf("Compile-time version is different than runtime version of hidapi.\n]n"); - } - - if (hid_init()) - return -1; - -#if defined(__APPLE__) && HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) - // To work properly needs to be called before hid_open/hid_open_path after hid_init. - // Best/recommended option - call it right after hid_init. - hid_darwin_set_open_exclusive(0); -#endif - - devs = hid_enumerate(0x0, 0x0); - print_devices_with_descriptor(devs); - hid_free_enumeration(devs); - - hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token1); - hid_hotplug_register_callback(0x054c, 0x0ce6, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token2); - - while (1) - { - - } - - hid_hotplug_deregister_callback(token2); - hid_hotplug_deregister_callback(token1); // Set up the command buffer. memset(buf,0x00,sizeof(buf)); @@ -213,8 +203,7 @@ int main(int argc, char* argv[]) handle = hid_open(0x4d8, 0x3f, NULL); if (!handle) { printf("unable to open device\n"); - hid_exit(); - return 1; + return; } // Read the Manufacturer String @@ -344,13 +333,221 @@ int main(int argc, char* argv[]) } hid_close(handle); +} - /* Free static HIDAPI objects. */ - hid_exit(); +// +// Normal hotplug testing +int device_callback( + hid_hotplug_callback_handle callback_handle, + struct hid_device_info* device, + hid_hotplug_event event, + void* user_data) +{ + (void)user_data; -#ifdef _WIN32 - system("pause"); + if (event & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED) + printf("Handle %d: New device is connected: %s.\n", callback_handle, device->path); + else + printf("Handle %d: Device was disconnected: %s.\n", callback_handle, device->path); + + printf("type: %04hx %04hx\n serial_number: %ls", device->vendor_id, device->product_id, device->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", device->manufacturer_string); + printf(" Product: %ls\n", device->product_string); + printf(" Release: %hx\n", device->release_number); + printf(" Interface: %d\n", device->interface_number); + printf(" Usage (page): 0x%hx (0x%hx)\n", device->usage, device->usage_page); + printf("(Press Q to exit the test)\n"); + printf("\n"); + + return 0; +} + + +void test_hotplug(void) +{ + printf("Starting the Hotplug test\n"); + printf("(Press Q to exit the test)\n"); + + hid_hotplug_callback_handle token1, token2; + + hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token1); + hid_hotplug_register_callback(0x054c, 0x0ce6, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token2); + + while (1) + { + int command = tolower(waitkey()); + if ('q' == command) + { + break; + } + } + + hid_hotplug_deregister_callback(token2); + hid_hotplug_deregister_callback(token1); + + printf("\n\nHotplug test stopped\n"); +} + +// +// Stress-testing weird edge cases in hotplugs +int cb1_handle; +int cb2_handle; +int cb_test1_triggered; + +int cb2_func(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, + void *user_data) +{ + (void) callback_handle; + (void) device; + (void) event; + (void) user_data; + // TIP: only perform the test once + if(cb_test1_triggered) + { + return 1; + } + + printf("Callback 2 fired\n"); + + // Deregister the first callback + // It should be placed in the list at an index prior to the current one, which will make the pointer to the current one invalid on some implementations + hid_hotplug_deregister_callback(cb1_handle); + + cb_test1_triggered = 1; + + // As long as we are inside this callback, nothing goes wrong; however, returning from here will cause a use-after-free error on flawed implementations + // as to retrieve the next element (or to check for it's presence) it will look those dereference a pointer located in an already freed area + // Undefined behavior + return 1; +} + +int cb1_func(hid_hotplug_callback_handle callback_handle, + struct hid_device_info *device, + hid_hotplug_event event, + void *user_data) +{ + (void) callback_handle; + (void) device; + (void) event; + (void) user_data; + + // TIP: only perform the test once + if(cb_test1_triggered) + { + return 1; + } + + printf("Callback 1 fired\n"); + + // Register the second callback and make it be called immediately by enumeration attempt + // Will cause a deadlock on Linux immediately + hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, cb2_func, NULL, &cb2_handle); + return 1; +} + +void test_hotplug_deadlocks(void) +{ + cb_test1_triggered = 0; + printf("Starting the Hotplug callbacks deadlocks test\n"); + printf("TIP: if you don't see a message that it succeeded, it means the test failed and the system is now deadlocked\n"); + // Register the first callback and make it be called immediately by enumeration attempt (if at least 1 device is present) + hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, cb1_func, NULL, &cb1_handle); + + printf("Test finished successfully (at least no deadlocks were found)\n"); + + // Intentionally leave a callback registered to test how hid_exit handles it + //hid_hotplug_deregister_callback(cb2_handle); +} + + +// +// CLI + +void print_version_check(void) +{ + printf("hidapi test/example tool. Compiled with hidapi version %s, runtime version %s.\n", HID_API_VERSION_STR, hid_version_str()); + if (HID_API_VERSION == HID_API_MAKE_VERSION(hid_version()->major, hid_version()->minor, hid_version()->patch)) { + printf("Compile-time version matches runtime version of hidapi.\n\n"); + } + else { + printf("Compile-time version is different than runtime version of hidapi.\n]n"); + } +} + +void interactive_loop(void) +{ + int command = 0; + + print_version_check(); + + do { + printf("Interactive HIDAPI testing utility\n"); + printf(" 1: List connected devices\n"); + printf(" 2: Dynamic hotplug test\n"); + printf(" 3: Test specific device [04d8:003f]\n"); + printf(" 4: Test hotplug callback management deadlocking scenario\n"); + printf(" Q: Quit\n"); + printf("Please enter command:"); + + /* Printed data might not show on the screen when the command is done - force it out */ + fflush(stdout); + + command = toupper(waitkey()); + + printf("%c\n\n========================================\n\n", command); + + // GET COMMAND + switch (command) { + case '1': + test_static(); + break; + case '2': + test_hotplug(); + break; + case '3': + test_device(); + break; + case '4': + test_hotplug_deadlocks(); + break; + case 'Q': + printf("Quitting.\n"); + return; + default: + printf("Command not recognized\n"); + break; + } + + /* Printed data might not show on the screen when the command is done - force it out */ + fflush(stdout); + + printf("\n\n========================================\n\n"); + } while(command != 'Q'); +} + +// +// Main +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; + + if (hid_init()) + return -1; + +#if defined(__APPLE__) && HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0) + // To work properly needs to be called before hid_open/hid_open_path after hid_init. + // Best/recommended option - call it right after hid_init. + hid_darwin_set_open_exclusive(0); #endif + interactive_loop(); + + /* Free static HIDAPI objects. */ + hid_exit(); + return 0; } diff --git a/libusb/hid.c b/libusb/hid.c index 22e1234fe..47077a0f4 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -160,11 +160,12 @@ static struct hid_hotplug_context { hidapi_thread_state callback_thread; /* This mutex prevents changes to the callback list */ - pthread_mutex_t cb_mutex; + pthread_mutex_t mutex; - int mutex_ready; - - int thread_running; + /* Boolean flags */ + unsigned char mutex_ready; + unsigned char mutex_in_use; + unsigned char cb_list_dirty; struct hid_hotplug_queue* queue; @@ -176,7 +177,6 @@ static struct hid_hotplug_context { } hid_hotplug_context = { .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, .mutex_ready = 0, - .thread_running = 0, .queue = NULL, .hotplug_cbs = NULL, .devs = NULL, @@ -541,28 +541,46 @@ struct hid_hotplug_callback hid_hotplug_callback_handle handle; }; +static void hid_internal_hotplug_remove_postponed() +{ + /* Unregister the callbacks whose removal was postponed */ + /* This function is always called inside a locked mutex */ + /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { + 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; + } + current = &callback->next; + } + + /* Clear the flag so we don't start the cycle unless necessary */ + hid_hotplug_context.cb_list_dirty = 0; +} + static void hid_internal_hotplug_cleanup() { - if (hid_hotplug_context.hotplug_cbs != NULL) { + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { return; } - /* Mark the threads as stopped */ - hid_hotplug_context.thread_running = 0; + /* Before checking if the list is empty, clear any entries whose removal was postponed first */ + hid_internal_hotplug_remove_postponed(); - /* Forcibly wake up the thread so it can shut down immediately */ - hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + if (hid_hotplug_context.hotplug_cbs != NULL) { + return; + } /* Wait for both threads to stop */ hidapi_thread_join(&hid_hotplug_context.libusb_thread); - hidapi_thread_join(&hid_hotplug_context.callback_thread); - - /* Cleanup connected device list */ - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; - /* Disarm the libusb listener */ - libusb_hotplug_deregister_callback(usb_context, hid_hotplug_context.callback_handle); - libusb_exit(hid_hotplug_context.context); } static void hid_internal_hotplug_init() @@ -570,8 +588,18 @@ static void hid_internal_hotplug_init() if (!hid_hotplug_context.mutex_ready) { hidapi_thread_state_init(&hid_hotplug_context.libusb_thread); hidapi_thread_state_init(&hid_hotplug_context.callback_thread); - pthread_mutex_init(&hid_hotplug_context.cb_mutex, NULL); + + /* Initialize the mutex as recursive */ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&hid_hotplug_context.mutex, &attr); + pthread_mutexattr_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; } } @@ -581,7 +609,7 @@ static void hid_internal_hotplug_exit() return; } - pthread_mutex_lock(&hid_hotplug_context.cb_mutex); + pthread_mutex_lock(&hid_hotplug_context.mutex); struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; /* Remove all callbacks from the list */ while (*current) { @@ -590,9 +618,9 @@ static void hid_internal_hotplug_exit() *current = next; } hid_internal_hotplug_cleanup(); - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + pthread_mutex_unlock(&hid_hotplug_context.mutex); hid_hotplug_context.mutex_ready = 0; - pthread_mutex_destroy(&hid_hotplug_context.cb_mutex); + pthread_mutex_destroy(&hid_hotplug_context.mutex); hidapi_thread_state_destroy(&hid_hotplug_context.callback_thread); hidapi_thread_state_destroy(&hid_hotplug_context.libusb_thread); @@ -1071,25 +1099,27 @@ static int match_libusb_to_info(libusb_device *device, struct hid_device_info* i static void hid_internal_invoke_callbacks(struct hid_device_info* info, hid_hotplug_event event) { - pthread_mutex_lock(&hid_hotplug_context.cb_mutex); + 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; 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 remove the callback and proceed */ - /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + /* If the result is non-zero, we mark the callback for removal and proceed */ if (result) { - struct hid_hotplug_callback *callback = *current; - *current = (*current)->next; - free(callback); + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; continue; } } current = &callback->next; } - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + + hid_hotplug_context.mutex_in_use = 0; + hid_internal_hotplug_remove_postponed(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); } static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void * user_data) @@ -1128,23 +1158,6 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic return 0; } -static void* hotplug_thread(void* user_data) -{ - (void) user_data; - - /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ - /* This timeout only affects how much time it takes to stop the thread */ - struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 5000; - - while (hid_hotplug_context.thread_running) { - /* 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); - } - return NULL; -} - static void process_hotplug_event(struct hid_hotplug_queue* msg) { if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) { @@ -1190,10 +1203,9 @@ static void process_hotplug_event(struct hid_hotplug_queue* msg) /* Release the libusb device - we are done with it */ libusb_unref_device(msg->device); - /* Clean up if the last callback was removed */ - pthread_mutex_lock(&hid_hotplug_context.cb_mutex); - hid_internal_hotplug_cleanup(); - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + /* 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 */ + /* No further cleaning is needed */ } static void* callback_thread(void* user_data) @@ -1207,25 +1219,67 @@ static void* callback_thread(void* user_data) ts.tv_nsec = 5000000; hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); - while (hid_hotplug_context.thread_running) { + + /* We stop the thread if by the moment there are no events left in the queue there are no callbacks left */ + while (1) { + /* Make the tread fall asleep and wait for a condition to wake it up */ + hidapi_thread_cond_timedwait(&hid_hotplug_context.callback_thread, &ts); + /* We use this thread's mutex to protect the queue */ hidapi_thread_mutex_lock(&hid_hotplug_context.libusb_thread); while (hid_hotplug_context.queue) { - process_hotplug_event(hid_hotplug_context.queue); + struct hid_hotplug_queue *cur_event = hid_hotplug_context.queue; + process_hotplug_event(cur_event); /* Empty the queue */ - hid_hotplug_context.queue = hid_hotplug_context.queue->next; + cur_event = cur_event->next; + free(hid_hotplug_context.queue); + hid_hotplug_context.queue = cur_event; } hidapi_thread_mutex_unlock(&hid_hotplug_context.libusb_thread); - - /* Make the tread fall asleep and wait for a condition to wake it up */ - hidapi_thread_cond_timedwait(&hid_hotplug_context.callback_thread, &ts); + if (!hid_hotplug_context.hotplug_cbs) { + break; + } } + + /* Cleanup connected device list */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); return NULL; } +static void* hotplug_thread(void* user_data) +{ + (void) user_data; + + hidapi_thread_create(&hid_hotplug_context.callback_thread, callback_thread, NULL); + + /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ + /* This timeout only affects how much time it takes to stop the thread */ + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 5000; + + while (hid_hotplug_context.hotplug_cbs) { + /* This will allow libusb to call the callbacks, which will fill up the queue */ + libusb_handle_events_timeout_completed(hid_hotplug_context.context, &tv, NULL); + } + + /* Disarm the libusb listener */ + libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); + libusb_exit(hid_hotplug_context.context); + + /* Forcibly wake up the thread so it can shut down immediately and wait for it to stop */ + hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + + hidapi_thread_join(&hid_hotplug_context.callback_thread); + + return NULL; +} + int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle) { if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { @@ -1258,7 +1312,7 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven hid_internal_hotplug_init(); /* Lock the mutex to avoid race itions */ - pthread_mutex_lock(&hid_hotplug_context.cb_mutex); + pthread_mutex_lock(&hid_hotplug_context.mutex); hotplug_cb->handle = hid_hotplug_context.next_handle++; @@ -1284,7 +1338,7 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven /* Fill already connected devices so we can use this info in disconnection notification */ if (libusb_init(&hid_hotplug_context.context)) { free(hotplug_cb); - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } @@ -1300,16 +1354,18 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven libusb_exit(hid_hotplug_context.context); free(hotplug_cb); hid_hotplug_context.hotplug_cbs = NULL; - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } /* Initialization succeeded! We run the threads now */ - hid_hotplug_context.thread_running = 1; hidapi_thread_create(&hid_hotplug_context.libusb_thread, hotplug_thread, NULL); - hidapi_thread_create(&hid_hotplug_context.callback_thread, callback_thread, NULL); } + /* Mark the mutex as IN USE, to prevent callback removal from inside a callback */ + unsigned char old_state = hid_hotplug_context.mutex_in_use; + hid_hotplug_context.mutex_in_use = 1; + if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { struct hid_device_info* device = hid_hotplug_context.devs; /* Notify about already connected devices, if asked so */ @@ -1322,42 +1378,52 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven } } - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + hid_hotplug_context.mutex_in_use = old_state; + + hid_internal_hotplug_cleanup(); + + pthread_mutex_unlock(&hid_hotplug_context.mutex); return 0; } int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - struct hid_hotplug_callback *hotplug_cb = NULL; - if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG) || !hid_hotplug_context.mutex_ready || callback_handle <= 0) { return -1; } - pthread_mutex_lock(&hid_hotplug_context.cb_mutex); + pthread_mutex_lock(&hid_hotplug_context.mutex); if (hid_hotplug_context.hotplug_cbs == NULL) { - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; } + int result = -1; + /* Remove this notification */ for (struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; *current != NULL; current = &(*current)->next) { if ((*current)->handle == callback_handle) { - struct hid_hotplug_callback *next = (*current)->next; - hotplug_cb = *current; - *current = next; - free(hotplug_cb); + /* Check if we were already in a locked state, as we are NOT allowed to remove any callbacks if we are */ + if (hid_hotplug_context.mutex_in_use) { + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } else { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + } + result = 0; break; } } hid_internal_hotplug_cleanup(); - pthread_mutex_unlock(&hid_hotplug_context.cb_mutex); + pthread_mutex_unlock(&hid_hotplug_context.mutex); - return 0; + return result; } hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) diff --git a/linux/hid.c b/linux/hid.c index 06b32a60b..e77bf28ba 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -899,8 +899,11 @@ static struct hid_hotplug_context { pthread_t thread; pthread_mutex_t mutex; - - int mutex_ready; + + /* Boolean flags */ + unsigned char mutex_ready; + unsigned char mutex_in_use; + unsigned char cb_list_dirty; /* HIDAPI unique callback handle counter */ hid_hotplug_callback_handle next_handle; @@ -931,27 +934,61 @@ struct hid_hotplug_callback { struct hid_hotplug_callback *next; }; +static void hid_internal_hotplug_remove_postponed() +{ + /* Unregister the callbacks whose removal was postponed */ + /* This function is always called inside a locked mutex */ + /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { + 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; + } + current = &callback->next; + } + + /* Clear the flag so we don't start the cycle unless necessary */ + hid_hotplug_context.cb_list_dirty = 0; +} + static void hid_internal_hotplug_cleanup() { + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + return; + } + + /* Before checking if the list is empty, clear any entries whose removal was postponed first */ + hid_internal_hotplug_remove_postponed(); + if (hid_hotplug_context.hotplug_cbs != NULL) { return; } pthread_join(hid_hotplug_context.thread, NULL); - - /* Cleanup connected device list */ - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; - /* Disarm the udev monitor */ - udev_monitor_unref(hid_hotplug_context.mon); - udev_unref(hid_hotplug_context.udev_ctx); } static void hid_internal_hotplug_init() { if (!hid_hotplug_context.mutex_ready) { - pthread_mutex_init(&hid_hotplug_context.mutex, NULL); + /* Initialize the mutex as recursive */ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&hid_hotplug_context.mutex, &attr); + pthread_mutexattr_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; } } @@ -1109,23 +1146,28 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event) { + 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; 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 remove the callback and proceed */ - /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + /* If the result is non-zero, we mark the callback for removal and proceed */ if (result) { - struct hid_hotplug_callback *callback = *current; - *current = (*current)->next; - free(callback); + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; continue; } } current = &callback->next; } + + hid_hotplug_context.mutex_in_use = 0; + hid_internal_hotplug_remove_postponed(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); } static int match_udev_to_info(struct udev_device* raw_dev, struct hid_device_info *info) @@ -1141,11 +1183,19 @@ static void* hotplug_thread(void* user_data) { (void) user_data; + /* Note: the cleanup sequence is always executed with the mutex locked, so we shoud never lock the mutex without checking if we need to stop */ + while (hid_hotplug_context.monitor_fd > 0) { fd_set fds; struct timeval tv; int ret; + /* On every iteration, check if we still have any callbacks left and leave if none are left */ + /* NOTE: the check is performed UNLOCKED and the value CAN change in the background */ + if (!hid_hotplug_context.hotplug_cbs) { + break; + } + FD_ZERO(&fds); FD_SET(hid_hotplug_context.monitor_fd, &fds); /* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */ @@ -1155,6 +1205,11 @@ static void* hotplug_thread(void* user_data) ret = select(hid_hotplug_context.monitor_fd+1, &fds, NULL, NULL, &tv); + /* An extra check, just in case within those 5msec the thread was told to stop */ + if (!hid_hotplug_context.hotplug_cbs) { + break; + } + /* Check if our file descriptor has received data. */ if (ret > 0 && FD_ISSET(hid_hotplug_context.monitor_fd, &fds)) { @@ -1162,9 +1217,7 @@ static void* hotplug_thread(void* user_data) select() ensured that this will not block. */ struct udev_device *raw_dev = udev_monitor_receive_device(hid_hotplug_context.mon); if (raw_dev) { - /* Lock the mutex so callback/device lists don't change elsewhere from here on */ pthread_mutex_lock(&hid_hotplug_context.mutex); - const char* action = udev_device_get_action(raw_dev); if (!strcmp(action, "add")) { // We create a list of all usages on this UDEV device @@ -1209,6 +1262,14 @@ static void* hotplug_thread(void* user_data) } } } + + /* Cleanup connected device list */ + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; + /* Disarm the udev monitor */ + udev_monitor_unref(hid_hotplug_context.mon); + udev_unref(hid_hotplug_context.udev_ctx); + return NULL; } @@ -1268,7 +1329,7 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven else { // Prepare a UDEV context to run monitoring on hid_hotplug_context.udev_ctx = udev_new(); - if(!hid_hotplug_context.udev_ctx) + if (!hid_hotplug_context.udev_ctx) { pthread_mutex_unlock(&hid_hotplug_context.mutex); return -1; @@ -1289,6 +1350,26 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven pthread_create(&hid_hotplug_context.thread, NULL, &hotplug_thread, NULL); } + /* Mark the mutex as IN USE, to prevent callback removal from inside a callback */ + unsigned char old_state = hid_hotplug_context.mutex_in_use; + hid_hotplug_context.mutex_in_use = 1; + + if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { + struct hid_device_info* device = hid_hotplug_context.devs; + /* Notify about already connected devices, if asked so */ + while (device != NULL) { + if (hid_internal_match_device_id(device->vendor_id, device->product_id, hotplug_cb->vendor_id, hotplug_cb->product_id)) { + (*hotplug_cb->callback)(hotplug_cb->handle, device, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED, hotplug_cb->user_data); + } + + device = device->next; + } + } + + hid_hotplug_context.mutex_in_use = old_state; + + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); return 0; @@ -1296,7 +1377,7 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle) { - if (!hid_hotplug_context.mutex_ready) { + if (!hid_hotplug_context.mutex_ready || callback_handle <= 0) { return -1; } @@ -1312,9 +1393,15 @@ 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) { - struct hid_hotplug_callback *next = (*current)->next; - free(*current); - *current = next; + /* Check if we were already in a locked state, as we are NOT allowed to remove any callbacks if we are */ + if (hid_hotplug_context.mutex_in_use) { + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } else { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + } result = 0; break; } diff --git a/mac/hid.c b/mac/hid.c index 75038b471..910747632 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -497,7 +497,10 @@ static struct hid_hotplug_context { pthread_mutex_t mutex; - int mutex_ready; + /* Boolean flags */ + unsigned char mutex_ready; + unsigned char mutex_in_use; + unsigned char cb_list_dirty; /* Linked list of the hotplug callbacks */ struct hid_hotplug_callback *hotplug_cbs; @@ -516,8 +519,40 @@ static struct hid_hotplug_context { .devs = NULL }; -static void hid_internal_hotplug_cleanup() +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) { + 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; + } + current = &callback->next; + } + + /* Clear the flag so we don't start the cycle unless necessary */ + hid_hotplug_context.cb_list_dirty = 0; +} + +static void hid_internal_hotplug_cleanup(void) +{ + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + return; + } + + /* Before checking if the list is empty, clear any entries whose removal was postponed first */ + hid_internal_hotplug_remove_postponed(); + if (hid_hotplug_context.hotplug_cbs != NULL) { return; } @@ -537,15 +572,24 @@ static void hid_internal_hotplug_cleanup() pthread_join(hid_hotplug_context.thread, NULL); } -static void hid_internal_hotplug_init() +static void hid_internal_hotplug_init(void) { if (!hid_hotplug_context.mutex_ready) { - pthread_mutex_init(&hid_hotplug_context.mutex, NULL); + /* Initialize the mutex as recursive */ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&hid_hotplug_context.mutex, &attr); + pthread_mutexattr_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; } } -static void hid_internal_hotplug_exit() +static void hid_internal_hotplug_exit(void) { if (!hid_hotplug_context.mutex_ready) { return; @@ -604,11 +648,12 @@ static int hid_internal_match_device_id(unsigned short vendor_id, unsigned short return (expected_vendor_id == 0x0 || vendor_id == expected_vendor_id) && (expected_product_id == 0x0 || product_id == expected_product_id); } -static void process_pending_events(void) { +static void process_pending_events(void) +{ SInt32 res; do { res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.001, FALSE); - } while(res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut); + } while (res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut); } static int read_usb_interface_from_hid_service_parent(io_service_t hid_service) @@ -925,23 +970,29 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event) { + 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; 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 remove the callback and proceed */ + /* If the result is non-zero, we mark the callback for removal and proceed */ /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ if (result) { - struct hid_hotplug_callback *callback = *current; - *current = (*current)->next; - free(callback); + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; continue; } } current = &callback->next; } + + hid_hotplug_context.mutex_in_use = 0; + hid_internal_hotplug_remove_postponed(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); } static void hid_internal_hotplug_connect_callback(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) @@ -951,19 +1002,19 @@ static void hid_internal_hotplug_connect_callback(void *context, IOReturn result (void) sender; struct hid_device_info* info = create_device_info(device); - if(!info) { + if (!info) { return; } struct hid_device_info* info_cur = info; - /* NOTE: we don't call any callbacks and we don't lock the mutex during initialization: the mutex is held by the main thread, but it's waiting by a barrier*/ + /* NOTE: we don't call any callbacks and we don't lock the mutex during initialization: the mutex is held by the main thread, but it's waiting by a barrier */ if (hid_hotplug_context.thread_state > 0) { /* Lock the mutex to avoid race conditions */ pthread_mutex_lock(&hid_hotplug_context.mutex); /* Invoke all callbacks */ - while(info_cur) + while (info_cur) { hid_internal_invoke_callbacks(info_cur, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED); info_cur = info_cur->next; @@ -1036,10 +1087,10 @@ static void hid_internal_hotplug_disconnect_callback(void *context, IOReturn res } } -static void hotplug_stop_callback(void *context) +static void hotplug_stop_callback(void* context) { - hid_device *dev = (hid_device*) context; - CFRunLoopStop(dev->run_loop); /*TODO: CFRunLoopGetCurrent()*/ + (void) context; + CFRunLoopStop(hid_hotplug_context.run_loop); } static void* hotplug_thread(void* user_data) @@ -1049,7 +1100,7 @@ static void* hotplug_thread(void* user_data) hid_hotplug_context.thread_state = 0; hid_hotplug_context.manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); - if(!hid_hotplug_context.run_loop_mode) { + if (!hid_hotplug_context.run_loop_mode) { const char *str = "HIDAPI_hotplug"; hid_hotplug_context.run_loop_mode = CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); } @@ -1060,7 +1111,7 @@ static void* hotplug_thread(void* user_data) hid_hotplug_context.run_loop = CFRunLoopGetCurrent(); /* Create the RunLoopSource which is used to signal the - event loop to stop when hid_close() is called. */ + event loop to stop when hid_internal_hotplug_cleanup() is called. */ CFRunLoopSourceContext ctx; memset(&ctx, 0, sizeof(ctx)); ctx.version = 0; @@ -1178,6 +1229,10 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven hid_hotplug_context.hotplug_cbs = hotplug_cb; } + /* Mark the mutex as IN USE, to prevent callback removal from inside a callback */ + unsigned char old_state = hid_hotplug_context.mutex_in_use; + hid_hotplug_context.mutex_in_use = 1; + if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { struct hid_device_info* device = hid_hotplug_context.devs; /* Notify about already connected devices, if asked so */ @@ -1189,7 +1244,11 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven device = device->next; } } - + + hid_hotplug_context.mutex_in_use = old_state; + + hid_internal_hotplug_cleanup(); + pthread_mutex_unlock(&hid_hotplug_context.mutex); return 0; @@ -1213,9 +1272,15 @@ 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) { - struct hid_hotplug_callback *next = (*current)->next; - free(*current); - *current = next; + /* Check if we were already in a locked state, as we are NOT allowed to remove any callbacks if we are */ + if (hid_hotplug_context.mutex_in_use) { + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } else { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + } result = 0; break; } diff --git a/windows/hid.c b/windows/hid.c index f849c3038..d45c103cb 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -210,7 +210,10 @@ static struct hid_hotplug_context { /* Critical section (faster mutex substitute), for both cached device list and callback list changes */ CRITICAL_SECTION critical_section; - BOOL critical_section_ready; + /* Boolean flags */ + unsigned char mutex_ready; + unsigned char mutex_in_use; + unsigned char cb_list_dirty; /* HIDAPI unique callback handle counter */ hid_hotplug_callback_handle next_handle; @@ -222,7 +225,7 @@ static struct hid_hotplug_context { struct hid_device_info *devs; } hid_hotplug_context = { .notify_handle = NULL, - .critical_section_ready = 0, + .mutex_ready = 0, .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, .hotplug_cbs = NULL, .devs = NULL @@ -318,7 +321,7 @@ static void register_winapi_error_to_buffer(wchar_t **error_buffer, const WCHAR /* Get rid of the CR and LF that FormatMessage() sticks at the end of the message. Thanks Microsoft! */ - while(msg[msg_len-1] == L'\r' || msg[msg_len-1] == L'\n' || msg[msg_len-1] == L' ') + while (msg[msg_len-1] == L'\r' || msg[msg_len-1] == L'\n' || msg[msg_len-1] == L' ') { msg[msg_len-1] = L'\0'; msg_len--; @@ -399,9 +402,13 @@ HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) static void hid_internal_hotplug_init() { - if (!hid_hotplug_context.critical_section_ready) { + if (!hid_hotplug_context.mutex_ready) { InitializeCriticalSection(&hid_hotplug_context.critical_section); - hid_hotplug_context.critical_section_ready = 1; + + /* Set state to Ready */ + hid_hotplug_context.mutex_ready = 1; + hid_hotplug_context.mutex_in_use = 0; + hid_hotplug_context.cb_list_dirty = 0; } } @@ -422,8 +429,52 @@ int HID_API_EXPORT hid_init(void) return 0; } +struct hid_hotplug_callback { + hid_hotplug_callback_handle handle; + unsigned short vendor_id; + unsigned short product_id; + hid_hotplug_event events; + void *user_data; + hid_hotplug_callback_fn callback; + + /* Pointer to the next notification */ + struct hid_hotplug_callback *next; +}; + +static void hid_internal_hotplug_remove_postponed() +{ + /* Unregister the callbacks whose removal was postponed */ + /* This function is always called inside a locked mutex */ + /* However, any actions are only allowed if the mutex is NOT in use and if the DIRTY flag is set */ + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use || !hid_hotplug_context.cb_list_dirty) { + 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; + } + current = &callback->next; + } + + /* Clear the flag so we don't start the cycle unless necessary */ + hid_hotplug_context.cb_list_dirty = 0; +} + static void hid_internal_hotplug_cleanup() { + if (!hid_hotplug_context.mutex_ready || hid_hotplug_context.mutex_in_use) { + return; + } + + /* Before checking if the list is empty, clear any entries whose removal was postponed first */ + hid_internal_hotplug_remove_postponed(); + /* Unregister the HID device connection notification when removing the last callback */ /* This function is always called inside a locked mutex */ if (hid_hotplug_context.hotplug_cbs != NULL) { @@ -446,21 +497,9 @@ static void hid_internal_hotplug_cleanup() hid_hotplug_context.notify_handle = NULL; } -struct hid_hotplug_callback { - hid_hotplug_callback_handle handle; - unsigned short vendor_id; - unsigned short product_id; - hid_hotplug_event events; - void *user_data; - hid_hotplug_callback_fn callback; - - /* Pointer to the next notification */ - struct hid_hotplug_callback *next; -}; - static void hid_internal_hotplug_exit() { - if (!hid_hotplug_context.critical_section_ready) { + if (!hid_hotplug_context.mutex_ready) { /* If the critical section is not initialized, we are safe to assume nothing else is */ return; } @@ -474,20 +513,20 @@ static void hid_internal_hotplug_exit() } hid_internal_hotplug_cleanup(); LeaveCriticalSection(&hid_hotplug_context.critical_section); - hid_hotplug_context.critical_section_ready = 0; + hid_hotplug_context.mutex_ready = 0; DeleteCriticalSection(&hid_hotplug_context.critical_section); } int HID_API_EXPORT hid_exit(void) { + hid_internal_hotplug_exit(); + #ifndef HIDAPI_USE_DDK free_library_handles(); hidapi_initialized = FALSE; #endif register_global_error(NULL); - hid_internal_hotplug_exit(); - return 0; } @@ -1094,28 +1133,34 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, } if (device) { + /* Mark the critical section as IN USE, to prevent callback removal from inside a callback */ + hid_hotplug_context.mutex_in_use = 1; + /* Call the notifications for the device */ struct hid_hotplug_callback **current = &hid_hotplug_context.hotplug_cbs; while (*current) { struct hid_hotplug_callback *callback = *current; if ((callback->events & hotplug_event) && hid_internal_match_device_id(device->vendor_id, device->product_id, callback->vendor_id, callback->product_id)) { int result = (callback->callback)(callback->handle, device, hotplug_event, callback->user_data); - /* If the result is non-zero, we remove the callback and proceed */ - /* Do not use the deregister call as it locks the mutex, and we are currently in a lock */ + + /* If the result is non-zero, we MARK the callback for future removal and proceed */ + /* We avoid changing the list until we are done calling the callbacks to simplify the process */ if (result) { - *current = (*current)->next; - free(callback); - continue; + callback->events = 0; + hid_hotplug_context.cb_list_dirty = 1; } } current = &callback->next; } + hid_hotplug_context.mutex_in_use = 0; + /* Free removed device */ if (hotplug_event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT) { free(device); } + /* Remove any callbacks that were marked for removal and stop the notification if none are left */ hid_internal_hotplug_cleanup(); } @@ -1208,6 +1253,10 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven } } + /* Mark the critical section as IN USE, to prevent callback removal from inside a callback */ + unsigned char old_state = hid_hotplug_context.mutex_in_use; + hid_hotplug_context.mutex_in_use = 1; + if ((flags & HID_API_HOTPLUG_ENUMERATE) && (events & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)) { struct hid_device_info* device = hid_hotplug_context.devs; /* Notify about already connected devices, if asked so */ @@ -1220,6 +1269,11 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven } } + hid_hotplug_context.mutex_in_use = old_state; + + /* Remove any callbacks that were marked for removal and stop the notification if none are left */ + hid_internal_hotplug_cleanup(); + LeaveCriticalSection(&hid_hotplug_context.critical_section); return 0; @@ -1227,7 +1281,7 @@ 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 (callback_handle <= 0 || !hid_hotplug_context.critical_section_ready) { + if (callback_handle <= 0 || !hid_hotplug_context.mutex_ready) { return -1; } @@ -1242,9 +1296,16 @@ 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) { - struct hid_hotplug_callback *next = (*current)->next; - free(*current); - *current = next; + /* Check if we were already in the critical section, as we are NOT allowed to remove any callbacks if we are */ + if (hid_hotplug_context.mutex_in_use) { + /* If we are not allowed to remove the callback, we mark it as pending removal */ + (*current)->events = 0; + hid_hotplug_context.cb_list_dirty = 1; + } else { + struct hid_hotplug_callback *next = (*current)->next; + free(*current); + *current = next; + } break; } } From 91145c93c643f8d13e113da6b59af5e7a1fba11e Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Tue, 31 Mar 2026 14:29:11 +0300 Subject: [PATCH 09/19] fix console log Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- hidtest/test.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hidtest/test.c b/hidtest/test.c index 36d30f087..434d432ee 100644 --- a/hidtest/test.c +++ b/hidtest/test.c @@ -488,7 +488,7 @@ void print_version_check(void) printf("Compile-time version matches runtime version of hidapi.\n\n"); } else { - printf("Compile-time version is different than runtime version of hidapi.\n]n"); + printf("Compile-time version is different than runtime version of hidapi.\n\n"); } } From 9f2c4722964bd0775f0e3ef6ad77e362d6f29afb Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Tue, 31 Mar 2026 16:19:33 +0300 Subject: [PATCH 10/19] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- libusb/hid.c | 5 +++-- linux/hid.c | 2 +- mac/hid.c | 2 +- windows/hid.c | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libusb/hid.c b/libusb/hid.c index ad0c4e874..e88c3a2d9 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -1135,6 +1135,7 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic struct hid_hotplug_queue* msg = calloc(1, sizeof(struct hid_hotplug_queue)); if (NULL == msg) { + libusb_unref_device(device); return 0; } @@ -1195,8 +1196,8 @@ static void process_hotplug_event(struct hid_hotplug_queue* msg) *current = (*current)->next; info->next = NULL; hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); - /* Free every removed device */ - free(info); + /* Free every removed device (and its internal allocations) */ + hid_free_enumeration(info); } else { current = &info->next; } diff --git a/linux/hid.c b/linux/hid.c index 0b462000a..58f88e3f7 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1276,7 +1276,7 @@ static void* hotplug_thread(void* user_data) info->next = NULL; hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); /* Free every removed device */ - free(info); + hid_free_enumeration(info); } else { current = &info->next; } diff --git a/mac/hid.c b/mac/hid.c index ca9beb222..4a555596e 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -1077,7 +1077,7 @@ static void hid_internal_hotplug_disconnect_callback(void *context, IOReturn res info->next = NULL; hid_internal_invoke_callbacks(info, HID_API_HOTPLUG_EVENT_DEVICE_LEFT); /* Free every removed device */ - free(info); + hid_free_enumeration(info); } else { current = &info->next; } diff --git a/windows/hid.c b/windows/hid.c index 6bd881a92..999eebcbd 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1176,7 +1176,9 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, /* Free removed device */ if (hotplug_event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT) { - free(device); + /* Ensure only this node is freed by hid_free_enumeration() */ + device->next = NULL; + hid_free_enumeration(device); } /* Remove any callbacks that were marked for removal and stop the notification if none are left */ From 5360e03d6edcb7820eda3dd0fa1f8706e82e2600 Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Tue, 31 Mar 2026 16:27:15 +0300 Subject: [PATCH 11/19] better device unplug handling on Windows --- windows/hid.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/windows/hid.c b/windows/hid.c index 999eebcbd..e558ac4e4 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1142,6 +1142,7 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, if (_stricmp((*current)->path, path) == 0) { struct hid_device_info *next = (*current)->next; device = *current; + device->next = NULL; *current = next; break; } @@ -1176,8 +1177,6 @@ DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, /* Free removed device */ if (hotplug_event == HID_API_HOTPLUG_EVENT_DEVICE_LEFT) { - /* Ensure only this node is freed by hid_free_enumeration() */ - device->next = NULL; hid_free_enumeration(device); } From 8bd8ff7edcee5e0f766bdf69c947babdd03d8ce8 Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Tue, 31 Mar 2026 16:54:39 +0300 Subject: [PATCH 12/19] code review fix --- mac/hid.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mac/hid.c b/mac/hid.c index 4a555596e..0c29ca1a3 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -720,10 +720,10 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev, /* A small trick to store an io_service_t tag along with hid_device_info for matching info with unplugged device */ cur_dev_ex = (struct hid_device_info_ex *)calloc(1, sizeof(struct hid_device_info_ex)); - cur_dev = &(cur_dev_ex->info); - if (cur_dev == NULL) { + if (cur_dev_ex == NULL) { return NULL; } + cur_dev = &(cur_dev_ex->info); dev_vid = get_vendor_id(dev); dev_pid = get_product_id(dev); @@ -1045,7 +1045,7 @@ static void hid_internal_hotplug_connect_callback(void *context, IOReturn result } } -int match_ref_to_info(IOHIDDeviceRef device, struct hid_device_info *info) +static int match_ref_to_info(IOHIDDeviceRef device, struct hid_device_info *info) { if (!device || !info) { return 0; From 1889e9fe22963e692e72ab8ace5572ff182db4ec Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:59:11 +0300 Subject: [PATCH 13/19] libusb: Fix high CPU usage in callback thread (#783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Callback_thread used hidapi_thread_cond_timedwait with an absolute timespec already in the past, causing a tight spin loop. Replaced with cond_wait + predicate pattern so the thread sleeps until an event or a shutdown signal arrives. Multithreading fixes folded in along the way: - Queue mutex moved from libusb_thread to callback_thread (its natural condvar partner), with signal-under-mutex to avoid a lost-wakeup race at shutdown. - process_hotplug_event() now runs under hid_hotplug_context.mutex to match hid_hotplug_register_callback()'s HID_API_HOTPLUG_ENUMERATE traversal of hid_hotplug_context.devs — fixes a use-after-free where a concurrent disconnect could free a list node mid-traversal. - devs teardown moved to hid_internal_hotplug_cleanup() (after hidapi_thread_join) so every access to devs happens under hid_hotplug_context.mutex. - hid_hotplug_register_callback() now frees devs on the libusb_hotplug_register_callback() error path, which previously leaked. Closes: #782 Assisted-by: Copilot:claude-sonnet-4.6 Assisted-by: Copilot:claude-opus-4.6 Assisted-by: Claude:claude-opus-4.7 --- .gitignore | 2 ++ libusb/hid.c | 71 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index a479f336d..ee128dbaf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,6 @@ doxygen/html/ # Visual Studio Code + CMake .vscode/ + +# Local build directory build/ diff --git a/libusb/hid.c b/libusb/hid.c index e88c3a2d9..7e186ad08 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -172,7 +172,10 @@ static struct hid_hotplug_context { /* Linked list of the hotplug callbacks */ struct hid_hotplug_callback *hotplug_cbs; - /* Linked list of the device infos (mandatory when the device is disconnected) */ + /* Linked list of the device infos (mandatory when the device is disconnected). + * 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; } hid_hotplug_context = { .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, @@ -584,6 +587,11 @@ static void hid_internal_hotplug_cleanup() /* Wait for both threads to stop */ hidapi_thread_join(&hid_hotplug_context.libusb_thread); + + /* 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; } static void hid_internal_hotplug_init() @@ -1143,8 +1151,8 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic msg->event = event; msg->next = NULL; - /* We use this thread's mutex to protect the queue */ - hidapi_thread_mutex_lock(&hid_hotplug_context.libusb_thread); + /* Use callback_thread's mutex to protect the queue and signal it */ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); struct hid_hotplug_queue* end = hid_hotplug_context.queue; if (end) { while (end->next) { @@ -1154,16 +1162,21 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic } else { hid_hotplug_context.queue = msg; } - hidapi_thread_mutex_unlock(&hid_hotplug_context.libusb_thread); - /* Wake up the other thread so it can react to the new message immediately */ + /* Wake up the callback thread so it can react to the new message immediately */ hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); return 0; } static void process_hotplug_event(struct hid_hotplug_queue* msg) { + /* 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. */ + pthread_mutex_lock(&hid_hotplug_context.mutex); + 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; @@ -1204,6 +1217,8 @@ static void process_hotplug_event(struct hid_hotplug_queue* msg) } } + pthread_mutex_unlock(&hid_hotplug_context.mutex); + /* Release the libusb device - we are done with it */ libusb_unref_device(msg->device); @@ -1216,40 +1231,32 @@ static void* callback_thread(void* user_data) { (void) user_data; - /* 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 */ - hidapi_timespec ts; - ts.tv_sec = 0; - ts.tv_nsec = 5000000; - 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 */ while (1) { - /* Make the tread fall asleep and wait for a condition to wake it up */ - hidapi_thread_cond_timedwait(&hid_hotplug_context.callback_thread, &ts); + /* Wait for events to arrive or shutdown signal */ + while (!hid_hotplug_context.queue && hid_hotplug_context.hotplug_cbs) { + hidapi_thread_cond_wait(&hid_hotplug_context.callback_thread); + } - /* We use this thread's mutex to protect the queue */ - hidapi_thread_mutex_lock(&hid_hotplug_context.libusb_thread); + /* Process all pending events from the queue */ while (hid_hotplug_context.queue) { struct hid_hotplug_queue *cur_event = hid_hotplug_context.queue; - process_hotplug_event(cur_event); + hid_hotplug_context.queue = cur_event->next; - /* Empty the queue */ - cur_event = cur_event->next; - free(hid_hotplug_context.queue); - hid_hotplug_context.queue = cur_event; + /* Release the lock while processing to avoid blocking event producers */ + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); + process_hotplug_event(cur_event); + free(cur_event); + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); } - hidapi_thread_mutex_unlock(&hid_hotplug_context.libusb_thread); + if (!hid_hotplug_context.hotplug_cbs) { break; } } - /* Cleanup connected device list */ - hid_free_enumeration(hid_hotplug_context.devs); - hid_hotplug_context.devs = NULL; - hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); return NULL; @@ -1276,8 +1283,12 @@ static void* hotplug_thread(void* user_data) libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle); libusb_exit(hid_hotplug_context.context); - /* Forcibly wake up the thread so it can shut down immediately and wait for it to stop */ + /* 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. */ + hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread); hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread); + hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread); hidapi_thread_join(&hid_hotplug_context.callback_thread); @@ -1354,8 +1365,12 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven 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 */ + /* 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). */ libusb_exit(hid_hotplug_context.context); + hid_free_enumeration(hid_hotplug_context.devs); + hid_hotplug_context.devs = NULL; free(hotplug_cb); hid_hotplug_context.hotplug_cbs = NULL; pthread_mutex_unlock(&hid_hotplug_context.mutex); From 885e3e4e4ad9e1ec26dea7fcac6d2b94dd345786 Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Sun, 14 Jun 2026 13:41:32 +0300 Subject: [PATCH 14/19] ci: use setup-msvc-dev action instead of a hard-coded vcvars path (#817) The windows-cmake job set up the MSVC environment by calling a hard-coded "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" in its NMake, clang-cl and Meson steps. The hosted windows-latest image no longer has Visual Studio under that "Enterprise" path, so those steps started failing with "The system cannot find the path specified." Set up the developer environment once per job with the TheMrMilchmann/setup-msvc-dev action -- it locates the installed toolchain (no hard-coded path or edition) -- and drop all the hard-coded vcvars64.bat calls. That environment is job-wide, so the MinGW build (which must use gcc, not cl) is split out into its own windows-mingw job that runs without the MSVC environment. The MSVC, NMake, clang-cl and Meson builds stay in windows-cmake with the action. Assisted-by: claude-code:claude-opus-4-8 (cherry picked from commit 251483627ad13bdf00307d97160255b28559a135) --- .github/workflows/builds.yml | 39 ++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 83ebe190a..35ffa9d79 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -161,7 +161,7 @@ jobs: ninja - windows-cmake: + windows-cmake-msvc: runs-on: windows-latest @@ -175,6 +175,10 @@ jobs: choco install ninja pip3 install meson refreshenv + - name: Set up MSVC developer environment + uses: TheMrMilchmann/setup-msvc-dev@v4 + with: + arch: x64 - name: Configure CMake MSVC shell: cmd run: | @@ -209,13 +213,11 @@ jobs: - name: Configure CMake NMake MSVC shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" cmake -G"NMake Makefiles" -B build\nmake -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install\nmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%" - name: Build CMake NMake MSVC working-directory: build\nmake shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" nmake install - name: Check artifacts CMake NMake MSVC uses: andstor/file-existence-action@v2 @@ -228,7 +230,6 @@ jobs: - name: Check CMake NMake MSVC Export Package shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" cmake ^ -G"NMake Makefiles" ^ -B build\nmake_test ^ @@ -245,13 +246,11 @@ jobs: - name: Configure CMake NMake ClangCL shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" cmake -G"NMake Makefiles" -B build\clang_cl -S hidapisrc -D CMAKE_C_COMPILER=clang-cl -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_ENABLE_ASAN=OFF -DCMAKE_INSTALL_PREFIX=install\clang_cl -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%" - name: Build CMake NMake ClangCL working-directory: build\clang_cl shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" nmake install - name: Check artifacts CMake NMake ClangCL uses: andstor/file-existence-action@v2 @@ -264,7 +263,6 @@ jobs: - name: Check CMake NMake ClangCL Export Package shell: cmd run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" cmake ^ -G"NMake Makefiles" ^ -B build\clang_cl_test ^ @@ -280,6 +278,25 @@ jobs: working-directory: build/clang_cl run: ctest -C RelWithDebInfo --no-compress-output --output-on-failure + - name: Check Meson build + shell: cmd + run: | + meson setup build_meson hidapisrc + cd build_meson + ninja + + + windows-cmake-mingw: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + with: + path: hidapisrc + # MinGW builds with gcc and runs in its own job, deliberately without the + # MSVC developer environment (which would otherwise make CMake's "MinGW + # Makefiles" generator pick up cl). - name: Configure CMake MinGW shell: cmd run: | @@ -311,14 +328,6 @@ jobs: working-directory: build\mingw run: ctest --no-compress-output --output-on-failure - - name: Check Meson build - shell: cmd - run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" - meson setup build_meson hidapisrc - cd build_meson - ninja - windows-msbuild: From ab7c490700721ae7eba4f2eafcc30bf2214de70c Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Tue, 14 Jul 2026 00:59:37 +0300 Subject: [PATCH 15/19] Hotplug docs: thread-safety contract and async ENUMERATE semantics (#790) Document the full hotplug API contract in hidapi.h: thread-safety rules (re-entrant hotplug mutex, safe-to-call list inside callbacks), execution context (callbacks only ever run on HIDAPI's internal event context), HID_API_HOTPLUG_ENUMERATE redefined as an asynchronous initial pass from a registration-time snapshot (exactly-once, delivered before live events, return value honoured), device lifetime rules (device->next == NULL, cached DEVICE_LEFT info), deregistration post-conditions, handle non-reuse, argument validation, and Since-version annotations. Implementation gaps vs this contract are tracked as follow-up issues (see PR description). Supersedes #784. Assisted-by: claude-code:claude-fable-5 --- hidapi/hidapi.h | 259 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 221 insertions(+), 38 deletions(-) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index d7e89aabe..4c50d1503 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -263,65 +263,188 @@ extern "C" { /** @brief Callback handle. - Callbacks handles are generated by hid_hotplug_register_callback() - and can be used to deregister callbacks. Callback handles are unique - and it is safe to call hid_hotplug_deregister_callback() on - an already deregistered callback. + Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + + Callback handles are generated by hid_hotplug_register_callback() + and can be used to deregister callbacks. Callback handles are + unique and are not reused while the library remains initialized, + so it is safe to call hid_hotplug_deregister_callback() on an + already deregistered callback: it fails with -1 and never + affects another callback. + + A valid callback handle is always a positive value; + 0 is never a valid handle and may be used as a sentinel. @ingroup API */ typedef int hid_hotplug_callback_handle; - /** - Hotplug events + /** @brief Hotplug events. + + Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) @ingroup API */ typedef enum { - /** A device has been plugged in and is ready to use */ + /** A device has been plugged in and may be opened. + Opening can still fail, e.g. due to insufficient permissions + or if the device has already disconnected again. */ HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED = (1 << 0), /** A device has left and is no longer available. - It is the user's responsibility to call hid_close with a disconnected device. + If the application holds an open handle to the device, it is + still the application's responsibility to close it with + hid_close(). */ HID_API_HOTPLUG_EVENT_DEVICE_LEFT = (1 << 1) } hid_hotplug_event; - /** - Hotplug flags + /** @brief Hotplug flags. + + Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) @ingroup API */ typedef enum { - /** Arm the callback and fire it for all matching currently attached devices. */ + /** Arm the callback and fire it for all matching devices that are + already connected at registration time. + + hid_hotplug_register_callback() takes a snapshot of the + matching connected devices, and the callback is invoked + asynchronously, on the same internal event context that + delivers live events (see #hid_hotplug_callback_fn), once + for each device in that snapshot. Devices whose arrival is + detected after the snapshot is taken are reported as regular + live events: each device connection is reported to the + callback exactly once - either by the initial pass or as a + live event, never both and never neither. (A device that + disconnects and reconnects is a new connection and is + reported again.) + + The initial pass is delivered before any live events for + this callback. In particular, a callback registered with + this flag for both event types never observes a + #HID_API_HOTPLUG_EVENT_DEVICE_LEFT for a matching device + whose arrival it has not been told about first. + + These synthetic "arrived" events are delivered only when + #HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED is present in the + requested events mask. + + hid_hotplug_register_callback() returning does not imply + the initial pass has been delivered yet: the synthetic + events may fire before or after it returns. */ HID_API_HOTPLUG_ENUMERATE = (1 << 0) } hid_hotplug_flag; - /** @brief Hotplug callback function type. When requesting hotplug event notifications, - you pass a pointer to a callback function of this type. - - This callback may be called by an internal event thread and as such it is - recommended the callback do minimal processing before returning. - - hidapi will call this function later, when a matching event had happened on - a matching device. + /** @brief Hotplug callback function type. + + Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + + Called by HIDAPI when a device matching the registration filter + is connected or disconnected. See #hid_hotplug_register_callback. + + @par Execution context + + The callback is only ever invoked on HIDAPI's internal event + context, never on an application thread (including the + application's main thread). This includes the synthetic + "arrived" events requested with #HID_API_HOTPLUG_ENUMERATE: + they are delivered asynchronously on that same context and are + never delivered from within the hid_hotplug_register_callback() + call itself. (When a hotplug callback itself registers a new + callback with #HID_API_HOTPLUG_ENUMERATE, the new registration's + synthetic events are delivered later, on this same event + context.) + + Every callback invocation holds an internal hotplug mutex for + its duration: any other thread calling + hid_hotplug_register_callback() or + hid_hotplug_deregister_callback() will block until the callback + returns. The mutex is re-entrant: calling those functions from + within the callback itself (i.e. on the event context, which + already holds the mutex) does not block and cannot deadlock - + that is what makes them safe to call from inside the callback + (see below). Keep the callback short. + + When multiple callbacks are registered, each event is delivered + to every matching callback sequentially, in the order the + callbacks were registered. + + @par What the callback may call + + The hotplug API itself is thread-safe (see "Thread safety" under + #hid_hotplug_register_callback) and the following calls are always + safe from within the callback: + + - hid_hotplug_register_callback() + - hid_hotplug_deregister_callback() (including on its own handle) + - hid_error(dev) with a non-NULL device handle, provided no + other thread uses that same handle concurrently + + Any other HIDAPI function follows HIDAPI's general thread-safety + rule (see the Multi-threading Notes in the project wiki): it is + the application's responsibility to serialize hid_init / hid_exit / + hid_enumerate / hid_open* / hid_close / hid_error(NULL) across all + threads, including the hotplug callback thread (hid_exit() + additionally must never be called from within the callback + itself - see below). If your application + already calls those functions only from one thread, calling them + from the hotplug callback adds a second thread and is therefore + UNSAFE unless the application adds synchronisation itself. The + recommended pattern is to copy the needed fields of @p device out + of the callback and handle open/close on your own thread. + + Calling hid_exit() from within the callback has undefined behavior: + hid_exit() joins the hotplug thread, which would be joining itself. + + @par The device parameter + + The @p device pointer is owned by HIDAPI and is valid only for the + duration of the call. To keep any of its fields (path, + serial_number, manufacturer_string, etc.) beyond the callback, + copy them out; pointer fields must be deep-copied, as all strings + are owned by HIDAPI. + + The @p device->next pointer is always NULL. Each callback + invocation describes exactly one device; compound or composite + devices that expose multiple interfaces produce multiple callback + invocations (typically delivered in quick succession). + + For #HID_API_HOTPLUG_EVENT_DEVICE_LEFT events @p device points to + a copy captured when the device arrived (or was enumerated): all + fields, including the strings, are valid and describe the device + as it was while connected. A "left" event is delivered for any + matching device that disconnects while the callback is + registered, including devices that were already connected + before the registration (their arrival is reported to this + callback only if #HID_API_HOTPLUG_ENUMERATE was used). When the + callback has observed the device's arrival, the path field + matches the one reported then and may be used to correlate the + two events. + + @par Return value + + Return 0 to keep the callback registered. Return any non-zero + value to have HIDAPI deregister the callback; no further events + for this handle will be delivered, and the handle is freed as if + hid_hotplug_deregister_callback() had been called. This applies + to both live events and to the synthetic "arrived" events of + #HID_API_HOTPLUG_ENUMERATE: a non-zero return during the + initial pass stops the remainder of that pass and deregisters + the callback. + + @ingroup API + + @param callback_handle The handle of this callback. + @param device The hid_device_info of the device this event occurred on. + @param event Event that occurred: exactly one value (not a mask) + of \ref hid_hotplug_event. + @param user_data User data provided when this callback was registered + (may be NULL). - Note that when callbacks are called from hid_hotplug_register_callback() - because of the \ref HID_API_HOTPLUG_ENUMERATE flag, the callback return - value is ignored. In other words, you cannot cause a callback to be - deregistered by returning 1 when it is called from hid_hotplug_register_callback(). - - @ingroup API - - @param callback_handle The hid_hotplug_callback_handle callback handle. - @param device The hid_device_info of device this event occurred on event that occurred. - @param event Event that occurred. - @param user_data User data provided when this callback was registered. - (Optionally NULL). - - @returns bool - Whether this callback is finished processing events. - Returning non-zero value will cause this callback to be deregistered. + @returns + 0 to stay registered; any non-zero value to be deregistered. */ typedef int (HID_API_CALL *hid_hotplug_callback_fn)( hid_hotplug_callback_handle callback_handle, @@ -331,10 +454,41 @@ extern "C" { /** @brief Register a HID hotplug callback function. + Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + If @p vendor_id is set to 0 then any vendor matches. If @p product_id is set to 0 then any product matches. If @p vendor_id and @p product_id are both set to 0, then all HID devices will be notified. + If HIDAPI is not initialized yet, this function initializes it + implicitly (as if by hid_init()). + + When #HID_API_HOTPLUG_ENUMERATE is set, the synthetic "arrived" + events are delivered asynchronously on HIDAPI's internal event + context (see #hid_hotplug_flag): they may fire before or after + this function returns, but never from within this call itself, + and never on an application thread. + @p callback_handle (when non-NULL) is written before any events + can be delivered, and the callback always receives its own + handle as a parameter, so the callback may deregister itself + even during that initial pass. + + @par Thread safety + + hid_hotplug_register_callback() and hid_hotplug_deregister_callback() + are thread-safe. They may be called from any thread, including + from within a hotplug callback. This is a deliberate exception + to HIDAPI's general "not thread-safe" rule (see the + Multi-threading Notes in the project wiki). + + The first successful call to hid_hotplug_register_callback() + starts HIDAPI's internal hotplug machinery (on most platforms an + internal thread), which runs until either (a) the last callback + is deregistered, or (b) hid_exit() is called. hid_exit() + deregisters any callbacks that are still registered and + invalidates their handles. hid_exit() must not be called from + within a hotplug callback (see #hid_hotplug_callback_fn). + @ingroup API @param vendor_id The Vendor ID (VID) of the types of device to notify about. @@ -347,23 +501,52 @@ extern "C" { See \ref hid_hotplug_callback_fn. @param user_data The user data you wanted to provide to your callback function. @param callback_handle Pointer to store the handle of the allocated callback - (Optionally NULL). + (Optionally NULL). On failure, *callback_handle (when + non-NULL) is set to 0. @returns This function returns 0 on success or -1 on error. + Call hid_error(NULL) to get the failure reason. + Registration fails if @p callback is NULL, if @p events + contains no valid #hid_hotplug_event bit, or if @p events + or @p flags contain unknown bits. + + @note On backends without hotplug support (e.g. NetBSD) + this function always returns -1. */ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short vendor_id, unsigned short product_id, int events, int flags, hid_hotplug_callback_fn callback, void *user_data, hid_hotplug_callback_handle *callback_handle); /** @brief Deregister a callback from a HID hotplug. - This function is safe to call from within a hotplug callback. + Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + + Thread-safe. May be called from any thread, including from within + a hotplug callback (on its own handle or on another callback's + handle). Calling it on a handle that was already deregistered, + or on a handle that was never valid, is safe: it has no effect + and returns -1. + + When called from any thread other than HIDAPI's internal event + context, this function does not return until an in-progress + invocation of the callback (if any) has completed; once it + returns, the callback will not be invoked again - including any + undelivered synthetic events of #HID_API_HOTPLUG_ENUMERATE - + and it is safe to release any resources the callback uses + (e.g. whatever @p user_data points to). When called from within + a hotplug callback, the deregistered callback will not be + invoked once the currently executing invocation returns. + + See "Thread safety" on #hid_hotplug_register_callback for the + full thread-safety contract of the hotplug API. @ingroup API @param callback_handle The handle of the callback to deregister. @returns - This function returns 0 on success or -1 on error. + This function returns 0 when the callback was found and + deregistered, or -1 on error (including when + @p callback_handle is not a registered handle). */ int HID_API_EXPORT HID_API_CALL hid_hotplug_deregister_callback(hid_hotplug_callback_handle callback_handle); From 65a3235e2085fdccd45428a120fdc7234bf3a457 Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Tue, 14 Jul 2026 01:09:48 +0300 Subject: [PATCH 16/19] hotplug: fix C++ portability build across all backends The HIDAPI_BUILD_AS_CXX check (from #814, merged from master) rejected the hotplug code: out-of-order/partial designated initializers on the static hotplug contexts, int-to-enum conversions on the callback events bitmask, and an uncast calloc. Replace the static initializers with zero-initialized storage plus lazy next_handle setup, make the internal events field a plain int bitmask, and cast the remaining sites. Assisted-by: claude-code:claude-fable-5 --- libusb/hid.c | 12 ++++-------- linux/hid.c | 14 +++++--------- mac/hid.c | 18 +++++------------- windows/hid.c | 14 +++++--------- 4 files changed, 19 insertions(+), 39 deletions(-) diff --git a/libusb/hid.c b/libusb/hid.c index 291dcd76e..abf10bf8f 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -199,13 +199,7 @@ static struct hid_hotplug_context { * 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; -} hid_hotplug_context = { - .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, - .mutex_ready = 0, - .queue = NULL, - .hotplug_cbs = NULL, - .devs = NULL, -}; +} hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ uint16_t get_usb_code_for_current_locale(void); static int return_data(hid_device *dev, unsigned char *data, size_t length); @@ -725,6 +719,8 @@ static void hid_internal_hotplug_init() hid_hotplug_context.mutex_ready = 1; hid_hotplug_context.mutex_in_use = 0; hid_hotplug_context.cb_list_dirty = 0; + if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) + hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; } } @@ -1275,7 +1271,7 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic /* 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); - struct hid_hotplug_queue* msg = calloc(1, sizeof(struct hid_hotplug_queue)); + 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; diff --git a/linux/hid.c b/linux/hid.c index 5a5482b34..4d9fa38b2 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -942,20 +942,13 @@ static struct hid_hotplug_context { /* Linked list of the device infos (mandatory when the device is disconnected) */ struct hid_device_info *devs; -} hid_hotplug_context = { - .udev_ctx = NULL, - .monitor_fd = -1, - .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, - .mutex_ready = 0, - .hotplug_cbs = NULL, - .devs = NULL -}; +} hid_hotplug_context; /* zero-initialized (static storage); next_handle/monitor_fd set on first init */ struct hid_hotplug_callback { hid_hotplug_callback_handle handle; unsigned short vendor_id; unsigned short product_id; - hid_hotplug_event events; + int events; /* bitmask of hid_hotplug_event */ void *user_data; hid_hotplug_callback_fn callback; @@ -1018,6 +1011,9 @@ static void hid_internal_hotplug_init() hid_hotplug_context.mutex_ready = 1; hid_hotplug_context.mutex_in_use = 0; hid_hotplug_context.cb_list_dirty = 0; + hid_hotplug_context.monitor_fd = -1; + if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) + hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; } } diff --git a/mac/hid.c b/mac/hid.c index bc9539dd2..70951da72 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -472,7 +472,7 @@ struct hid_hotplug_callback { hid_hotplug_callback_handle handle; unsigned short vendor_id; unsigned short product_id; - hid_hotplug_event events; + int events; /* bitmask of hid_hotplug_event */ void *user_data; hid_hotplug_callback_fn callback; @@ -497,7 +497,7 @@ static struct hid_hotplug_context { CFRunLoopSourceRef source; CFStringRef run_loop_mode; pthread_barrier_t startup_barrier; /* Ensures correct startup sequence */ - int thread_state; + int thread_state; /* 0 = starting (events ignored), 1 = running (events processed), 2 = shutting down */ /* HIDAPI unique callback handle counter */ hid_hotplug_callback_handle next_handle; @@ -514,17 +514,7 @@ static struct hid_hotplug_context { /* Linked list of the device infos (mandatory when the device is disconnected) */ struct hid_device_info *devs; -} hid_hotplug_context = { - .manager = NULL, - .run_loop = NULL, - .run_loop_mode = NULL, - .source = NULL, - .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, - .mutex_ready = 0, - .thread_state = 0, /* 0 = starting (events ignored), 1 = running (events processed), 2 = shutting down */ - .hotplug_cbs = NULL, - .devs = NULL -}; +} hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ static void hid_internal_hotplug_remove_postponed(void) { @@ -593,6 +583,8 @@ static void hid_internal_hotplug_init(void) hid_hotplug_context.mutex_ready = 1; hid_hotplug_context.mutex_in_use = 0; hid_hotplug_context.cb_list_dirty = 0; + if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) + hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; } } diff --git a/windows/hid.c b/windows/hid.c index 8749f95f7..fceaaa05c 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -236,13 +236,7 @@ static struct hid_hotplug_context { /* Linked list of the device infos (mandatory when the device is disconnected) */ struct hid_device_info *devs; -} hid_hotplug_context = { - .notify_handle = NULL, - .mutex_ready = 0, - .next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE, - .hotplug_cbs = NULL, - .devs = NULL -}; +} hid_hotplug_context; /* zero-initialized (static storage); next_handle set on first init */ static hid_device *new_hid_device() { @@ -427,6 +421,8 @@ static void hid_internal_hotplug_init() hid_hotplug_context.mutex_ready = 1; hid_hotplug_context.mutex_in_use = 0; hid_hotplug_context.cb_list_dirty = 0; + if (hid_hotplug_context.next_handle < FIRST_HOTPLUG_CALLBACK_HANDLE) + hid_hotplug_context.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE; } } @@ -451,7 +447,7 @@ struct hid_hotplug_callback { hid_hotplug_callback_handle handle; unsigned short vendor_id; unsigned short product_id; - hid_hotplug_event events; + int events; /* bitmask of hid_hotplug_event */ void *user_data; hid_hotplug_callback_fn callback; @@ -1090,7 +1086,7 @@ void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *d DWORD WINAPI hid_internal_notify_callback(HCMNOTIFICATION notify, PVOID context, CM_NOTIFY_ACTION action, PCM_NOTIFY_EVENT_DATA event_data, DWORD event_data_size) { struct hid_device_info *device = NULL; - hid_hotplug_event hotplug_event = 0; + hid_hotplug_event hotplug_event = (hid_hotplug_event)0; (void)notify; (void)context; From acf58afc1ca0cae2468ab41730f06d9214b63061 Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Tue, 14 Jul 2026 20:28:17 +0300 Subject: [PATCH 17/19] hotplug docs: state the global-error caveat for the thread-safe hotplug calls hid_hotplug_register_callback() and hid_hotplug_deregister_callback() set the global error string on failure, so they must be serialized against hid_error(NULL) even though they need no serialization against each other. Also state the rule the backends implement: HIDAPI's internal threads never write the global error string, since an application cannot serialize against them. Assisted-by: claude-code:claude-opus-4-8 --- hidapi/hidapi.h | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index 4c50d1503..b28a0a46c 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -476,11 +476,22 @@ extern "C" { @par Thread safety hid_hotplug_register_callback() and hid_hotplug_deregister_callback() - are thread-safe. They may be called from any thread, including - from within a hotplug callback. This is a deliberate exception - to HIDAPI's general "not thread-safe" rule (see the + are thread-safe with respect to each other and to HIDAPI's + internal hotplug machinery. They may be called from any thread, + including from within a hotplug callback. This is a deliberate + exception to HIDAPI's general "not thread-safe" rule (see the Multi-threading Notes in the project wiki). + The one caveat is the global error string: on failure these two + functions set it, like every other HIDAPI function that reports + an error via hid_error(NULL). They therefore have to be + serialized against hid_error(NULL) - which the application is + already required to serialize across all threads - even though + they need no serialization against each other. HIDAPI's own + internal threads never write the global error string, so an + application that serializes its own hid_error(NULL) calls + against its other HIDAPI calls is safe. + The first successful call to hid_hotplug_register_callback() starts HIDAPI's internal hotplug machinery (on most platforms an internal thread), which runs until either (a) the last callback From e9567ca4c7543d5f219752b2fe7c1f63edbb67f6 Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Wed, 15 Jul 2026 01:25:14 +0300 Subject: [PATCH 18/19] hotplug docs: calls made from within a callback do not set the global error The callback runs on HIDAPI's internal event context, which never writes the global error string (an application cannot serialize against an internal thread, so writing it there is a use-after-free hazard). Return values still report failure, and hid_error(dev) still works for a device handle. Assisted-by: claude-code:claude-opus-4-8 --- hidapi/hidapi.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index b28a0a46c..2734453d9 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -382,6 +382,16 @@ extern "C" { - hid_error(dev) with a non-NULL device handle, provided no other thread uses that same handle concurrently + HIDAPI calls made from within the callback do not update the + global error string: the callback runs on HIDAPI's internal + event context, and internal contexts never write that string + (an application has no way to serialize against them, so writing + it there would be a use-after-free waiting to happen). Failures + are still reported through return values as usual, and + hid_error(dev) still works for a device handle - only + hid_error(NULL) is left untouched by calls made from the + callback. + Any other HIDAPI function follows HIDAPI's general thread-safety rule (see the Multi-threading Notes in the project wiki): it is the application's responsibility to serialize hid_init / hid_exit / From 68af3441082b9f7db06a6e9d0c5f25b3f272880c Mon Sep 17 00:00:00 2001 From: Ihor Dutchak Date: Wed, 15 Jul 2026 03:24:28 +0300 Subject: [PATCH 19/19] hotplug docs: note the unspecified invalid-arg string and the implicit-init thread binding Two clarifications surfaced by a cross-backend consistency review: when several arguments are invalid, which one the error string names is unspecified and differs between backends (only the -1 return and zeroed handle are guaranteed); and implicit hid_init() on registration binds the device-monitoring facilities to the calling thread on some backends (e.g. macOS run-loop scheduling), so an application that cares should call hid_init() from the intended thread first. Assisted-by: claude-code:claude-opus-4-8 --- hidapi/hidapi.h | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index 2734453d9..5eb1f9b52 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -471,7 +471,12 @@ extern "C" { If @p vendor_id and @p product_id are both set to 0, then all HID devices will be notified. If HIDAPI is not initialized yet, this function initializes it - implicitly (as if by hid_init()). + implicitly (as if by hid_init()). On some backends this binds + HIDAPI's device-monitoring facilities to the calling thread (for + example, the macOS backend schedules its run loop there). An + application that cares which thread owns those facilities should + call hid_init() explicitly from that thread first, rather than + relying on the implicit initialization performed here. When #HID_API_HOTPLUG_ENUMERATE is set, the synthetic "arrived" events are delivered asynchronously on HIDAPI's internal event @@ -530,7 +535,10 @@ extern "C" { Call hid_error(NULL) to get the failure reason. Registration fails if @p callback is NULL, if @p events contains no valid #hid_hotplug_event bit, or if @p events - or @p flags contain unknown bits. + or @p flags contain unknown bits. When more than one argument + is invalid, which one the failure string names is + unspecified and may differ between backends; only the -1 + return and the zeroed @p callback_handle are guaranteed. @note On backends without hotplug support (e.g. NetBSD) this function always returns -1.