@mcesystems/usb-device-listener 1.0.4 → 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 +41 -0
- package/native/addon.cc +118 -0
- package/native/usb_listener.cc +414 -0
- package/native/usb_listener.h +113 -0
- package/package.json +13 -8
- package/src/device-filter.ts +89 -0
- package/src/examples/example.ts +60 -0
- package/src/examples/list-devices.ts +29 -0
- package/src/index.ts +134 -0
- package/src/types.ts +212 -0
- package/tsconfig.json +14 -0
- package/dist/device-filter.d.ts +0 -24
- package/dist/device-filter.d.ts.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -139
- package/dist/index.js.map +0 -7
- package/dist/types.d.ts +0 -195
- package/dist/types.d.ts.map +0 -1
- package/prebuilds/win32-x64/@mcesystems+usb-device-listener.node +0 -0
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
|
+
}
|
package/native/addon.cc
ADDED
|
@@ -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, ¬ificationFilter, 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
|