Windows设备管理

1、前言

熟悉Windows系统的都应该使用过设备管理器。设备管理器将操作系统中所有已安装的设备分类展现出来。同时提供了安装、卸载、启用和禁用的功能。
那么,我们应该如何通过C++编程的方式实现这种功能呢?答案很简单,那就是使用SetupDi函数族。

2、设备管理

2.1 设备枚举和查询

在查询设备信息时,首先需要使用SetupDiGetClassDevs函数获取设备信息集的句柄,其次通过SetupDiEnumDeviceInfo函数遍历信息集,获取每一个元素的设备信息,最后可以通过SetupDiGetDeviceInstanceIdSetupDiGetDeviceRegistryPropertySetupDiOpenDevRegKey查询对应的设备信息。

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信息,否则请将参数置空。系统定义的类GUIDdevguid.h 中定义。
  • Flags参数通常可以使用DIGCF_ALLCLASSESDIGCF_PRESENTNULL。当使用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, &regDataType, (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_GLOBALDICS_FLAG_CONFIGSPECIFIC2个值,分别代表变更所有硬件配置文件和指定的硬件配置文件。
  • 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, &params.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_GLOBALDI_REMOVEDEVICE_CONFIGSPECIFIC2个值,分别代表变更所有硬件配置文件和指定的硬件配置文件。
  • 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, &params.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_HANDLEhRecipient 参数是窗口句柄。
DEVICE_NOTIFY_SERVICE_HANDLEhRecipient 参数是服务状态句柄。
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_DEVICEUSB设备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_DEVICECHANGEwParam参数对应具体的事件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_DEVICEEVENTwParam参数对应具体的事件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);
  }
}

5 、示例程序

基于MFC实现的设备管理程序

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值