1、前言
熟悉Windows系统的都应该使用过设备管理器。设备管理器将操作系统中所有已安装的设备分类展现出来。同时提供了安装、卸载、启用和禁用的功能。
那么,我们应该如何通过C++编程的方式实现这种功能呢?答案很简单,那就是使用SetupDi
函数族。
2、设备管理
2.1 设备枚举和查询
在查询设备信息时,首先需要使用SetupDiGetClassDevs
函数获取设备信息集的句柄,其次通过SetupDiEnumDeviceInfo
函数遍历信息集,获取每一个元素的设备信息,最后可以通过SetupDiGetDeviceInstanceId
、SetupDiGetDeviceRegistryProperty
、SetupDiOpenDevRegKey
查询对应的设备信息。
WINSETUPAPI HDEVINFO SetupDiGetClassDevs(
[in, optional] const GUID *ClassGuid,
[in, optional] PCWSTR Enumerator,
[in, optional] HWND hwndParent,
[in] DWORD Flags
);
// 获取所有的网卡设备,包含离线设备
HDEVINFO handle = SetupDiGetClassDevs(&GUID_DEVCLASS_NET, NULL, NULL, NULL);
- 当需要获取某一类的设备时,
ClassGuid
参数传递对应设备类的GUID信息,否则请将参数置空。系统定义的类GUID在 devguid.h 中定义。 - Flags参数通常可以使用
DIGCF_ALLCLASSES
、DIGCF_PRESENT
、NULL
。当使用DIGCF_ALLCLASSES
,获取当前系统所有设备安装类或所有设备接口类的已安装设备的列表。使用后两者,则只返回指定设备类的设备列表,区别在于DIGCF_PRESENT
只返回当前系统已连接的设备,而后者则返回所有。
WINSETUPAPI BOOL SetupDiEnumDeviceInfo(
[in] HDEVINFO DeviceInfoSet,
[in] DWORD MemberIndex,
[out] PSP_DEVINFO_DATA DeviceInfoData
);
DWORD dev_index = 0;
do {
SP_DEVINFO_DATA dev_data;
dev_data.cbSize = sizeof(SP_DEVINFO_DATA);
if (!SetupDiEnumDeviceInfo(handle, dev_index, &dev_data)) {
break;
}
dev_index++;
} while (true);
DeviceInfoSet
传入SetupDiGetClassDevs
获取的设备句柄。MemberIndex
传入设备序列,从0开始每次加一,直至返回FALSE
为止。DeviceInfoData
即获取的设备信息。
WINSETUPAPI BOOL SetupDiGetDeviceInstanceId(
[in] HDEVINFO DeviceInfoSet,
[in] PSP_DEVINFO_DATA DeviceInfoData,
[out, optional] PWSTR DeviceInstanceId,
[in] DWORD DeviceInstanceIdSize,
[out, optional] PDWORD RequiredSize
);
DWORD len = 1024;
wchar_t buffer[1024] = {};
if (SetupDiGetDeviceInstanceId(handle, &dev_data, buffer, len, &len) == TRUE) {
if (0 == _wcsnicmp(buffer, L"PCI", 3) || 0 == _wcsnicmp(buffer, L"USB", 3)) {
; // Usb网卡或者内置网卡
}
}
- 最终获取的设备实例ID如下所示:
WINSETUPAPI BOOL SetupDiGetDeviceRegistryProperty(
[in] HDEVINFO DeviceInfoSet,
[in] PSP_DEVINFO_DATA DeviceInfoData,
[in] DWORD Property,
[out, optional] PDWORD PropertyRegDataType,
[out, optional] PBYTE PropertyBuffer,
[in] DWORD PropertyBufferSize,
[out, optional] PDWORD RequiredSize
);
DWORD regDataType;
DWORD len = 1024;
wchar_t buffer[1024] = {};
SetupDiGetDeviceRegistryProperty(handle, &dev_data, SPDRP_FRIENDLYNAME, ®DataType, (PBYTE)buffer, len, &len)
- 从设备信息中取得想要的设备信息。比较常用的有
SPDRP_FRIENDLYNAME
等。
WINSETUPAPI HKEY SetupDiOpenDevRegKey(
[in] HDEVINFO DeviceInfoSet,
[in] PSP_DEVINFO_DATA DeviceInfoData,
[in] DWORD Scope,
[in] DWORD HwProfile,
[in] DWORD KeyType,
[in] REGSAM samDesired
);
HKEY hDeviceKey = SetupDiOpenDevRegKey(handle, &dev_data, DICS_FLAG_GLOBAL, 0, DIREG_DRV, KEY_READ);
-
打开设备对应的注册表信息,如下所示(通常情况下不需要使用本函数):
2.2 设备启用/禁用
WINSETUPAPI BOOL SetupDiSetClassInstallParams(
[in] HDEVINFO DeviceInfoSet,
[in, optional] PSP_DEVINFO_DATA DeviceInfoData,
[in, optional] PSP_CLASSINSTALL_HEADER ClassInstallParams,
[in] DWORD ClassInstallParamsSize
);
WINSETUPAPI BOOL SetupDiCallClassInstaller(
[in] DI_FUNCTION InstallFunction,
[in] HDEVINFO DeviceInfoSet,
[in, optional] PSP_DEVINFO_DATA DeviceInfoData
);
WINSETUPAPI BOOL SetupDiGetDeviceInstallParams(
[in] HDEVINFO DeviceInfoSet,
[in, optional] PSP_DEVINFO_DATA DeviceInfoData,
[out] PSP_DEVINSTALL_PARAMS_W DeviceInstallParams
);
-
SetupDiSetClassInstallParams
函数可以实现各种各样的功能,而具体执行的功能由ClassInstallParams
结构体的InstallFunction
决定。 -
而设备禁用/启用对应的功能编号是
DIF_PROPERTYCHANGE
,其对应的结构体是SP_PROPCHANGE_PARAMS
。
typedef struct _SP_PROPCHANGE_PARAMS {
SP_CLASSINSTALL_HEADER ClassInstallHeader;
DWORD StateChange;
DWORD Scope;
DWORD HwProfile;
} SP_PROPCHANGE_PARAMS, *PSP_PROPCHANGE_PARAMS;
StateChange
支持DICS_ENABLE
(启用)、DICS_DISABLE
(禁用)、DICS_PROPCHANGE
(设备的属性已更改)、DICS_START
(启动设备)、DICS_STOP
(设备正在停止)。Scope
可以使用DICS_FLAG_GLOBAL
和DICS_FLAG_CONFIGSPECIFIC
2个值,分别代表变更所有硬件配置文件和指定的硬件配置文件。HwProfile
指定变更的硬件配置文件,0代表当前硬件配置文件。- 个人见解,这里所说的硬件配置文件注册表中
HKEY_LOCAL_MACHINE\SYSTEM\ControlSetXXX
。 - 简单示例如下:
do {
SP_PROPCHANGE_PARAMS params;
params.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
params.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
// when disable, set params.StateChange = DICS_DISABLE
params.StateChange = DICS_ENABLE;
params.Scope = DICS_FLAG_CONFIGSPECIFIC;
params.HwProfile = 0;
if (!SetupDiSetClassInstallParams(
handle, &dev_data, ¶ms.ClassInstallHeader, sizeof(params)) ||
!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, handle, p_data)) {
// SetupDiCallClassInstaller maybe failed by 0xE000020b, ignore
if (GetLastError() != ERROR_NO_SUCH_DEVINST) {
break;
}
}
SP_DEVINSTALL_PARAMS devInstallParams;
devInstallParams.cbSize = sizeof(SP_DEVINSTALL_PARAMS);
// get new install params, check os whether need reboot
if (SetupDiGetDeviceInstallParams(handle, &dev_data, &devInstallParams)) {
if (devInstallParams.Flags & (DI_NEEDRESTART | DI_NEEDREBOOT)) {
; // os need reboot or restart
}
}
} while (false);
2.3 设备卸载
设备卸载对应的功能编号是DIF_REMOVE
。其对应的结构体则是SP_REMOVEDEVICE_PARAMS
。
typedef struct _SP_REMOVEDEVICE_PARAMS {
SP_CLASSINSTALL_HEADER ClassInstallHeader;
DWORD Scope;
DWORD HwProfile;
} SP_REMOVEDEVICE_PARAMS, *PSP_REMOVEDEVICE_PARAMS;
Scope
可以使用DI_REMOVEDEVICE_GLOBAL
和DI_REMOVEDEVICE_CONFIGSPECIFIC
2个值,分别代表变更所有硬件配置文件和指定的硬件配置文件。HwProfile
指定变更的硬件配置文件,0代表当前硬件配置文件。DI_REMOVEDEVICE_CONFIGSPECIFIC
标志仅适用于根枚举设备。 当 Windows 从配置设备的最后一个硬件配置文件中删除设备时,Windows 将执行全局删除。(具体用法请自行实践)
do {
SP_REMOVEDEVICE_PARAMS params;
params.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
params.ClassInstallHeader.InstallFunction = DIF_REMOVE;
params.Scope = DI_REMOVEDEVICE_GLOBAL;
params.HwProfile = 0;
if (!SetupDiSetClassInstallParams(
handle, &dev_data, ¶ms.ClassInstallHeader, sizeof(params)) ||
!SetupDiCallClassInstaller(DIF_REMOVE, handle, p_data)) {
break;
}
SP_DEVINSTALL_PARAMS devInstallParams;
devInstallParams.cbSize = sizeof(SP_DEVINSTALL_PARAMS);
// get new install params, check os whether need reboot
if (SetupDiGetDeviceInstallParams(handle, &dev_data, &devInstallParams)) {
if (devInstallParams.Flags & (DI_NEEDRESTART | DI_NEEDREBOOT)) {
; // os need reboot or restart
}
}
} while (false);
3、热插拔管理
对于计算机上已有的设备,可以通过上述步骤实现管控,那么新接入的设备应该如何管理呢?一个显而易见的方法就是监控所有设备的插拔事件。
而Windows系统也确实提供了一个对应的功能,即RegisterDeviceNotification
函数。
HDEVNOTIFY RegisterDeviceNotification(
[in] HANDLE hRecipient,
[in] LPVOID NotificationFilter,
[in] DWORD Flagsc
);
hRecipient
:指定接受设备事件的窗口或者服务句柄。NotificationFilter
:指向设备类型的数据块指针。此块始终以 DEV_BROADCAST_HDR 结构开头。Flagsc
:
值 | 含义 |
---|---|
DEVICE_NOTIFY_WINDOW_HANDLE | hRecipient 参数是窗口句柄。 |
DEVICE_NOTIFY_SERVICE_HANDLE | hRecipient 参数是服务状态句柄。 |
DEVICE_NOTIFY_ALL_INTERFACE_CLASSES | 通知接收方所有设备接口类的设备接口事件。 (实际测试中无效,待调查) |
3.1 硬件事件注册
由于需要注册的是设备事件,对应的结构如下:
typedef struct _DEV_BROADCAST_DEVICEINTERFACE_W {
DWORD dbcc_size;
DWORD dbcc_devicetype;
DWORD dbcc_reserved;
GUID dbcc_classguid;
wchar_t dbcc_name[1];
} DEV_BROADCAST_DEVICEINTERFACE_W, *PDEV_BROADCAST_DEVICEINTERFACE_W;
dbcc_devicetype
必须设置为DBT_DEVTYP_DEVICEINTERFACE
。dbcc_devicetype
则设置为需要监控的设备接口GUID。
定义 | 含义 | 头文件 |
---|---|---|
GUID_DEVINTERFACE_USB_DEVICE | USB设备 | usbiodef.h |
GUID_DEVINTERFACE_NET | 网络设备 | ndisguid.h |
GUID_DEVINTERFACE_CDROM | 光驱设备 | winioctl.h |
GUID_DEVINTERFACE_VOLUME | 卷设备 | winioctl.h |
HWND hWnd; // 窗口句柄
SERVICE_STATUS_HANDLE hSvrHandle = NULL; // 服务句柄
HDEVNOTIFY hDev[4] = { NULL, NULL, NULL, NULL };
BOOL UnregisterDeviceNotify() {
for (int i = 0; i < 4; i++) {
if (hDev[i] != NULL) {
UnregisterDeviceNotification(hDev[i]);
}
}
}
BOOL RegisterDeviceNotify() {
GUID dev_guids[4] = {GUID_DEVINTERFACE_CDROM, GUID_DEVINTERFACE_VOLUME,
GUID_DEVINTERFACE_NET, GUID_DEVINTERFACE_USB_DEVICE};
for (int i = 0; i < 4; i++) {
DEV_BROADCAST_DEVICEINTERFACE di = {0};
di.dbcc_size = sizeof(di);
di.dbcc_reserved = 0;
di.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
di.dbcc_classguid = dev_guids[i];
// hDev[i] = RegisterDeviceNotification(hSvrHandle, &di,
// DEVICE_NOTIFY_SERVICE_HANDLE);
hDev[i] =
RegisterDeviceNotification(hWnd, &di, DEVICE_NOTIFY_WINDOW_HANDLE);
if (hDev[i] == NULL) {
UnregisterDeviceNotify();
}
}
}
3.2 硬件事件回调
- 对于窗口进程而言,硬件事件会通知到**
WNDPROC
** 回调函数中。其中uMsg
参数固定为WM_DEVICECHANGE
,wParam
参数对应具体的事件id,lParam
为特定于事件的数据。
LRESULT CALLBACK WindowProc(HWND hwnd, // handle to window
UINT uMsg, // WM_DEVICECHANGE
WPARAM wParam, // device-change event
LPARAM lParam) // event-specific data
{
switch (message) {
case WM_DEVICECHANGE: {
switch (wParam) {
case DBT_DEVICEARRIVAL:
case DBT_DEVICEREMOVECOMPLETE: {
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam;
if (pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) {
PDEV_BROADCAST_DEVICEINTERFACE pDev =
(PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
// do some thing
}
break;
}
default:
break;
}
} break;
default:
}
return 0;
}
- 对于服务进程来说,硬件事件会通知到**
LPHANDLER_FUNCTION_EX
** 回调函数中。此回调函数需要通过registerServiceCtrlHandlerEx
注册。其中dwControl
参数固定为SERVICE_CONTROL_DEVICEEVENT
,wParam
参数对应具体的事件id,lParam
为特定于事件的数据。
DWORD ServiceCtrlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData,
LPVOID lpContext) {
switch (dwControl) {
case SERVICE_CONTROL_DEVICEEVENT:
switch (dwEventType) {
case DBT_DEVICEARRIVAL:
case DBT_DEVICEREMOVECOMPLETE: {
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lpEventData;
if (pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) {
PDEV_BROADCAST_DEVICEINTERFACE pDev =
(PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
// do some thing
}
}
default:
break;
}
default:
break;
};
return 0;
}
3.3 数据解析
我们从硬件事件中获取的数据DEV_BROADCAST_DEVICEINTERFACE
中,需要关注的只有2个数据。
dbcc_devicetype
:硬件设备接口的GUID。即上文中注册时使用的GUID。dbcc_name
:硬件设备的设备路径。如下所示:
“\\?\USBSTOR#CdRom&Ven_SecZure&Prod_SZU113&Rev_1.12#8&17c82afa&0&SZU2230621000019&0#{53f56308-b6bf-11d0-94f2-00a0c91efb8b}”
其中,\\?\
代表设备路径,#
号作为路径分隔符,
{53f56308-b6bf-11d0-94f2-00a0c91efb8b}
是设备接口的GUID信息,可以从中取得实例路径:USBSTOR\CdRom&Ven_SecZure&Prod_SZU113&Rev_1.12\8&17c82afa&0&SZU2230621000019&0
。
那么,取出设备地址可以干什么呢?答案就是注册表。
在windows操作系统中,所有的设备都会注册到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum
路径下,将两个路径拼接就可以获取设备的注册表信息。
可以看出SetupDiGetDeviceRegistryProperty
函数获取的信息和注册表中看到的信息基本上是一致的。
而其中的Driver
字段对应的则是HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class
路径下SetupDiOpenDevRegKey
打开的注册表路径。
bool GetInstanceId(const char *dev_path, char *instance_id, int *length) {
if (strncmp(dev_path.c_str(), "\\\\?\\", 4)) return;
size_t len = strlen(dev_path);
for (int pos = len - 1; pos >= 0; pos--) {
if (dev_path[pos] == '#') {
if (dev_path[pos + 1] == '{' &&
dev_path[pos + 1 + sizeof("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] ==
'}') {
for (int i = 4, j = 0; i < pos && j < (*length); i++) {
if (dev_path[i] == '#') {
instance_id[j++] = '\\';
} else {
instance_id[j++] = dev_path[i];
}
}
instance_id[j] = '\0';
return true;
} else {
return false;
}
}
}
return false;
}
4、类型识别
4.1 端口
- 通过
GUID_DEVCLASS_PORTS
枚举所有的端口设备 - 通过
SetupDiGetDeviceRegistryProperty
获取设备友好名称 - 包含
LPT
:并口 - 包含
COM
:串口
4.2 光驱
- 通过
GUID_DEVCLASS_CDROM
枚举所有的端口设备 - 通过
SetupDiGetDeviceInstanceId
获取设备实例路径 - 包含
CDROM
:光驱
4.3 软驱
- 通过
GUID_DEVCLASS_FLOPPYDISK
枚举所有的端口设备 - 通过
SetupDiGetDeviceInstanceId
获取设备实例路径 FDC
开始:软驱
4.4 蓝牙
- 通过
GUID_DEVCLASS_BLUETOOTH
枚举所有的端口设备 - 通过
SetupDiGetDeviceInstanceId
获取设备实例路径 USB
开始:蓝牙
4.5 网口
- 通过
GUID_DEVCLASS_NET
枚举所有的端口设备 - 通过
SetupDiGetDeviceInstanceId
获取设备实例路径 PCI
开始:有线网卡/无线网卡USB
开始:无线网卡/无线上网卡BTH
开始:蓝牙局部网{5D624F94-8850-40C3-A3FA-A4FD2080BAF3}
开始:热点- 其它:虚拟网卡
那么应该如何区分有线网卡,无线网卡和无线上网卡呢?那就是通过查询网卡注册表中的MediaSubType
字段,值为2的情形即为无线网卡。
// 首先打开设备注册表
WCHAR szInstanceId[MAX_PATH];
DWORD dwSize = sizeof(szInstanceId);
// 查询网卡的连接id
if (RegQueryValueEx(hDeviceKey, TEXT("NetCfgInstanceId"), NULL, NULL,
(LPBYTE)szInstanceId, &dwSize) == ERROR_SUCCESS) {
std::wstring hKeyPath =
L"SYSTEM\\CurrentControlSet\\Control\\Network\\{4D36E972-E325-"
L"11CE-BFC1-08002BE10318}\\";
hKeyPath += szInstanceId;
hKeyPath += L"\\Connection";
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, hKeyPath.c_str(), 0, KEY_READ, &hKey) ==
ERROR_SUCCESS) {
DWORD dwMediaSubType = 0;
DWORD dwSize = sizeof(dwMediaSubType);
if (RegQueryValueEx(hKey, L"MediaSubType", NULL, NULL,
(LPBYTE)&dwMediaSubType, &dwSize) == ERROR_SUCCESS &&
dwMediaSubType == 0x02) {
// 无线网卡
} else {
// 有线或者无线上网卡
}
RegCloseKey(hKey);
}
}