C++控制USB根集线器的启停


写在前面

在此之前先想想做这件事的原因,哦,我想起来了,上周自动化测试好像有关于网银盾的,业务方不接受手工辅助插拔
虽然他们的要求有点难搞,不过正所谓不查一下资料就不知道真的能搞,搜了两天的资料,终于拼凑出了这篇文章
主要功能是实现USB根集线器的自动启停,在这个过程中,USB设备会重新挂载(ps:鼠标也会短暂失灵)

主要思路

在查资料的时候,发现Windows提供了一系列SetUPDi函数,可以操作设备管理器

相关函数

FormatGUID
这个没什么好说的,主要是格式化输出设备类GUID(注意那个“类”字)

void FormatGUID(GUID guid)
{
    cout.fill('0');
    cout.width(8);
    cout << setiosflags(ios::uppercase) << hex << guid.Data1
        << "-" << guid.Data2 << "-" << guid.Data3 << "-";
    for (int j = 0; j < 8; j++)
    {
        cout.fill('0');
        cout.width(2);
        cout << hex << (int)guid.Data4[j];
        if (j == 1)
        {
            cout << "-";
        }
    }
    cout << endl;
}

ChangeStatus
启停设备的主要代码段
传入了三个参数,第一个是要修改为的状态,DICS_DISABLE(停用)或DICS_ENABLE(启用)
第二个是在枚举设备过程中得到的参数,用于快速获取设备信息
第三个参数类似设备句柄,有了它才能进行相应的操作

int ChangeStatus(DWORD NewStatus, DWORD SelectedItem, HDEVINFO hDevInfo)
{
    //两个结构体,一个用于修改设备参数,一个用于存储设备信息
    SP_PROPCHANGE_PARAMS PropChangeParams = { sizeof(SP_CLASSINSTALL_HEADER) };
    SP_DEVINFO_DATA DeviceInfoData = { sizeof(SP_DEVINFO_DATA) };
    // 通过SelectedItem重新获取hDevInfo.
    if (!SetupDiEnumDeviceInfo(hDevInfo, SelectedItem, &DeviceInfoData))
    {
        printf("SetupDiEnumDeviceInfo errorcode = %d\n", GetLastError());
        return 0;
    }
    // 修改设备的参数.
    PropChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
    PropChangeParams.Scope = DICS_FLAG_GLOBAL;
    // 启用或停用设备
    PropChangeParams.StateChange = NewStatus;
    // 修改后的参数添加到hDevInfo
    if (!SetupDiSetClassInstallParams(hDevInfo, &DeviceInfoData, (SP_CLASSINSTALL_HEADER*)&PropChangeParams, sizeof(PropChangeParams)))
    {
        printf("SetupDiSetClassInstallParams errorcode = %d\n", GetLastError());
        return 0;
    }
    // 调用API使更改生效.
    if (!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, hDevInfo, &DeviceInfoData))
    {
        printf("SetupDiCallClassInstaller errorcode = %d\n", GetLastError());
        return -1;
    }
    return 1;
}

GetDevInfo
程序入口函数
根据GUID枚举设备信息,获取设备的SPDRP_SERVICE,即服务名
与根集线器作比较,如果一致,则执行启/停操作
引入三个参数,分别是设备类GUID(这里需要通用串行总线控制器的GUID)
要修改为的状态(0:停用/1:启用),以及根集线器服务名

