|
25 | 25 |
|
26 | 26 | #include <stdio.h> |
27 | 27 | #include <dbt.h> |
28 | | -#include <usbiodef.h> |
29 | | - |
30 | | -/* The Windows Hotplug system is a three steps process. |
31 | | - * 1. We create a monitor on GUID_DEVINTERFACE_USB_DEVICE via a hidden window. |
32 | | - * 2. Upon notification of an event, we run the current windows backend to get |
33 | | - * the list of all devices. |
34 | | - * 3. We use PDEV_BROADCAST_DEVICEINTERFACE->dbcc_name for finding device object |
35 | | - * in this list and generate LEFT or ARRIVED events libusb client via hotplug callbacks |
| 28 | + |
| 29 | +/* The Windows Hotplug system is a two steps process. |
| 30 | + * 1. We create a hidden window and listen for DBT_DEVNODES_CHANGED, which Windows |
| 31 | + * broadcasts to all top-level windows whenever the device tree changes (no |
| 32 | + * registration required). Multiple rapid events (e.g. a hub with many children) |
| 33 | + * are coalesced via a short debounce timer so we only scan once the burst settles. |
| 34 | + * A maximum delay ceiling guarantees a scan fires within a bounded time even |
| 35 | + * during sustained bursts, preventing unbounded latency. |
| 36 | + * 2. Upon timer expiry, we snapshot the current device list, run a full re-enumeration |
| 37 | + * via the Windows backend, then diff the result: newly found devices that have been |
| 38 | + * successfully initialized generate DEVICE_ARRIVED events; devices that were not |
| 39 | + * encountered by the re-enumeration (physically removed) generate DEVICE_LEFT events. |
36 | 40 | */ |
37 | 41 |
|
| 42 | +#define HOTPLUG_DEBOUNCE_TIMER_ID 1 |
| 43 | +#define HOTPLUG_DEBOUNCE_MS 10 |
| 44 | +#define HOTPLUG_DEBOUNCE_MAX_DELAY_MS 100 |
| 45 | + |
| 46 | +static ULONGLONG first_debounce_tick; |
38 | 47 | static HWND windows_event_hwnd; |
39 | 48 | static HANDLE windows_event_thread_handle; |
40 | 49 | static DWORD WINAPI windows_event_thread_main(LPVOID lpParam); |
@@ -110,52 +119,65 @@ void windows_initial_scan_devices(struct libusb_context *ctx) |
110 | 119 | usbi_mutex_static_unlock(&active_contexts_lock); |
111 | 120 | } |
112 | 121 |
|
113 | | -static void windows_refresh_device_list(struct libusb_context *ctx, const bool device_arrived, const char* device_name) |
| 122 | +static void windows_refresh_device_list(struct libusb_context *ctx) |
114 | 123 | { |
| 124 | + struct libusb_device *dev, *next_dev; |
| 125 | + |
| 126 | + // Step 1: clear seen_during_scan so the scan can mark which devices are still |
| 127 | + // physically present, and set seen_before_scan so we can distinguish newly |
| 128 | + // created devices (which start with seen_before_scan=false via calloc). |
| 129 | + for_each_device_safe(ctx, dev, next_dev) |
| 130 | + { |
| 131 | + struct winusb_device_priv *priv = (struct winusb_device_priv *)usbi_get_device_priv(dev); |
| 132 | + priv->seen_during_scan = false; |
| 133 | + priv->seen_before_scan = true; |
| 134 | + } |
| 135 | + |
| 136 | + // Step 2: re-enumerate — winusb_get_device_list attaches newly-arrived devices |
| 137 | + // and sets seen_during_scan=true for every device it physically encounters. |
| 138 | + // seen_before_scan is untouched and will be left in default state (false) for newly created devices, allowing us to identify them in the next step. |
115 | 139 | const int ret = windows_get_device_list(ctx); |
116 | 140 | if (ret != LIBUSB_SUCCESS) |
117 | 141 | { |
118 | 142 | usbi_err(ctx, "hotplug failed to retrieve current list with error: %s", libusb_error_name(ret)); |
119 | 143 | return; |
120 | 144 | } |
121 | 145 |
|
122 | | - struct libusb_device *dev, *next_dev; |
123 | | - |
| 146 | + // Step 3: diff old vs new. |
124 | 147 | for_each_device_safe(ctx, dev, next_dev) |
125 | 148 | { |
126 | | - struct winusb_device_priv* priv = usbi_get_device_priv(dev); |
127 | | - |
128 | | - if(priv->path == NULL || _stricmp(priv->path, device_name) != 0) |
129 | | - { |
130 | | - continue; |
131 | | - } |
| 149 | + struct winusb_device_priv *priv = (struct winusb_device_priv *)usbi_get_device_priv(dev); |
132 | 150 |
|
133 | | - if(device_arrived) |
134 | | - { |
135 | | - usbi_hotplug_notification(ctx, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED); |
136 | | - } |
137 | | - else |
| 151 | + if (!priv->seen_during_scan) |
138 | 152 | { |
| 153 | + // Not encountered by the scan: device was physically removed. |
139 | 154 | if (priv->initialized) |
140 | 155 | { |
141 | | - usbi_disconnect_device(dev); |
| 156 | + usbi_disconnect_device(dev); // fires DEVICE_LEFT |
142 | 157 | } |
143 | 158 | else |
144 | 159 | { |
145 | 160 | usbi_detach_device(dev); |
146 | 161 | } |
147 | 162 | } |
| 163 | + else if (!priv->seen_before_scan) |
| 164 | + { |
| 165 | + if (priv->initialized) |
| 166 | + { |
| 167 | + usbi_hotplug_notification(ctx, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED); |
| 168 | + } |
| 169 | + } |
148 | 170 | } |
149 | 171 | } |
150 | 172 |
|
151 | | -static void windows_refresh_device_list_for_all_ctx(const bool device_arrived, const char* device_name) |
| 173 | +static void windows_refresh_device_list_for_all_ctx(void) |
152 | 174 | { |
153 | 175 | usbi_mutex_static_lock(&active_contexts_lock); |
154 | 176 |
|
155 | 177 | struct libusb_context *ctx; |
156 | 178 | for_each_context(ctx) |
157 | 179 | { |
158 | | - windows_refresh_device_list(ctx, device_arrived, device_name); |
| 180 | + windows_refresh_device_list(ctx); |
159 | 181 | } |
160 | 182 |
|
161 | 183 | usbi_mutex_static_unlock(&active_contexts_lock); |
@@ -232,107 +254,53 @@ static DWORD WINAPI windows_event_thread_main(LPVOID lpParam) |
232 | 254 | return 0; |
233 | 255 | } |
234 | 256 |
|
235 | | -static bool register_device_interface_to_window_handle( |
236 | | - IN GUID interface_class_guid, |
237 | | - IN HWND hwnd, |
238 | | - OUT HDEVNOTIFY* device_notify_handle) |
239 | | -{ |
240 | | - DEV_BROADCAST_DEVICEINTERFACE notificationFilter = { 0 }; |
241 | | - notificationFilter.dbcc_size = sizeof(notificationFilter); |
242 | | - notificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; |
243 | | - notificationFilter.dbcc_classguid = interface_class_guid; |
244 | | - |
245 | | - *device_notify_handle = RegisterDeviceNotification( |
246 | | - hwnd, |
247 | | - ¬ificationFilter, |
248 | | - DEVICE_NOTIFY_WINDOW_HANDLE |
249 | | - ); |
250 | | - |
251 | | - if (*device_notify_handle == NULL) |
252 | | - { |
253 | | - log_error("register_device_interface_to_window_handle"); |
254 | | - return false; |
255 | | - } |
256 | | - |
257 | | - return true; |
258 | | -} |
259 | | - |
260 | 257 | static LRESULT CALLBACK windows_proc_callback( |
261 | 258 | HWND hwnd, |
262 | 259 | UINT message, |
263 | 260 | WPARAM wParam, |
264 | 261 | LPARAM lParam) |
265 | 262 | { |
266 | | - UNUSED(lParam); |
267 | | - |
268 | | - static HDEVNOTIFY device_notify_handle; |
269 | | - |
270 | 263 | switch (message) |
271 | 264 | { |
272 | | - case WM_CREATE: |
273 | | - if (!register_device_interface_to_window_handle( |
274 | | - GUID_DEVINTERFACE_USB_DEVICE, |
275 | | - hwnd, |
276 | | - &device_notify_handle)) |
277 | | - { |
278 | | - return -1; |
279 | | - } |
280 | | - return 0; |
281 | | - |
282 | 265 | case WM_DEVICECHANGE: |
283 | | - switch (wParam) |
| 266 | + if (wParam == DBT_DEVNODES_CHANGED) |
284 | 267 | { |
285 | | - case DBT_DEVICEARRIVAL: |
286 | | - case DBT_DEVICEREMOVECOMPLETE: |
287 | | - if (((PDEV_BROADCAST_HDR)lParam)->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) |
| 268 | + if (first_debounce_tick == 0) |
288 | 269 | { |
289 | | -#ifdef UNICODE |
290 | | - char* device_name = NULL; |
291 | | - const WCHAR* w_dbcc_name = ((PDEV_BROADCAST_DEVICEINTERFACE)lParam)->dbcc_name; |
292 | | - |
293 | | - const int len = WideCharToMultiByte(CP_UTF8, 0, w_dbcc_name, -1, NULL, 0, NULL, NULL); |
294 | | - if (len == 0) |
295 | | - { |
296 | | - log_error("Conversion length calculation failed for ((PDEV_BROADCAST_DEVICEINTERFACE)lParam)->dbcc_name conversion from wchar to char"); |
297 | | - } |
298 | | - else |
299 | | - { |
300 | | - device_name = (char *)malloc(len); |
301 | | - if (device_name == NULL) |
302 | | - { |
303 | | - log_error("Memory allocation failed for ((PDEV_BROADCAST_DEVICEINTERFACE)lParam)->dbcc_name conversion from wchar to char"); |
304 | | - } |
305 | | - else |
306 | | - { |
307 | | - const int result = WideCharToMultiByte(CP_UTF8, 0, w_dbcc_name, -1, device_name, len, NULL, NULL); |
308 | | - if (result == 0) |
309 | | - { |
310 | | - log_error("Conversion failed for ((PDEV_BROADCAST_DEVICEINTERFACE)lParam)->dbcc_name conversion from wchar to char"); |
311 | | - free(device_name); |
312 | | - device_name = NULL; |
313 | | - } |
314 | | - } |
315 | | - } |
316 | | -#else |
317 | | - const char* device_name = ((PDEV_BROADCAST_DEVICEINTERFACE)lParam)->dbcc_name; |
318 | | -#endif |
319 | | - |
320 | | - windows_refresh_device_list_for_all_ctx(wParam == DBT_DEVICEARRIVAL ? true : false, device_name); |
321 | | - |
322 | | -#ifdef UNICODE |
323 | | - free(device_name); |
324 | | -#endif |
325 | | - return TRUE; |
| 270 | + // First event in a new burst — record the time and start the debounce timer. |
| 271 | + first_debounce_tick = GetTickCount64(); |
| 272 | + SetTimer(hwnd, HOTPLUG_DEBOUNCE_TIMER_ID, HOTPLUG_DEBOUNCE_MS, NULL); |
326 | 273 | } |
327 | | - break; |
| 274 | + else if (GetTickCount64() - first_debounce_tick >= HOTPLUG_DEBOUNCE_MAX_DELAY_MS) |
| 275 | + { |
| 276 | + // Maximum delay reached — force an immediate scan so devices |
| 277 | + // are not invisible for the entire duration of a sustained burst. |
| 278 | + KillTimer(hwnd, HOTPLUG_DEBOUNCE_TIMER_ID); |
| 279 | + first_debounce_tick = 0; |
| 280 | + windows_refresh_device_list_for_all_ctx(); |
| 281 | + } |
| 282 | + else |
| 283 | + { |
| 284 | + // Still within the max-delay window — reset the debounce timer. |
| 285 | + SetTimer(hwnd, HOTPLUG_DEBOUNCE_TIMER_ID, HOTPLUG_DEBOUNCE_MS, NULL); |
| 286 | + } |
| 287 | + return TRUE; |
328 | 288 | } |
329 | | - return BROADCAST_QUERY_DENY; |
| 289 | + return DefWindowProc(hwnd, message, wParam, lParam); |
330 | 290 |
|
331 | | - case WM_CLOSE: |
332 | | - if (!UnregisterDeviceNotification(device_notify_handle)) |
| 291 | + case WM_TIMER: |
| 292 | + if (wParam == HOTPLUG_DEBOUNCE_TIMER_ID) |
333 | 293 | { |
334 | | - log_error("UnregisterDeviceNotification"); |
| 294 | + KillTimer(hwnd, HOTPLUG_DEBOUNCE_TIMER_ID); |
| 295 | + first_debounce_tick = 0; |
| 296 | + windows_refresh_device_list_for_all_ctx(); |
| 297 | + return 0; |
335 | 298 | } |
| 299 | + return DefWindowProc(hwnd, message, wParam, lParam); |
| 300 | + |
| 301 | + case WM_CLOSE: |
| 302 | + KillTimer(hwnd, HOTPLUG_DEBOUNCE_TIMER_ID); |
| 303 | + first_debounce_tick = 0; |
336 | 304 | if (!DestroyWindow(hwnd)) |
337 | 305 | { |
338 | 306 | log_error("DestroyWindow"); |
|
0 commit comments