From 1b0b6acce5505aaa66b550f648c7662a03a53f7e Mon Sep 17 00:00:00 2001 From: Dimitriy Ryazantcev Date: Mon, 1 May 2023 14:20:08 +0300 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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; } }