int GetDevInfo(char* guidstring, int nStatus, char* Service_Name)
{
    // 这行代码可以用于枚举所有设备
    //HDEVINFO hDevinfo = SetupDiGetClassDevs(NULL, 0, 0, DIGCF_ALLCLASSES | DIGCF_PRESENT);
    GUID devGUID;
    ZeroMemory(&devGUID, sizeof(GUID));
    TCHAR* GUIDString = NULL;
    GUIDString = (TCHAR*)(guidstring);
    BOOL FOUND = FALSE;
    if (UuidFromString((RPC_CSTR)GUIDString, &devGUID))
    {
        return FALSE;
    }
    // 根据类GUID获取设备句柄
    HDEVINFO hDevinfo = SetupDiGetClassDevs(&devGUID, 0, 0, DIGCF_PRESENT);
    int res = 1;
    if (hDevinfo == INVALID_HANDLE_VALUE)
    {
        return FALSE;
    }
    SP_DEVINFO_DATA data;
    DWORD nindex;
    LPTSTR szBuf = NULL;
    DWORD DataT;
    DWORD buffsize = 0;
    memset(&data, 0, sizeof(SP_DEVINFO_DATA));
    data.cbSize = sizeof(SP_DEVINFO_DATA);
    // 枚举设备类下的所有设备
    for (nindex = 0; SetupDiEnumDeviceInfo(hDevinfo, nindex, &data); ++nindex)
    {
        // 获取设备的信息
        while (!SetupDiGetDeviceRegistryProperty(hDevinfo, &data, SPDRP_SERVICE, &DataT, (PBYTE)szBuf, buffsize, &buffsize))
        {
            if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
                if (szBuf) {
                    LocalFree(szBuf);
                }
                szBuf = (LPTSTR)LocalAlloc(LPTR, buffsize);
            }
            else {
                break;
            }
        }
        char This_Service[sizeof(szBuf) * 4]; int n;
        n = sprintf_s(This_Service, "%s", szBuf);
        // 与预设的服务名进行比较
        if (szBuf && (string)Service_Name == (string)This_Service)
        {
            FOUND = TRUE;
            printf("已发现通用串行总线控制器,GUID:");
            FormatGUID(data.ClassGuid);
            SP_PROPCHANGE_PARAMS procChange = { sizeof(SP_CLASSINSTALL_HEADER) };
            procChange.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
            procChange.Scope = DICS_FLAG_GLOBAL;
            procChange.StateChange = DICS_DISABLE;
            if (nStatus == 0) {
                printf("即将禁用USB根集线器,目标服务:%s\n", szBuf);
                Sleep(1000);
                // 禁用设备
                res = ChangeStatus(DICS_DISABLE, nindex, hDevinfo);
            }
            else if (nStatus == 1)
            {
                Sleep(1000);
                printf("即将启用USB根集线器,目标服务:%s\n", szBuf);
                // 启用设备
                res = ChangeStatus(DICS_ENABLE, nindex, hDevinfo);
                printf("已启用USB根集线器\n");
            }
            else {
                return -1;
            }
            switch (res) {
            case -1:
                printf("启停过程异常,请在设备管理器中查看集线器状态\n");
                break;
            case 0:
                printf("操作失败!枚举或设置参数过程引发异常\n");
                break;
            case 1:
                printf("当前改动已生效\n");
                break;
            }
        }
        if (!FOUND)
            printf("通用串行总线控制器中未发现相关设备");
        return res;
    }
}

完整代码

setupditest.h
需要的头可能没有那么多,可以试着去掉几个2333

#pragma once
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <setupapi.h>
#include <iostream>
#include <Rpc.h>
#include <devguid.h>
#include <usbiodef.h>
#include <atlstr.h>
#include <iomanip>
#pragma comment(lib, "setupapi.lib")
#pragma comment(lib, "Rpcrt4.lib")

main.cpp

#include "setupditest.h"

using namespace std;

int GetDevInfo(char* guidstring,int nStatus,char* Service_Name);
void FormatGUID(GUID guid);
int ChangeStatus(DWORD NewStatus, DWORD SelectedItem, HDEVINFO hDevInfo);

void FormatGUID(GUID guid)
{
    cout.fill('0');
    cout.width(8);
    cout << setiosflags(ios::uppercase) << hex << guid.Data1
        << "-" << guid.Data2 << "-" << guid.Data3 << "-";
    for (int j = 0; j < 8; j++)
    {
        cout.fill('0');
        cout.width(2);
        cout << hex << (int)guid.Data4[j];
        if (j == 1)
        {
            cout << "-";
        }
    }
    cout << endl;
}

int ChangeStatus(DWORD NewStatus, DWORD SelectedItem, HDEVINFO hDevInfo)
{
    SP_PROPCHANGE_PARAMS PropChangeParams = { sizeof(SP_CLASSINSTALL_HEADER) };
    SP_DEVINFO_DATA DeviceInfoData = { sizeof(SP_DEVINFO_DATA) };
    // Get a handle to the Selected Item.
    if (!SetupDiEnumDeviceInfo(hDevInfo, SelectedItem, &DeviceInfoData))
    {
        printf("SetupDiEnumDeviceInfo errorcode = %d\n", GetLastError());
        return 0;
    }
    // Set the PropChangeParams structure.
    PropChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
    PropChangeParams.Scope = DICS_FLAG_GLOBAL;
    PropChangeParams.StateChange = NewStatus;
    if (!SetupDiSetClassInstallParams(hDevInfo, &DeviceInfoData, (SP_CLASSINSTALL_HEADER*)&PropChangeParams, sizeof(PropChangeParams)))
    {
        printf("SetupDiSetClassInstallParams errorcode = %d\n", GetLastError());
        return 0;
    }
    // Call the ClassInstaller and perform the change.
    if (!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, hDevInfo, &DeviceInfoData))
    {
        printf("SetupDiCallClassInstaller errorcode = %d\n", GetLastError());
        return -1;
    }
    return 1;
}

