@mcesystems/usb-device-listener 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/binding.gyp ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "usb_device_listener",
5
+ "sources": [
6
+ "native/addon.cc",
7
+ "native/usb_listener.cc"
8
+ ],
9
+ "include_dirs": [
10
+ "<!@(node -p \"require('node-addon-api').include\")"
11
+ ],
12
+ "dependencies": [
13
+ "<!(node -p \"require('node-addon-api').gyp\")"
14
+ ],
15
+ "defines": [
16
+ "NAPI_DISABLE_CPP_EXCEPTIONS",
17
+ "UNICODE",
18
+ "_UNICODE"
19
+ ],
20
+ "conditions": [
21
+ [
22
+ "OS=='win'",
23
+ {
24
+ "libraries": [
25
+ "Setupapi.lib",
26
+ "Cfgmgr32.lib"
27
+ ],
28
+ "msvs_settings": {
29
+ "VCCLCompilerTool": {
30
+ "ExceptionHandling": 1,
31
+ "AdditionalOptions": [
32
+ "/std:c++20"
33
+ ]
34
+ }
35
+ }
36
+ }
37
+ ]
38
+ ]
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,118 @@
1
+ #include <napi.h>
2
+ #include "usb_listener.h"
3
+ #include <memory>
4
+
5
+ static std::unique_ptr<USBListener> g_listener;
6
+
7
+ Napi::Value StartListening(const Napi::CallbackInfo& info) {
8
+ Napi::Env env = info.Env();
9
+
10
+ if (!g_listener) {
11
+ g_listener = std::make_unique<USBListener>();
12
+ }
13
+
14
+ std::string errorMsg;
15
+ if (!g_listener->StartListening(errorMsg)) {
16
+ Napi::Error::New(env, errorMsg).ThrowAsJavaScriptException();
17
+ return env.Undefined();
18
+ }
19
+
20
+ return env.Undefined();
21
+ }
22
+
23
+ Napi::Value StopListening(const Napi::CallbackInfo& info) {
24
+ Napi::Env env = info.Env();
25
+
26
+ if (g_listener) {
27
+ g_listener->StopListening();
28
+ }
29
+
30
+ return env.Undefined();
31
+ }
32
+
33
+ Napi::Value OnDeviceAdd(const Napi::CallbackInfo& info) {
34
+ Napi::Env env = info.Env();
35
+
36
+ if (info.Length() < 1 || !info[0].IsFunction()) {
37
+ Napi::TypeError::New(env, "Callback function expected").ThrowAsJavaScriptException();
38
+ return env.Undefined();
39
+ }
40
+
41
+ Napi::Function callback = info[0].As<Napi::Function>();
42
+
43
+ if (!g_listener) {
44
+ g_listener = std::make_unique<USBListener>();
45
+ }
46
+
47
+ Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
48
+ env,
49
+ callback,
50
+ "DeviceAddCallback",
51
+ 0,
52
+ 1
53
+ );
54
+
55
+ g_listener->SetAddCallback(tsfn);
56
+
57
+ return env.Undefined();
58
+ }
59
+
60
+ Napi::Value OnDeviceRemove(const Napi::CallbackInfo& info) {
61
+ Napi::Env env = info.Env();
62
+
63
+ if (info.Length() < 1 || !info[0].IsFunction()) {
64
+ Napi::TypeError::New(env, "Callback function expected").ThrowAsJavaScriptException();
65
+ return env.Undefined();
66
+ }
67
+
68
+ Napi::Function callback = info[0].As<Napi::Function>();
69
+
70
+ if (!g_listener) {
71
+ g_listener = std::make_unique<USBListener>();
72
+ }
73
+
74
+ Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
75
+ env,
76
+ callback,
77
+ "DeviceRemoveCallback",
78
+ 0,
79
+ 1
80
+ );
81
+
82
+ g_listener->SetRemoveCallback(tsfn);
83
+
84
+ return env.Undefined();
85
+ }
86
+
87
+ Napi::Value ListDevices(const Napi::CallbackInfo& info) {
88
+ Napi::Env env = info.Env();
89
+
90
+ if (!g_listener) {
91
+ g_listener = std::make_unique<USBListener>();
92
+ }
93
+
94
+ std::vector<DeviceInfo> devices = g_listener->ListAllDevices();
95
+
96
+ Napi::Array result = Napi::Array::New(env, devices.size());
97
+ for (size_t i = 0; i < devices.size(); i++) {
98
+ Napi::Object obj = Napi::Object::New(env);
99
+ obj.Set("deviceId", Napi::String::New(env, devices[i].deviceId));
100
+ obj.Set("vid", Napi::Number::New(env, devices[i].vid));
101
+ obj.Set("pid", Napi::Number::New(env, devices[i].pid));
102
+ obj.Set("locationInfo", Napi::String::New(env, devices[i].locationInfo));
103
+ result[i] = obj;
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
110
+ exports.Set("startListening", Napi::Function::New(env, StartListening));
111
+ exports.Set("stopListening", Napi::Function::New(env, StopListening));
112
+ exports.Set("onDeviceAdd", Napi::Function::New(env, OnDeviceAdd));
113
+ exports.Set("onDeviceRemove", Napi::Function::New(env, OnDeviceRemove));
114
+ exports.Set("listDevices", Napi::Function::New(env, ListDevices));
115
+ return exports;
116
+ }
117
+
118
+ NODE_API_MODULE(usb_device_listener, Init)
@@ -0,0 +1,414 @@
1
+ #include "usb_listener.h"
2
+ #include <devguid.h>
3
+ #include <usbiodef.h> // For GUID_DEVINTERFACE_USB_DEVICE
4
+ #include <sstream>
5
+ #include <iomanip>
6
+ #include <algorithm>
7
+
8
+ USBListener::USBListener() = default;
9
+
10
+ USBListener::~USBListener() {
11
+ StopListening();
12
+ }
13
+
14
+ /**
15
+ * Start the USB device listener
16
+ * Creates a separate thread that runs a Windows message loop to receive device notifications
17
+ */
18
+ bool USBListener::StartListening(std::string& errorMsg) {
19
+ bool expected = false;
20
+ if (!m_running.compare_exchange_strong(expected, true)) {
21
+ errorMsg = "Listener already running";
22
+ return false;
23
+ }
24
+
25
+ // Create thread with RAII wrapper
26
+ HANDLE threadHandle = CreateThread(nullptr, 0, ListenerThreadProc, this, 0, nullptr);
27
+ if (!threadHandle) {
28
+ m_running = false;
29
+ errorMsg = "Failed to create listener thread";
30
+ return false;
31
+ }
32
+ m_thread = ThreadHandle(threadHandle);
33
+
34
+ // Brief wait for thread initialization
35
+ Sleep(100);
36
+ EnumerateConnectedDevices();
37
+
38
+ return true;
39
+ }
40
+
41
+ void USBListener::StopListening() {
42
+ bool expected = true;
43
+ if (!m_running.compare_exchange_strong(expected, false)) {
44
+ return; // Already stopped
45
+ }
46
+
47
+ // Signal message loop to quit
48
+ if (m_hwnd) {
49
+ PostMessage(m_hwnd, WM_QUIT, 0, 0);
50
+ }
51
+
52
+ // Thread cleanup is automatic via ThreadHandle RAII
53
+ // Destructor will wait and close handle
54
+ m_thread = ThreadHandle(); // Reset to trigger cleanup
55
+
56
+ // Release ThreadSafeFunction callbacks
57
+ if (m_addCallback) {
58
+ m_addCallback.Release();
59
+ }
60
+ if (m_removeCallback) {
61
+ m_removeCallback.Release();
62
+ }
63
+
64
+ // Clear device cache
65
+ std::unique_lock lock(m_cacheMutex);
66
+ m_deviceCache.clear();
67
+ }
68
+
69
+ void USBListener::SetAddCallback(Napi::ThreadSafeFunction callback) {
70
+ m_addCallback = callback;
71
+ }
72
+
73
+ void USBListener::SetRemoveCallback(Napi::ThreadSafeFunction callback) {
74
+ m_removeCallback = callback;
75
+ }
76
+
77
+ DWORD WINAPI USBListener::ListenerThreadProc(LPVOID param) {
78
+ USBListener* listener = static_cast<USBListener*>(param);
79
+ listener->MessageLoop();
80
+ return 0;
81
+ }
82
+
83
+ void USBListener::MessageLoop() {
84
+ WNDCLASSEX wc = {0};
85
+ wc.cbSize = sizeof(WNDCLASSEX);
86
+ wc.lpfnWndProc = WindowProc;
87
+ wc.hInstance = GetModuleHandle(nullptr);
88
+ wc.lpszClassName = L"USBListenerWindow";
89
+
90
+ if (!RegisterClassEx(&wc)) {
91
+ return;
92
+ }
93
+
94
+ m_hwnd = CreateWindowEx(0, L"USBListenerWindow", L"USBListener", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, GetModuleHandle(nullptr), nullptr);
95
+ if (!m_hwnd) {
96
+ return;
97
+ }
98
+
99
+ SetWindowLongPtr(m_hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
100
+
101
+ DEV_BROADCAST_DEVICEINTERFACE notificationFilter = {0};
102
+ notificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
103
+ notificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
104
+ notificationFilter.dbcc_classguid = GUID_DEVINTERFACE_USB_DEVICE;
105
+
106
+ m_notifyHandle = RegisterDeviceNotification(m_hwnd, &notificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
107
+
108
+ MSG msg;
109
+ while (m_running && GetMessage(&msg, nullptr, 0, 0)) {
110
+ TranslateMessage(&msg);
111
+ DispatchMessage(&msg);
112
+ }
113
+
114
+ if (m_notifyHandle) {
115
+ UnregisterDeviceNotification(m_notifyHandle);
116
+ m_notifyHandle = nullptr;
117
+ }
118
+
119
+ if (m_hwnd) {
120
+ DestroyWindow(m_hwnd);
121
+ m_hwnd = nullptr;
122
+ }
123
+
124
+ UnregisterClass(L"USBListenerWindow", GetModuleHandle(nullptr));
125
+ }
126
+
127
+ LRESULT CALLBACK USBListener::WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
128
+ if (msg == WM_DEVICECHANGE) {
129
+ USBListener* listener = reinterpret_cast<USBListener*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
130
+ if (listener) {
131
+ listener->HandleDeviceChange(wParam, lParam);
132
+ }
133
+ }
134
+ return DefWindowProc(hwnd, msg, wParam, lParam);
135
+ }
136
+
137
+ /**
138
+ * Handle WM_DEVICECHANGE messages
139
+ * For connections: queries Windows API for full device info
140
+ * For disconnections: retrieves cached info since device is already gone
141
+ */
142
+ void USBListener::HandleDeviceChange(WPARAM wParam, LPARAM lParam) {
143
+ // Only handle device arrival and removal events
144
+ if (wParam != DBT_DEVICEARRIVAL && wParam != DBT_DEVICEREMOVECOMPLETE) {
145
+ return;
146
+ }
147
+
148
+ // Extract device interface information
149
+ DEV_BROADCAST_DEVICEINTERFACE* devInterface = reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACE*>(lParam);
150
+ if (!devInterface || devInterface->dbcc_devicetype != DBT_DEVTYP_DEVICEINTERFACE) {
151
+ return;
152
+ }
153
+
154
+ // Convert device path to string
155
+ std::wstring devicePathW = devInterface->dbcc_name;
156
+ std::string devicePath(devicePathW.begin(), devicePathW.end());
157
+
158
+ DeviceInfo info;
159
+ bool gotInfo = false;
160
+
161
+ if (wParam == DBT_DEVICEARRIVAL) {
162
+ // For connections, query Windows API for full device details
163
+ gotInfo = GetDeviceInfo(devicePath, info);
164
+ } else if (wParam == DBT_DEVICEREMOVECOMPLETE) {
165
+ // For disconnections, device is gone - retrieve from cache
166
+ DeviceInfo pathInfo;
167
+ if (GetDeviceInfoFromPath(devicePath, pathInfo)) {
168
+ std::unique_lock lock(m_cacheMutex); // Exclusive lock for write
169
+ if (auto it = m_deviceCache.find(pathInfo.deviceId); it != m_deviceCache.end()) {
170
+ info = std::move(it->second); // Move for efficiency
171
+ m_deviceCache.erase(it);
172
+ gotInfo = true;
173
+ }
174
+ }
175
+ }
176
+
177
+ if (!gotInfo) {
178
+ return;
179
+ }
180
+
181
+ if (wParam == DBT_DEVICEARRIVAL && m_addCallback) {
182
+ // Cache device info for disconnect events
183
+ {
184
+ std::unique_lock lock(m_cacheMutex); // Exclusive lock for write
185
+ m_deviceCache[info.deviceId] = info;
186
+ }
187
+ auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
188
+ Napi::Object obj = Napi::Object::New(env);
189
+ obj.Set("deviceId", Napi::String::New(env, data->deviceId));
190
+ obj.Set("vid", Napi::Number::New(env, data->vid));
191
+ obj.Set("pid", Napi::Number::New(env, data->pid));
192
+ obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
193
+ if (data->logicalPort >= 0) {
194
+ obj.Set("logicalPort", Napi::Number::New(env, data->logicalPort));
195
+ } else {
196
+ obj.Set("logicalPort", env.Null());
197
+ }
198
+ jsCallback.Call({obj});
199
+ delete data;
200
+ };
201
+ m_addCallback.BlockingCall(new DeviceInfo(info), callback);
202
+ } else if (wParam == DBT_DEVICEREMOVECOMPLETE && m_removeCallback) {
203
+ auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
204
+ Napi::Object obj = Napi::Object::New(env);
205
+ obj.Set("deviceId", Napi::String::New(env, data->deviceId));
206
+ obj.Set("vid", Napi::Number::New(env, data->vid));
207
+ obj.Set("pid", Napi::Number::New(env, data->pid));
208
+ obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
209
+ if (data->logicalPort >= 0) {
210
+ obj.Set("logicalPort", Napi::Number::New(env, data->logicalPort));
211
+ } else {
212
+ obj.Set("logicalPort", env.Null());
213
+ }
214
+ jsCallback.Call({obj});
215
+ delete data;
216
+ };
217
+ m_removeCallback.BlockingCall(new DeviceInfo(info), callback);
218
+ }
219
+ }
220
+
221
+ bool USBListener::GetDeviceInfoFromPath(const std::string& devicePath, DeviceInfo& info) {
222
+ size_t hashPos = devicePath.rfind('#');
223
+ if (hashPos == std::string::npos) {
224
+ return false;
225
+ }
226
+
227
+ std::string deviceIdStr = devicePath.substr(4, hashPos - 4);
228
+ std::replace(deviceIdStr.begin(), deviceIdStr.end(), '#', '\\');
229
+
230
+ info.deviceId = deviceIdStr;
231
+
232
+ if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
233
+ return false;
234
+ }
235
+
236
+ size_t lastHash = deviceIdStr.rfind('\\');
237
+ if (lastHash != std::string::npos) {
238
+ info.locationInfo = deviceIdStr.substr(lastHash + 1);
239
+ } else {
240
+ info.locationInfo = "";
241
+ }
242
+
243
+ return true;
244
+ }
245
+
246
+ bool USBListener::GetDeviceInfo(const std::string& devicePath, DeviceInfo& info) {
247
+ size_t hashPos = devicePath.rfind('#');
248
+ if (hashPos == std::string::npos) {
249
+ return false;
250
+ }
251
+
252
+ std::string deviceIdStr = devicePath.substr(4, hashPos - 4);
253
+ std::replace(deviceIdStr.begin(), deviceIdStr.end(), '#', '\\');
254
+
255
+ std::wstring deviceIdW(deviceIdStr.begin(), deviceIdStr.end());
256
+
257
+ HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
258
+ if (deviceInfoSet == INVALID_HANDLE_VALUE) {
259
+ return false;
260
+ }
261
+
262
+ SP_DEVINFO_DATA devInfoData = {0};
263
+ devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
264
+
265
+ for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &devInfoData); i++) {
266
+ WCHAR instanceId[MAX_PATH];
267
+ if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
268
+ if (_wcsicmp(instanceId, deviceIdW.c_str()) == 0) {
269
+ std::wstring idW = instanceId;
270
+ info.deviceId = std::string(idW.begin(), idW.end());
271
+
272
+ if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
273
+ SetupDiDestroyDeviceInfoList(deviceInfoSet);
274
+ return false;
275
+ }
276
+
277
+ DEVINST devInst = devInfoData.DevInst;
278
+ if (!GetLocationInfo(devInst, info.locationInfo)) {
279
+ SetupDiDestroyDeviceInfoList(deviceInfoSet);
280
+ return false;
281
+ }
282
+
283
+ SetupDiDestroyDeviceInfoList(deviceInfoSet);
284
+ return true;
285
+ }
286
+ }
287
+ }
288
+
289
+ SetupDiDestroyDeviceInfoList(deviceInfoSet);
290
+ return false;
291
+ }
292
+
293
+ bool USBListener::GetLocationInfo(DEVINST devInst, std::string& locationInfo) {
294
+ WCHAR buffer[MAX_PATH];
295
+ ULONG size = sizeof(buffer);
296
+
297
+ CONFIGRET ret = CM_Get_DevNode_Registry_Property(devInst, CM_DRP_LOCATION_INFORMATION, nullptr, buffer, &size, 0);
298
+ if (ret != CR_SUCCESS) {
299
+ return false;
300
+ }
301
+
302
+ std::wstring locationW = buffer;
303
+ locationInfo = std::string(locationW.begin(), locationW.end());
304
+ return true;
305
+ }
306
+
307
+ bool USBListener::GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t& pid) {
308
+ size_t vidPos = deviceId.find("VID_");
309
+ size_t pidPos = deviceId.find("PID_");
310
+
311
+ if (vidPos == std::string::npos || pidPos == std::string::npos) {
312
+ return false;
313
+ }
314
+
315
+ try {
316
+ std::string vidStr = deviceId.substr(vidPos + 4, 4);
317
+ std::string pidStr = deviceId.substr(pidPos + 4, 4);
318
+
319
+ vid = static_cast<uint16_t>(std::stoul(vidStr, nullptr, 16));
320
+ pid = static_cast<uint16_t>(std::stoul(pidStr, nullptr, 16));
321
+ return true;
322
+ } catch (...) {
323
+ return false;
324
+ }
325
+ }
326
+
327
+ void USBListener::EnumerateConnectedDevices() {
328
+ HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
329
+ if (deviceInfoSet == INVALID_HANDLE_VALUE) {
330
+ return;
331
+ }
332
+
333
+ SP_DEVINFO_DATA devInfoData = {0};
334
+ devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
335
+
336
+ for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &devInfoData); i++) {
337
+ WCHAR instanceId[MAX_PATH];
338
+ if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
339
+ DeviceInfo info;
340
+ std::wstring idW = instanceId;
341
+ info.deviceId = std::string(idW.begin(), idW.end());
342
+
343
+ if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
344
+ continue;
345
+ }
346
+
347
+ DEVINST devInst = devInfoData.DevInst;
348
+ if (!GetLocationInfo(devInst, info.locationInfo)) {
349
+ continue;
350
+ }
351
+
352
+ // Cache device with exclusive lock
353
+ {
354
+ std::unique_lock lock(m_cacheMutex);
355
+ m_deviceCache[info.deviceId] = info;
356
+ }
357
+
358
+ if (m_addCallback) {
359
+ auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
360
+ Napi::Object obj = Napi::Object::New(env);
361
+ obj.Set("deviceId", Napi::String::New(env, data->deviceId));
362
+ obj.Set("vid", Napi::Number::New(env, data->vid));
363
+ obj.Set("pid", Napi::Number::New(env, data->pid));
364
+ obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
365
+ if (data->logicalPort >= 0) {
366
+ obj.Set("logicalPort", Napi::Number::New(env, data->logicalPort));
367
+ } else {
368
+ obj.Set("logicalPort", env.Null());
369
+ }
370
+ jsCallback.Call({obj});
371
+ delete data;
372
+ };
373
+ m_addCallback.BlockingCall(new DeviceInfo(info), callback);
374
+ }
375
+ }
376
+ }
377
+
378
+ SetupDiDestroyDeviceInfoList(deviceInfoSet);
379
+ }
380
+
381
+ std::vector<DeviceInfo> USBListener::ListAllDevices() {
382
+ std::vector<DeviceInfo> devices;
383
+
384
+ HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
385
+ if (deviceInfoSet == INVALID_HANDLE_VALUE) {
386
+ return devices;
387
+ }
388
+
389
+ SP_DEVINFO_DATA devInfoData = {0};
390
+ devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
391
+
392
+ for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &devInfoData); i++) {
393
+ WCHAR instanceId[MAX_PATH];
394
+ if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
395
+ DeviceInfo info;
396
+ std::wstring idW = instanceId;
397
+ info.deviceId = std::string(idW.begin(), idW.end());
398
+
399
+ if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
400
+ continue;
401
+ }
402
+
403
+ DEVINST devInst = devInfoData.DevInst;
404
+ if (!GetLocationInfo(devInst, info.locationInfo)) {
405
+ continue;
406
+ }
407
+
408
+ devices.push_back(info);
409
+ }
410
+ }
411
+
412
+ SetupDiDestroyDeviceInfoList(deviceInfoSet);
413
+ return devices;
414
+ }
@@ -0,0 +1,113 @@
1
+ #ifndef USB_LISTENER_H
2
+ #define USB_LISTENER_H
3
+
4
+ #include <napi.h>
5
+ #include <windows.h>
6
+ #include <initguid.h>
7
+ #include <setupapi.h>
8
+ #include <cfgmgr32.h>
9
+ #include <dbt.h>
10
+ #include <string>
11
+ #include <vector>
12
+ #include <unordered_map>
13
+ #include <memory>
14
+ #include <atomic>
15
+ #include <shared_mutex>
16
+
17
+ /**
18
+ * Device information structure
19
+ * Contains all relevant USB device identification and location data
20
+ */
21
+ struct DeviceInfo {
22
+ std::string deviceId; // Windows device instance ID
23
+ uint16_t vid{0}; // Vendor ID
24
+ uint16_t pid{0}; // Product ID
25
+ std::string locationInfo; // Physical port location
26
+ int logicalPort{-1}; // User-defined logical port number (-1 if not mapped)
27
+ };
28
+
29
+ /**
30
+ * RAII wrapper for Windows thread handle
31
+ */
32
+ class ThreadHandle {
33
+ public:
34
+ ThreadHandle() noexcept = default;
35
+ explicit ThreadHandle(HANDLE h) noexcept : handle(h) {}
36
+ ~ThreadHandle() { if (handle) { WaitForSingleObject(handle, 5000); CloseHandle(handle); } }
37
+
38
+ ThreadHandle(const ThreadHandle&) = delete;
39
+ ThreadHandle& operator=(const ThreadHandle&) = delete;
40
+ ThreadHandle(ThreadHandle&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {}
41
+ ThreadHandle& operator=(ThreadHandle&& other) noexcept {
42
+ if (this != &other) {
43
+ if (handle) { WaitForSingleObject(handle, 5000); CloseHandle(handle); }
44
+ handle = std::exchange(other.handle, nullptr);
45
+ }
46
+ return *this;
47
+ }
48
+
49
+ HANDLE get() const noexcept { return handle; }
50
+ explicit operator bool() const noexcept { return handle != nullptr; }
51
+
52
+ private:
53
+ HANDLE handle{nullptr};
54
+ };
55
+
56
+ /**
57
+ * USB Device Listener - Windows native implementation
58
+ *
59
+ * Monitors USB device connection/disconnection events using Windows WM_DEVICECHANGE messages.
60
+ * Runs a hidden message-only window in a separate thread to receive device notifications.
61
+ * Thread-safe: callbacks are invoked on the Node.js thread using Napi::ThreadSafeFunction.
62
+ */
63
+ class USBListener {
64
+ public:
65
+ USBListener();
66
+ ~USBListener();
67
+
68
+ // Start monitoring USB device events
69
+ bool StartListening(std::string& errorMsg);
70
+
71
+ // Stop monitoring and clean up resources
72
+ void StopListening();
73
+
74
+ // Set callback for device connection events
75
+ void SetAddCallback(Napi::ThreadSafeFunction callback);
76
+
77
+ // Set callback for device disconnection events
78
+ void SetRemoveCallback(Napi::ThreadSafeFunction callback);
79
+
80
+ // Get list of all currently connected USB devices
81
+ std::vector<DeviceInfo> ListAllDevices();
82
+
83
+ private:
84
+ // Windows message handling
85
+ static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
86
+ static DWORD WINAPI ListenerThreadProc(LPVOID param);
87
+
88
+ // Core functionality
89
+ void MessageLoop(); // Main message loop in listener thread
90
+ void HandleDeviceChange(WPARAM wParam, LPARAM lParam); // Process WM_DEVICECHANGE messages
91
+ bool GetDeviceInfo(const std::string& devicePath, DeviceInfo& info); // Get device info from Windows API (for connections)
92
+ bool GetDeviceInfoFromPath(const std::string& devicePath, DeviceInfo& info); // Parse device info from path (for disconnections)
93
+ bool GetLocationInfo(DEVINST devInst, std::string& locationInfo); // Get physical port location
94
+ bool GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t& pid); // Parse VID/PID from device ID string
95
+ void EnumerateConnectedDevices(); // Enumerate devices on startup
96
+
97
+ // Windows handles (managed resources)
98
+ HWND m_hwnd{nullptr}; // Hidden message-only window
99
+ HDEVNOTIFY m_notifyHandle{nullptr}; // Device notification handle
100
+ ThreadHandle m_thread; // Listener thread handle (RAII)
101
+ std::atomic<bool> m_running{false}; // Thread control flag
102
+
103
+ // Device cache with shared_mutex for reader-writer lock pattern
104
+ // Multiple readers (enumerate) or single writer (add/remove)
105
+ mutable std::shared_mutex m_cacheMutex; // C++17 shared_mutex for better concurrency
106
+ std::unordered_map<std::string, DeviceInfo> m_deviceCache; // Fast lookup with hash
107
+
108
+ // Callbacks
109
+ Napi::ThreadSafeFunction m_addCallback; // Device add callback
110
+ Napi::ThreadSafeFunction m_removeCallback; // Device remove callback
111
+ };
112
+
113
+ #endif // USB_LISTENER_H