Skip to content

Commit

Permalink
feat(windows): Fetch USB serial number by lookups in win registry (#1483
Browse files Browse the repository at this point in the history
)

Serial numbers in win32 from USB devices cannot be readily derived from the PnP ID.
This adds some logic to iterate through the win32 registry, as per the research in #1459.
  • Loading branch information
Per Kristian Schanke authored and reconbot committed Feb 28, 2018
1 parent 270c2be commit 45b3a2f
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 2 deletions.
2 changes: 1 addition & 1 deletion lib/bindings/win32.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class WindowsBinding extends BaseBinding {
return promisify(binding.list)().then(ports => {
// Grab the serial number from the pnp id
ports.forEach(port => {
if (port.pnpId) {
if (port.pnpId && !port.serialNumber) {
const serialNumber = serialNumParser(port.pnpId);
if (serialNumber) {
port.serialNumber = serialNumber;
Expand Down
169 changes: 168 additions & 1 deletion src/serialport_win.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
#include <string.h>
#include <windows.h>
#include <Setupapi.h>
#include <initguid.h>
#include <devpkey.h>
#include <devguid.h>
#pragma comment(lib, "setupapi.lib")

#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0]))

#define MAX_BUFFER_SIZE 1000

// As per https://msdn.microsoft.com/en-us/library/windows/desktop/ms724872(v=vs.85).aspx
#define MAX_REGISTRY_KEY_SIZE 255

// Declare type of pointer to CancelIoEx function
typedef BOOL (WINAPI *CancelIoExType)(HANDLE hFile, LPOVERLAPPED lpOverlapped);

Expand Down Expand Up @@ -610,6 +617,162 @@ NAN_METHOD(List) {
uv_queue_work(uv_default_loop(), req, EIO_List, (uv_after_work_cb)EIO_AfterList);
}

// It's possible that the s/n is a construct and not the s/n of the parent USB
// composite device. This performs some convoluted registry lookups to fetch the USB s/n.
void getSerialNumber(const char *vid,
const char *pid,
const HDEVINFO hDevInfo,
SP_DEVINFO_DATA deviceInfoData,
const unsigned int maxSerialNumberLength,
char* serialNumber) {
_snprintf_s(serialNumber, maxSerialNumberLength, _TRUNCATE, "");
if (vid == NULL || pid == NULL) {
return;
}

DWORD dwSize;
WCHAR szWUuidBuffer[MAX_BUFFER_SIZE];
WCHAR containerUuid[MAX_BUFFER_SIZE];


// Fetch the "Container ID" for this device node. In USB context, this "Container
// ID" refers to the composite USB device, i.e. the USB device as a whole, not
// just one of its interfaces with a serial port driver attached.

// From https://stackoverflow.com/questions/3438366/setupdigetdeviceproperty-usage-example:
// Because this is not compiled with UNICODE defined, the call to SetupDiGetDevicePropertyW
// has to be setup manually.
DEVPROPTYPE ulPropertyType;
typedef BOOL (WINAPI *FN_SetupDiGetDevicePropertyW)(
__in HDEVINFO DeviceInfoSet,
__in PSP_DEVINFO_DATA DeviceInfoData,
__in const DEVPROPKEY *PropertyKey,
__out DEVPROPTYPE *PropertyType,
__out_opt PBYTE PropertyBuffer,
__in DWORD PropertyBufferSize,
__out_opt PDWORD RequiredSize,
__in DWORD Flags);

FN_SetupDiGetDevicePropertyW fn_SetupDiGetDevicePropertyW = (FN_SetupDiGetDevicePropertyW)
GetProcAddress(GetModuleHandle(TEXT("Setupapi.dll")), "SetupDiGetDevicePropertyW");

if (fn_SetupDiGetDevicePropertyW (
hDevInfo,
&deviceInfoData,
&DEVPKEY_Device_ContainerId,
&ulPropertyType,
reinterpret_cast<BYTE*>(szWUuidBuffer),
sizeof(szWUuidBuffer),
&dwSize,
0)) {
szWUuidBuffer[dwSize] = '\0';

// Given the UUID bytes, build up a (widechar) string from it. There's some mangling
// going on.
StringFromGUID2((REFGUID)szWUuidBuffer, containerUuid, ARRAY_SIZE(containerUuid));
} else {
// Container UUID could not be fetched, return empty serial number.
return;
}

// NOTE: Devices might have a containerUuid like {00000000-0000-0000-FFFF-FFFFFFFFFFFF}
// This means they're non-removable, and are not handled (yet).
// Maybe they should inherit the s/n from somewhere else.

// Sanitize input - for whatever reason, StringFromGUID2() returns a WCHAR* but
// the comparisons later need a plain old char*, in lowercase ASCII.
char wantedUuid[MAX_BUFFER_SIZE];
_snprintf_s(wantedUuid, MAX_BUFFER_SIZE, _TRUNCATE, "%ws", containerUuid);
strlwr(wantedUuid);

// Iterate through all the USB devices with the given VendorID/ProductID

HKEY vendorProductHKey;
DWORD retCode;
char hkeyPath[MAX_BUFFER_SIZE];

_snprintf_s(hkeyPath, MAX_BUFFER_SIZE, _TRUNCATE, "SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_%s&PID_%s", vid, pid);

retCode = RegOpenKeyEx(
HKEY_LOCAL_MACHINE,
hkeyPath,
0,
KEY_READ,
&vendorProductHKey);

if (retCode == ERROR_SUCCESS) {
DWORD serialNumbersCount = 0; // number of subkeys

// Fetch how many subkeys there are for this VendorID/ProductID pair.
// That's the number of devices for this VendorID/ProductID known to this machine.

retCode = RegQueryInfoKey(
vendorProductHKey, // hkey handle
NULL, // buffer for class name
NULL, // size of class string
NULL, // reserved
&serialNumbersCount, // number of subkeys
NULL, // longest subkey size
NULL, // longest class string
NULL, // number of values for this key
NULL, // longest value name
NULL, // longest value data
NULL, // security descriptor
NULL); // last write time

if (retCode == ERROR_SUCCESS && serialNumbersCount > 0) {
for (unsigned int i=0; i < serialNumbersCount; i++) {
// Each of the subkeys here is the serial number of a USB device with the
// given VendorId/ProductId. Now fetch the string for the S/N.
DWORD serialNumberLength = maxSerialNumberLength;
retCode = RegEnumKeyEx(vendorProductHKey,
i,
serialNumber,
&serialNumberLength,
NULL,
NULL,
NULL,
NULL);

if (retCode == ERROR_SUCCESS) {
// Lookup info for VID_(vendorId)&PID_(productId)\(serialnumber)

_snprintf_s(hkeyPath, MAX_BUFFER_SIZE, _TRUNCATE,
"SYSTEM\\CurrentControlSet\\Enum\\USB\\VID_%s&PID_%s\\%s",
vid, pid, serialNumber);

HKEY deviceHKey;

if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, hkeyPath, 0, KEY_READ, &deviceHKey) == ERROR_SUCCESS) {
char readUuid[MAX_BUFFER_SIZE];
DWORD readSize = sizeof(readUuid);

// Query VID_(vendorId)&PID_(productId)\(serialnumber)\ContainerID
DWORD retCode = RegQueryValueEx(deviceHKey, "ContainerID", NULL, NULL, (LPBYTE)&readUuid, &readSize);
if (retCode == ERROR_SUCCESS) {
readUuid[readSize] = '\0';
if (strcmp(wantedUuid, readUuid) == 0) {
// The ContainerID UUIDs match, return now that serialNumber has
// the right value.
RegCloseKey(deviceHKey);
RegCloseKey(vendorProductHKey);
return;
}
}
}
RegCloseKey(deviceHKey);
}
}
}

/* In case we did not obtain the path, for whatever reason, we close the key and return an empty string. */
RegCloseKey(vendorProductHKey);
}

_snprintf_s(serialNumber, maxSerialNumberLength, _TRUNCATE, "");
return;
}

void EIO_List(uv_work_t* req) {
ListBaton* data = static_cast<ListBaton*>(req->data);

Expand All @@ -619,13 +782,14 @@ void EIO_List(uv_work_t* req) {

int memberIndex = 0;
DWORD dwSize, dwPropertyRegDataType;
char szBuffer[400];
char szBuffer[MAX_BUFFER_SIZE];
char *pnpId;
char *vendorId;
char *productId;
char *name;
char *manufacturer;
char *locationId;
char serialNumber[MAX_REGISTRY_KEY_SIZE];
bool isCom;
while (true) {
pnpId = NULL;
Expand Down Expand Up @@ -660,6 +824,8 @@ void EIO_List(uv_work_t* req) {
productId = copySubstring(productId, 4);
}

getSerialNumber(vendorId, productId, hDevInfo, deviceInfoData, MAX_REGISTRY_KEY_SIZE, serialNumber);

if (SetupDiGetDeviceRegistryProperty(hDevInfo, &deviceInfoData,
SPDRP_LOCATION_INFORMATION, &dwPropertyRegDataType,
reinterpret_cast<BYTE*>(szBuffer),
Expand Down Expand Up @@ -693,6 +859,7 @@ void EIO_List(uv_work_t* req) {
if (productId) {
resultItem->productId = productId;
}
resultItem->serialNumber = serialNumber;
if (locationId) {
resultItem->locationId = locationId;
}
Expand Down

0 comments on commit 45b3a2f

Please sign in to comment.