int GetDevInfo(char* guidstring, int nStatus, char* Service_Name)
{
    //HDEVINFO hDevinfo = SetupDiGetClassDevs(NULL, 0, 0, DIGCF_ALLCLASSES | DIGCF_PRESENT);
    GUID devGUID;
    ZeroMemory(&devGUID, sizeof(GUID));
    TCHAR* GUIDString = NULL;
    GUIDString = (TCHAR*)(guidstring);
    if (UuidFromString((RPC_CSTR)GUIDString, &devGUID))
    {
        return FALSE;
    }
    HDEVINFO hDevinfo = SetupDiGetClassDevs(&devGUID, 0, 0, DIGCF_PRESENT);
    int res = 1;
    if (hDevinfo == INVALID_HANDLE_VALUE)
    {
        return FALSE;
    }
    SP_DEVINFO_DATA data;
    DWORD nindex;
    LPTSTR szBuf = NULL;
    DWORD DataT;
    DWORD buffsize = 0;
    memset(&data, 0, sizeof(SP_DEVINFO_DATA));
    data.cbSize = sizeof(SP_DEVINFO_DATA);
    for (nindex = 0; SetupDiEnumDeviceInfo(hDevinfo, nindex, &data); ++nindex)
    {
        //printf("%d %Id\n", data.DevInst,data.Reserved);
        while (!SetupDiGetDeviceRegistryProperty(hDevinfo, &data, SPDRP_SERVICE, &DataT, (PBYTE)szBuf, buffsize, &buffsize))
        {
            if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
                if (szBuf) {
                    LocalFree(szBuf);
                }
                szBuf = (LPTSTR)LocalAlloc(LPTR, buffsize);
            }
            else {
                break;
            }
        }
        char This_Service[sizeof(szBuf) * 4]; int n;
        n = sprintf_s(This_Service, "%s", szBuf);
        if (szBuf && (string)Service_Name == (string)This_Service)
        {
            printf("已发现通用串行总线控制器,GUID:");
            FormatGUID(data.ClassGuid);
            SP_PROPCHANGE_PARAMS procChange = { sizeof(SP_CLASSINSTALL_HEADER) };
            procChange.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
            procChange.Scope = DICS_FLAG_GLOBAL;
            procChange.StateChange = DICS_DISABLE;
            if (nStatus == 0) {
                printf("即将禁用USB根集线器,目标服务:%s\n", szBuf);
                Sleep(1000);
                res = ChangeStatus(DICS_DISABLE, nindex, hDevinfo);
            }
            else if (nStatus == 1)
            {
                Sleep(1000);
                printf("即将启用USB根集线器,目标服务:%s\n", szBuf);
                res = ChangeStatus(DICS_ENABLE, nindex, hDevinfo);
                printf("已启用USB根集线器\n");
            }
            else {
                return 2;
            }
            switch (res) {
            case -1:
                printf("启停过程异常,请在设备管理器中查看集线器状态\n");
                break;
            case 0:
                printf("操作失败!枚举或设置参数过程引发异常\n");
                break;
            case 1:
                printf("当前改动已生效\n");
                break;
            }
        }
    }
    return res;
}

int main(void)
{
    GetDevInfo((char*)"36FC9E60-C465-11CF-8056-444553540000",0, (char*)"iusb3hub");
    Sleep(10);
    GetDevInfo((char*)"36FC9E60-C465-11CF-8056-444553540000",1, (char*)"iusb3hub");
}

动态链接库

如果要生成动态链接库,需要修改函数的声明与定义方式
如下所示:

// 定义一个宏,直接使用也是可以的
#define DLLEXPORT extern "C" __declspec(dllexport)
// 声明
DLLEXPORT void FormatGUID(GUID guid);
// 定义
DLLEXPORT void FormatGUID(GUID guid)
{
    cout.fill('0');
    cout.width(8);
    cout << setiosflags(ios::uppercase) << hex << guid.Data1
        << "-" << guid.Data2 << "-" << guid.Data3 << "-";
    for (int j = 0; j < 8; j++)
    {
        cout.fill('0');
        cout.width(2);
        cout << hex << (int)guid.Data4[j];
        if (j == 1)
        {
            cout << "-";
        }
    }
    cout << endl;
}

调用DLL

生成动态链接库后可以在python中调用,以下是一个例子:
ps:在cmd以外的地方运行py时,无法看到C代码中printf的输出

import ctypes
import time

UsbControl = ctypes.cdll.LoadLibrary("DllUSB.DLL")
# 一定要注意参数类型,不能直接传递字符串
GUID = ctypes.c_char_p(b'36FC9E60-C465-11CF-8056-444553540000')
Service_Name = ctypes.c_char_p(b'iusb3hub')
UsbControl.GetDevInfo(GUID,0,Service_Name)
time.sleep(3)
UsbControl.GetDevInfo(GUID,1,Service_Name)

Q&A

如何获取GUID?
右击我的电脑,点击管理,点击设备管理器,找到目标设备,右键,点击属性,点击详细信息,选择设备类GUID即可看到
如果你懒得手打,可以使用WMIC或者RW工具,此外devguid.h和usbiodef.h两个头文件中也存储有设备的GUID