65.9K
CodeProject 正在变化。 阅读更多。
Home

重启还是不重启?

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2023年10月13日

CPOL

30分钟阅读

viewsIcon

7308

downloadIcon

235

这是一个关于如何配置您的应用程序、服务或驱动程序以处理新设备出现或设备移除,避免重启请求的指南。

目录

引言

有时,当您更新设备软件时,它可能会请求重启您的 PC。这是因为一个或几个应用程序正在使用正在更新的设备驱动程序。实际上,系统会通知所有应用程序该设备需要被释放,但大多数软件无法处理这种情况。例如,我们有一个捕获设备:一个简单的网络摄像头。将其放入 GraphEdit 工具 中,而不启动播放甚至不进行任何连接,然后在设备管理器中,尝试用鼠标右键菜单禁用该捕获设备。是的,您会看到系统重启请求,如下图所示。

这是因为设备对象被应用程序(在本例中是 GraphEdit)持有,而该应用程序无法处理设备移除请求。

简单的应用程序

现在我们可以尝试以编程方式重现创建摄像头设备对象的这些步骤,并尝试正确处理上述情况。为此,我们可以使用 DirectShow 设备枚举器来访问视频捕获类别的设备。

CComPtr<ICreateDevEnum> _dev;
if (S_OK == _dev.CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER))
{
    // Enumerate video capture devices
    CComPtr<IEnumMoniker> _enum;
    if (S_OK == _dev->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &_enum, 0))
    {
        //...
    }
}

我们枚举所有设备,并通过检查每个设备的 monik 字符串来搜索 USB 摄像头。这样,我们就可以跳过虚拟设备,因为我们需要真实的硬件来测试未来的实现。为此,我们使用以下代码检查设备接口路径。

CComPtr<IBindCtx> _context;
CreateBindCtx(0, &_context);
CComPtr<IMoniker> _moniker;
ULONG cFetched = 0;
// While we not got the USB camera or finish
while (g_hDevice == INVALID_HANDLE_VALUE
    && S_OK == _enum->Next(1, &_moniker, &cFetched)
    && _moniker)
{
    // Retrieve moniker string
    LPOLESTR pszTemp = NULL;
    if (S_OK == _moniker->GetDisplayName(_context, NULL, &pszTemp) && pszTemp)
    {
        // Check for USB Device
        _wcslwr_s(pszTemp, wcslen(pszTemp) + 1);
        if (wcsstr(pszTemp, L"pnp:\\\\?\\usb#"))
        {
            //...
        }
    }
}

找到合适的设备 monik 后,我们尝试初始化它并绑定到 IBaseFilter 对象。

// Create Filter Object
if (S_OK == _moniker->BindToObject(NULL, NULL, __uuidof(IBaseFilter), (void**)&_filter))
{
    //...
}

我们可以将该对象保存在应用程序中,并在退出时才释放它。现在您可以启动应用程序并查看:当您尝试在设备管理器中禁用选定的摄像头时,我们的应用程序将持有该硬件的一个实例,并且屏幕上会弹出重启请求对话框。

在常规的 Windows 应用程序中,可以使用设备通知轻松解决此问题。为了能够接收设备移除通知,我们应该使用 RegisterDeviceNotification API。对于此 API,我们应该有一个窗口对象,并准备一个用于处理窗口消息的循环以及窗口通知处理程序过程。

// Window Handle For Notifications
static TCHAR szClassName[100] = { 0 };
WNDCLASS wc = { 0 };
HINSTANCE hInstance = GetModuleHandle(NULL);
_stprintf_s(szClassName, _countof(szClassName), _T("Example_%d"), GetTickCount());
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProcHandler;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName = NULL;
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.lpszClassName = szClassName;

RegisterClass(&wc);
// Creating Window
HWND hWnd = CreateWindowEx(
    WS_EX_OVERLAPPEDWINDOW | WS_EX_APPWINDOW, szClassName, L"TestWindow",
    WS_OVERLAPPEDWINDOW, 0, 0, 640, 480, NULL, NULL, hInstance, NULL);

通知通过 WM_DEVICECHANGE 窗口消息发送。实际的窗口可以隐藏在应用程序中,因为我们只对通知处理感兴趣。RegisterDeviceNotification API 在注册通知时可能接收两种不同类型的结构作为参数。第一种是 DEV_BROADCAST_DEVICEINTERFACE,它允许接收具有指定接口或类 guid 的设备通知。第二种是 DEV_BROADCAST_HANDLE,我们需要指定目标设备句柄以仅接收关于该设备的通知。我们将使用第二种来处理我们的情况。

DEV_BROADCAST_HANDLE filter = { 0 };
memset(&filter, 0x00, sizeof(filter));
filter.dbch_size = sizeof(DEV_BROADCAST_HANDLE);
filter.dbch_devicetype = DBT_DEVTYP_HANDLE;
filter.dbch_handle = g_hDevice;

g_hNotify = RegisterDeviceNotification(hWnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE);

我们调用 RegisterDeviceNotification API 的其他参数是窗口句柄和 DEVICE_NOTIFY_WINDOW_HANDLE 标志。它表明我们正在使用窗口循环来处理通知。

最后一件事是获取收到的 IBaseFilter 对象的设备句柄。如果我们有真实硬件捕获设备的驱动程序,那么它支持 IKsObject 接口。通过使用此接口,我们可以使用 KsGetObjectHandle() 方法接收底层硬件对象的句柄。之后,我们可以复制该句柄并将其存储以供将来使用。

// Create Filter Object
if (S_OK == _moniker->BindToObject(NULL, NULL, __uuidof(IBaseFilter), (void**)&_filter))
{
    CComQIPtr<IKsObject> _object = _filter;
    if (_object)
    {
        if (!DuplicateHandle(hProcess,
            _object->KsGetObjectHandle(),
            hProcess, &g_hDevice, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
            _tprintf(_T("DuplicateHandle Error 0x%08X\n"), GetLastError());
        }
    }
}

现在,在 Windows 消息处理程序过程中,我们需要处理设备通知的 WM_DEVICECHANGE 消息。传递给它的 WPARAM 参数,在请求设备管理器移除设备时,可能是 DBT_DEVICEQUERYREMOVE。这可能是由于重新安装硬件的新驱动程序或手动禁用设备,就像我们之前那样。我们感兴趣的 WPARAM 参数的另一个值是 DBT_DEVICEREMOVECOMPLETE。当设备意外移除时,会发送该值——例如,USB 摄像头被拔出。在这两种 WPARAM 值下,我们都应该释放应用程序使用的所有设备资源。

LRESULT CALLBACK WindowProcHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (uMsg == WM_DEVICECHANGE) {
        if (wParam == DBT_DEVICEQUERYREMOVE || wParam == DBT_DEVICEREMOVECOMPLETE) {
            DEV_BROADCAST_HDR * hdr = (DEV_BROADCAST_HDR *)lParam;
            if (hdr->dbch_devicetype == DBT_DEVTYP_HANDLE) {
                DEV_BROADCAST_HANDLE * _handle = (DEV_BROADCAST_HANDLE *)hdr;
                if (_handle->dbch_hdevnotify == g_hNotify) {
                    // Close The Driver Handle 
                    if (g_hDevice) {
                        CloseHandle(g_hDevice);
                        g_hDevice = NULL;
                        _tprintf(_T("Camera '%s' removed from system, 
                                     press any key for quit\n"),
                            g_szCameraName);
                    }
                }
            }
        }
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

可以为不同的设备注册一些通知。这些通知可以通过 LPARAM 参数进行过滤,该参数最初被强制转换为 DEV_BROADCAST_HDR 结构指针。根据 dbch_devicetype 字段,可以将其强制转换为特定类型。在我们的例子中,dbch_devicetype 等于 DBT_DEVTYP_HANDLE,而 LPARAM 包含指向 DEV_BROADCAST_HANDLE 结构的指针。该结构的 dbch_hdevnotify 字段设置为等于作为 RegisterDeviceNotification API 调用结果收到的通知句柄。因此,我们对其进行比较以确保我们获得正确的通知。

要停止接收通知,必须调用 UnregisterDeviceNotification API 并传递先前调用 RegisterDeviceNotification API 返回的句柄。

// Unregister notification
if (g_hNotify) {
    UnregisterDeviceNotification(g_hNotify);
}

一旦应用程序启动,它就可以接收通知,然后我们持有的摄像头即将被移除或意外从系统中移除。因此,在应用程序中收到移除请求时,我们释放所有持有的摄像头的硬件资源,并且不会收到系统的重启请求。

PeekMessage 和设备通知

现在在测试应用程序中,我们有一个额外的窗口消息过程回调,我们在创建窗口时指定了它。您可能会说,我们不需要额外的过程来进行消息循环来处理 Windows 消息,因为我们可以在翻译消息时处理它。类似这样

MSG msg = { 0 };
while (PeekMessage(&msg, hWnd, 0, 0, PM_REMOVE)) {
    if (msg.message == WM_QUIT) {
        bExit = true;
        break;
    }
    if (msg.message == WM_DEVICECHANGE) {
        _tprintf(_T("WM_DEVICECHANGE\n"));
    }
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

但是,如果您启动应用程序,即使设备被禁用并重新启用,或者 USB 线被拔出,您也无法在控制台窗口中看到“WM_DEVICECHANGE”文本。

发生这种情况是因为 WM_DEVICECHANGE 通知会忽略消息队列,直接发送到窗口分派例程。因此,如果您设计的应用程序使用了默认的,您将无法收到这些通知。

Windows 服务呢?

现在清楚如何在应用程序中处理设备移除通知。但 Windows 服务呢?我见过一些捕获设备供应商的 Windows 服务,它们持有摄像头并在收到请求时也不释放它们。如果您的设备是 USB 设备,并且您只是拔出它,那没问题,但在其他情况下则不行,您会收到重启请求。如何避免因 Windows 服务而导致系统出现此类通知,但 Windows 服务不包含窗口消息循环?为此,Windows 服务应使用扩展 API RegisterServiceCtrlHandlerEx 来注册其控制函数回调。

// register our service control handler:
m_ServiceHandle = RegisterServiceCtrlHandlerExW( 
                                    SERVICE_NAME, ServiceCtrlHandlerEx, NULL);

if (!m_ServiceHandle) {
    return;
}

该回调将能够接收设备控制通知,事件为 SERVICE_CONTROL_DEVICEEVENT

// Service Control
DWORD WINAPI ServiceCtrlHandlerEx(DWORD dwCtrl, DWORD dwEventType, 
                                  LPVOID lpEventData, LPVOID lpContext)
{
    DWORD Result = NO_ERROR;
    switch (dwCtrl) {
    case SERVICE_CONTROL_STOP:
        ReportServiceStatus(SERVICE_STOP_PENDING);
        // Stop Service
        StopService();
        ReportServiceStatus(SERVICE_STOPPED);
        break;
    case SERVICE_CONTROL_DEVICEEVENT:
        ServiceDeviceEvent(dwEventType, lpEventData);
        break;
    default:
        Result = ERROR_CALL_NOT_IMPLEMENTED;
        break;
    }
    return Result;
}

要在服务中接收指定设备的设备移除通知,我们也应该使用 RegisterDeviceNotification API。但是,将从 RegisterServiceCtrlHandlerEx API 返回的服务句柄作为参数传递,而不是窗口句柄。同时,我们应该将标志参数设置为 DEVICE_NOTIFY_SERVICE_HANDLE 值,以指示我们传递的是服务句柄。目标捕获设备的选取方式与常规 Windows 应用程序相同。

// Register Device Handle Notification
DEV_BROADCAST_HANDLE filter = { 0 };
memset(&filter, 0x00, sizeof(filter));
filter.dbch_size = sizeof(DEV_BROADCAST_HANDLE);
filter.dbch_devicetype = DBT_DEVTYP_HANDLE;
filter.dbch_handle = g_hDevice;

g_hNotify = RegisterDeviceNotification(m_ServiceHandle, &filter, DEVICE_NOTIFY_SERVICE_HANDLE);

一旦我们能够收到通知,告知选定的设备已被移除或即将被移除。我们像之前讨论的常规 Windows 应用程序一样处理该通知。只是我们有一个 EventData 参数而不是窗口分派中的 LPARAM,我们将其强制转换为 DEV_BROADCAST_HDR 类型,并通过 OutputDebugString API 传递输出信息。

VOID ServiceDeviceEvent(DWORD dwEventType, LPVOID lpEventData) {

    if (dwEventType == DBT_DEVICEQUERYREMOVE || dwEventType == DBT_DEVICEREMOVECOMPLETE) {
        DEV_BROADCAST_HDR * hdr = (DEV_BROADCAST_HDR *)lpEventData;
        // Check for target device removal
        if (hdr->dbch_devicetype == DBT_DEVTYP_HANDLE) {
            DEV_BROADCAST_HANDLE * _handle = (DEV_BROADCAST_HANDLE *)hdr;
            // If notification which we had registered
            if (_handle->dbch_hdevnotify == g_hNotify) {
                // Close The Driver Handle 
                if (g_hDevice) {
                    CloseHandle(g_hDevice);
                    g_hDevice = NULL;
                    std::wostringstream os;
                    os << SERVICE_NAME << L": " << L"Camera '" << g_sCameraName 
                                            << L"' removed from system\n";
                    OutputDebugStringW(os.str().c_str());
                }
                // Unregister Notification
                if (g_hNotify) {
                    UnregisterDeviceNotification(g_hNotify);
                    g_hNotify = NULL;
                }
            }
        }
    }
}

一旦我们安装并启动了服务,我们就会在 DbgView 工具 中看到关于将被移除等待的设备的信息。当我们禁用设备管理器中的该设备或断开其电缆连接时,DbgView 工具会显示指定的通知信息。

内核模式下的通知

我们已经处理了用户模式应用程序和服务中的通知。现在该在内核模式下处理了。在驱动程序实现中,我们也可以打开任何设备并与它们通信。例如,我们可以打开摄像头捕获设备并读取帧,或者与蓝牙终端通信,或者执行其他需要打开设备句柄的操作,甚至可以在驱动程序中使用 USB 闪存卡。无论如何,如果驱动程序持有即将被移除的内容,我们也会收到包含重启请求的通知。要处理驱动程序中的设备移除,我们有 IoRegisterPlugPlayNotification API。此函数可以设置一个回调例程,该例程将在传递给它的设备即将移除时执行。

要使用 IoRegisterPlugPlayNotification API 来处理目标设备,我们应该拥有我们想要等待移除的设备的 Fs 对象:这与用户模式没有什么不同。对于内核示例实现,与之前的示例一样,我们只使用第一个 USB 网络摄像头作为移除目标。最初,我们应该获取该设备的 Fs 对象。这里的原始步骤是枚举现有摄像头。每个捕获设备都可以注册其接口在一个或多个不同的类别中。您可以在 GraphEdit 工具的捕获设备 monik string 中看到接口类别 guid。

通常,视频捕获设备有三个类别。它们注册了接口:KSCATEGORY_VIDEO - 用于视频设备,KSCATEGORY_CAPTURE - 用于捕获设备,KSCATEGORY_VIDEO_CAMERA - 用于摄像头设备。捕获类别以及视频设备也包含音频。视频摄像头类别用于注册摄像头。媒体基础捕获设备枚举使用此接口类别。通用视频类别是 KSCATEGORY_VIDEO,它可以通过 DirectShow 枚举器访问设备。可以帮助您查看每个类别下的内核设备的工具是 WDK 包中的 KSStudio 工具

因此,在我们的内核实现中,我们可以检查这些类别中的一个以获得正确的设备接口。用于测试,我们使用通用视频类别:KSCATEGORY_VIDEO。要获取类别中所有注册的接口,我们调用 IoGetDeviceInterfaces API。此 API 返回一个字符串数组,该数组应由调用者释放。

PWSTR pszszDeviceList = NULL;
GUID Interface = KSCATEGORY_VIDEO;
if (STATUS_SUCCESS == (Status = IoGetDeviceInterfaces(&Interface, NULL, 0, &pszszDeviceList))) {
    //...
    ExFreePool(pszszDeviceList);
}

每个接口符号链接字符串都以零结尾。在实现中,我们也只检查 USB 设备,就像我们在前面的示例中所做的那样。

PWSTR p = pszszDeviceList;
if (p) {
    while (wcslen(p) && !s_pCameraFileObject) {
        size_t cch = wcslen(p);
        if (cch) {
            if (_wcsnicmp(p, L"\\??\\usb#", 8) == 0) {
                UNICODE_STRING SymbolicLink = { 0 };
                RtlInitUnicodeString(&SymbolicLink, p);
                PDEVICE_OBJECT DeviceObject = NULL;
                if (STATUS_SUCCESS == (Status = IoGetDeviceObjectPointer(
                    &SymbolicLink, GENERIC_READ, &s_pCameraFileObject, &DeviceObject))) {
                    ULONG Size = sizeof(s_szDeviceName);
                    Status = IoGetDeviceProperty(s_pCameraFileObject->DeviceObject,
                        DevicePropertyFriendlyName, Size, s_szDeviceName, &Size
                    );
                    if (!NT_SUCCESS(Status)) {
                        DbgPrint("IoGetDeviceProperty Status: 0x%08x\n", Status);
                        wcscpy_s(s_szDeviceName, p);
                    }
                    Status = IoRegisterPlugPlayNotification(EventCategoryTargetDeviceChange,0, 
                        s_pCameraFileObject, DriverObject, DriverNotificationCallback, NULL, 
                        &s_TargetNotificationEntry);
                    if (!NT_SUCCESS(Status)) {
                        DbgPrint("IoRegisterPlugPlayNotification Status: 0x%08x", Status);
                    }
                    else {
                        DbgPrint("%S: Waiting device to be removed: '%S'\n", 
                                        DRIVER_NAME, s_szDeviceName);
                    }
                }
            }
        }
        p += (cch + 1);
    }
}

一旦我们找到了正确的接口,我们就可以打开目标设备 Fs 对象。这可以通过 IoGetDeviceObjectPointer API 或 ZwCreateFile API 来完成。当我们获得 Fs 对象后,我们将其传递给 IoRegisterPlugPlayNotification API 以及我们准备好的回调函数。我们应该将 EventCategoryTargetDeviceChange 指定为该函数的第一个参数,因为我们对特定设备的移除通知感兴趣。

为了在 DbgPring 输出中正确显示选定摄像头的设备名称,我们使用 IoGetDeviceProperty 函数请求目标 Fs 对象的 DevicePropertyFriendlyName 属性。

在回调通知函数中,我们收到 PLUGPLAY_NOTIFICATION_HEADER 结构,其 Event 字段等于 GUID_TARGET_DEVICE_REMOVE_COMPLETEGUID_TARGET_DEVICE_QUERY_REMOVE 值。在这种情况下,我们使用 ObDereferenceObject API 关闭 Fs 对象,并使用 IoUnregisterPlugPlayNotificationEx API 注销通知。

EXTERN_C NTSTATUS DriverNotificationCallback(IN PVOID NotificationStructure,
    IN PVOID Context) {
    PAGED_CODE();
    UNREFERENCED_PARAMETER(Context);

    NTSTATUS Status = STATUS_SUCCESS;
    PLUGPLAY_NOTIFICATION_HEADER * pHeader = 
        (PLUGPLAY_NOTIFICATION_HEADER *)NotificationStructure;
    // Check For Target Device Removal Notification
    if (IsEqualGUID(pHeader->Event, GUID_TARGET_DEVICE_REMOVE_COMPLETE)
        || IsEqualGUID(pHeader->Event, GUID_TARGET_DEVICE_QUERY_REMOVE)
        ) {
        DbgPrint("Device Removed: \"%S\"\n", s_szDeviceName);

        if (s_TargetNotificationEntry) {
            IoUnregisterPlugPlayNotificationEx(s_TargetNotificationEntry);
            s_TargetNotificationEntry = NULL;
        }
        if (s_pCameraFileObject) {
            ObDereferenceObject(s_pCameraFileObject);
            s_pCameraFileObject = NULL;
        }
    }
    return Status;
}

我们将通知句柄传递给 IoUnregisterPlugPlayNotificationEx 函数,该函数之前是从注册结果中收到的。

要测试实现,您应该使用“target”参数启动驱动程序测试应用程序,不带引号。

代码执行结果显示在下一个 DbgView 应用程序的日志中。

处理新设备出现

在大多数情况下,除了检测设备移除请求之外,还有必要检测新设备是否已添加到系统中。例如:在捕获 USB 摄像头数据的应用程序中,该摄像头意外拔出。好的,我们正在处理,但我们想继续从该摄像头捕获数据,并且需要在该摄像头插回时开始执行此操作。根据我们正在开发的内容,还有不同的方法来处理这些事情:常规应用程序、Windows 服务或驱动程序。

WM_DEVICECHANGE

在特定应用程序中,我们可以像之前一样在消息循环中使用 WM_DEVICECHANGE 通知。通过该消息,我们将 DBT_DEVNODES_CHANGED 作为 wParam 参数接收,这表示设备已添加到系统或从系统中移除。要检测哪个设备已添加或移除,我们可以有一个感兴趣的设备初始列表,并检查该列表中的哪些设备已更改。例如,我们可以通过以下方式实现对添加到系统或从系统中移除的摄像头的检测。

初始构建设备列表。

HRESULT BuildDeviceList(std::map<std::wstring,std::wstring> & devices) {

    HRESULT hr;
    CComPtr<ICreateDevEnum> _dev;
    if (S_OK == (hr = _dev.CoCreateInstance
       (CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER)))
    {
        // Enumerate video capture devices
        CComPtr<IEnumMoniker> _enum;
        if (S_OK == (hr = _dev->CreateClassEnumerator
                    (CLSID_VideoInputDeviceCategory, &_enum, 0)))
        {
            USES_CONVERSION;
            CComPtr<IBindCtx> _context;
            CreateBindCtx(0, &_context);
            CComPtr<IMoniker> _moniker;
            ULONG cFetched = 0;
            // While we not got the USB camera or finish
            while (S_OK == _enum->Next(1, &_moniker, &cFetched) && _moniker)
            {
                // Retrieve moniker string
                LPOLESTR pszTemp = NULL;
                if (S_OK == _moniker->GetDisplayName(_context, NULL, &pszTemp) && pszTemp)
                {
                    std::wstring name;
                    std::wstring moniker;
                    moniker = pszTemp;
                    // Check for Hardware Device
                    _wcslwr_s(pszTemp,wcslen(pszTemp) + 1);
                    if (wcsstr(pszTemp, L"@device:pnp:\\\\?\\"))
                    {
                        CComPtr< IPropertyBag > _bag;
                        // Get Name Of the Device
                        if (S_OK == _moniker->BindToStorage(0, 0, 
                                                            __uuidof(IPropertyBag), 
                                                            (void**)&_bag)) {
                            VARIANT _variant;
                            VariantInit(&_variant);
                            _bag->Read(L"FriendlyName", &_variant, NULL);
                            if (_variant.vt == VT_BSTR) {
                                name = _variant.bstrVal;
                            }
                            VariantClear(&_variant);
                        }
                        devices.insert(devices.cend(),
                                        std::pair<std::wstring,std::wstring>(moniker,name));
                    }
                    CoTaskMemFree(pszTemp);
                }
                _moniker.Release();
            }
        }
    }
    return hr;
}

我们在初始时调用构建设备列表,并在接收到带有 DBT_DEVNODES_CHANGEDWM_DEVICECHANGE 的窗口消息循环中调用。下一步是比较通知列表与初始列表以检测已添加或移除的设备。

{
    // Check for the new devices
    auto src = devices.begin();
    while (src != devices.end()) {
        bool bFound = false;
        auto it = g_Devices.begin();
        while (!bFound && it != g_Devices.end()) {
            bFound = (it->first == src->first);
            it++;
        }
        if (!bFound) {
            wprintf(L"Device Added: '%s'\n", src->second.c_str());
        }
        src++;
    }
}
{
    // Check for device removing
    auto src = g_Devices.begin();
    while (src != g_Devices.end()) {
        bool bFound = false;
        auto it = devices.begin();
        while (!bFound && it != devices.end()) {
            bFound = (it->first == src->first);
            it++;
        }
        if (!bFound) {
            wprintf(L"Device Removed: '%s'\n", src->second.c_str());
        }
        src++;
    }
}

一旦我们启动应用程序并启用或禁用摄像头设备,我们就会在控制台窗口中看到以下结果。

当我们在启用摄像头设备后立即看到

Windows 服务

我们已经讨论过 Windows 服务中没有窗口消息循环。但是对于硬件控制,我们也可以使用 RegisterDeviceNotification API。它有一个指定的标志参数 DEVICE_NOTIFY_ALL_INTERFACE_CLASSES。它仅与传递的 DEV_BROADCAST_DEVICEINTERFACE 结构兼容。通过使用此标志,我们可以接收 DBT_DEVICEARRIVALDBT_DEVICEREMOVECOMPLETE 通知,以确定设备何时被安装或移除。

// Register notification for device interfaces
DEV_BROADCAST_DEVICEINTERFACE filter = { 0 };
memset(&filter, 0x00, sizeof(filter));
filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
filter.dbcc_classguid = GUID_NULL;

// Set Flag for all device classes
g_hChangeNotify = RegisterDeviceNotification(m_ServiceHandle,&filter,
                                              DEVICE_NOTIFY_SERVICE_HANDLE 
                                            | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
if (!g_hChangeNotify) {
    std::wostringstream os;
    os << SERVICE_NAME << L": " << L"RegisterDeviceNotificationW Failed: " 
                            << GetLastError() << std::endl;
    OutputDebugStringW(os.str().c_str());
}

与之前的应用程序一样,我们可以构建我们感兴趣的设备初始列表,一旦收到通知,就以相同的方式检查这些设备的更改。

这里的另一种方法是使用传递给回调的参数,这样我们就不需要构建额外的设备列表并每次都进行比较。当我们将通知注册为接收所有接口通知时,DBT_DEVICEARRIVALDBT_DEVICEREMOVECOMPLETE 的通知会将 DEV_BROADCAST_DEVICEINTERFACE 结构指针作为参数传递。通过使用此结构,我们可以确定设备接口及其属性,而无需像我们之前那样构建设备列表。首先,我们将输入结构强制转换为基类型 DEV_BROADCAST_HDR,并检查 dbch_devicetype 字段是否等于 DBT_DEVTYP_DEVICEINTERFACE,然后我们可以将其强制转换为 DEV_BROADCAST_DEVICEINTERFACE 结构以获取设备接口路径。

VOID ServiceDeviceEvent(DWORD dwEventType, LPVOID lpEventData) {

    if (dwEventType == DBT_DEVICEARRIVAL || dwEventType == DBT_DEVICEREMOVECOMPLETE) {
        // Check for new device added or removed
        if (   lpEventData 
            && ((DEV_BROADCAST_HDR*)lpEventData)->dbch_devicetype == 
                 DBT_DEVTYP_DEVICEINTERFACE) {
            DEV_BROADCAST_DEVICEINTERFACE * _interface = 
                                            (DEV_BROADCAST_DEVICEINTERFACE *)lpEventData;
            LPCWSTR path = &_interface->dbcc_name[0];
            if (path && wcslen(path)) {
                std::wostringstream os;
                os << SERVICE_NAME << L": " << L"Device '" << path
                    << (dwEventType == DBT_DEVICEARRIVAL ? L"' installed" : L"' removed") 
                    << std::endl;
                OutputDebugStringW(os.str().c_str());
            }
        }
    }
}

然后,我们就可以在 DbgView 工具中看到事件,并获得完整的接口路径。

我们看到单个设备有多个相同的事件,因为这样的设备只有几个已注册的接口。我们可以向代码添加功能以跳过重复的通知。更重要的是,我们可以通过使用 SetupAPI 库函数从接口路径中获取设备名称。

HDEVINFO _info = SetupDiCreateDeviceInfoList(NULL, 0);
if (_info) {
    SP_DEVICE_INTERFACE_DATA _interface;
    _interface.cbSize = sizeof(_interface);
    if (SetupDiOpenDeviceInterfaceW(_info, path, 0, &_interface)) {
        DEVPROPTYPE Type = 0;
        PWSTR pszName = NULL;
        DWORD Size = 0;
        const DEVPROPKEY Key = DEVPKEY_NAME;
        SetupDiGetDeviceInterfacePropertyW(_info, &_interface, &Key, 
                                           &Type, (PBYTE)pszName, Size, &Size, 0);
        if (Size) {
            pszName = (PWSTR)malloc(Size + 2);
            if (pszName) {
                memset(pszName, 0x00, Size + 2);
                if (SetupDiGetDeviceInterfacePropertyW(_info, &_interface, &Key, 
                                                 &Type, (PBYTE)pszName, Size, &Size, 0)) {
                    name = pszName;
                }
                free(pszName);
            }
        }
    }
    SetupDiDestroyDeviceInfoList(_info);
}

如果以前安装或移除的设备的名称与我们之前收到的名称相同,则跳过将其输出到调试。

static std::wstring last_name;
static DWORD dwLastEventType = 0;
if (!(last_name == name && dwLastEventType == dwEventType)) {
    last_name = name;
    dwLastEventType = dwEventType;
    std::wostringstream os;
    os << SERVICE_NAME << L": " << L"Device '" << name
        << (dwEventType == DBT_DEVICEARRIVAL ? L"' installed" : L"' removed") << std::endl;
    OutputDebugStringW(os.str().c_str());
}

现在我们获得了更好的通知。

为 Windows 服务列出的方法也适用于特定的 Windows 应用程序。在应用程序的情况下,我们只是不注册额外的通知,就像我们为服务所做的那样,而是使用现有的 DBT_DEVNODES_CHANGED

内核模式

在某些实现中,我们需要在驱动程序实现中处理内核模式下的设备。是的,在这种情况下,我们也可以检测到系统上出现了一个新设备。在 Windows 服务实现中,我们接收所有发出设备添加或移除通知的接口。在驱动程序中,我们通过接口处理设备,因此我们可以设置一个通知,一旦指定类型的接口出现在系统上或从系统中消失。这也可以通过我们之前提到的 IoRegisterPlugPlayNotification 函数来完成。在该函数中,我们可以指定我们想要检查的接口 guid。接口 guid 就像设备类别一样,例如,我们可以检查是否有新的可移动媒体或新的蓝牙设备被添加。

与之前的示例一样,我们检查视频捕获设备。因此,我们感兴趣的接口类别是 KSCATEGORY_VIDEO。通知注册代码如下。

// Register Device Notification
GUID Interface = KSCATEGORY_VIDEO;
Status = IoRegisterPlugPlayNotification(EventCategoryDeviceInterfaceChange,
    0, &Interface, DriverObject, DriverNotificationCallback, NULL, &s_NotificationEntry);

DbgPrint("IoRegisterPlugPlayNotification Status: 0x%08x", Status);

我们传递一个回调函数,该函数将在设备接口添加或移除时被调用。通过回调,我们还可以接收已添加或移除的设备接口的符号链接;并像在 Windows 服务实现中所做的那样,使用 DbgPrint 显示它们。

EXTERN_C NTSTATUS DriverNotificationCallback
    (IN PVOID NotificationStructure, IN PVOID Context) {
    PAGED_CODE();
    UNREFERENCED_PARAMETER(Context);

    NTSTATUS Status = STATUS_SUCCESS;
    PLUGPLAY_NOTIFICATION_HEADER * pHeader = 
       (PLUGPLAY_NOTIFICATION_HEADER *)NotificationStructure;
    // Check For Device Interface Notification
    if (IsEqualGUID(pHeader->Event, GUID_DEVICE_INTERFACE_ARRIVAL)
        || IsEqualGUID(pHeader->Event, GUID_DEVICE_INTERFACE_REMOVAL)
        ) {
        DEVICE_INTERFACE_CHANGE_NOTIFICATION * pNotification = 
                              (DEVICE_INTERFACE_CHANGE_NOTIFICATION *)pHeader;
        if (pNotification->SymbolicLinkName->Length && 
                           pNotification->SymbolicLinkName->Buffer) {
            size_t cch = pNotification->SymbolicLinkName->Length + 2;
            PWCHAR DisplayName = (PWCHAR)ExAllocatePool(NonPagedPool, cch);
            if (DisplayName) {
                memset(DisplayName, 0x00, cch);
                wcscpy_s(DisplayName, cch >> 1, pNotification->SymbolicLinkName->Buffer);
            }
            BOOLEAN bAdded = (IsEqualGUID(pHeader->Event, GUID_DEVICE_INTERFACE_ARRIVAL) != 0);
            DbgPrint("%S: Device %s: \"%S\"\n", DRIVER_NAME, 
                        bAdded ? "Added" : "Removed", DisplayName ? DisplayName : L"");
            if (DisplayName) {
                ExFreePool(DisplayName);
            }
        }
    }
}

要注销通知,请使用 IoUnregisterPlugPlayNotificationEx API。

if (s_NotificationEntry) {
    IoUnregisterPlugPlayNotificationEx(s_NotificationEntry);
    s_NotificationEntry = NULL;
}

要测试实现,您应该使用“all”参数启动驱动程序测试应用程序,不带引号。

我们可以在 DbgView 应用程序中看到结果。

与 Windows 服务一样,我们还应该改进显示,使其稍微美观一点,加上设备名称。我们可以使用注册表来获取设备名称信息。设备实例的初始化函数将无法正常工作,因为回调是在设备已被移除时调用的,您无法初始化它。因此,IoGetDeviceObjectPointer 函数在移除通知的情况下会失败。但是,在 Windows 服务实现中,我们可以在设备移除回调时检索设备名称。在 Windows 服务实现中,我们使用了 SetupDiGetDeviceInterfaceProperty 函数,该函数可以访问指定接口的属性。这些属性位于注册表中,即使设备已被移除,我们也可以访问它们。我们感兴趣的属性是“FriendlyName”,它表示捕获设备的名称。

PWCHAR DisplayName = NULL;
HANDLE hKey = NULL;
// Get Device Friendly Name From The Registry
if (STATUS_SUCCESS == (Status = IoOpenDeviceInterfaceRegistryKey
                      (pNotification->SymbolicLinkName, GENERIC_READ, &hKey))) {
    UNICODE_STRING sTemp;
    RtlInitUnicodeString(&sTemp,L"FriendlyName");
    ULONG cb = sizeof(KEY_VALUE_FULL_INFORMATION) + 512;
    PKEY_VALUE_FULL_INFORMATION info = 
        (PKEY_VALUE_FULL_INFORMATION)ExAllocatePool(NonPagedPool,cb);
    if (info) {
        Status = ZwQueryValueKey(hKey,&sTemp,KeyValueFullInformation,
            info,cb,&cb);
        if (NT_SUCCESS(Status)) {
            DisplayName = (PWCHAR)ExAllocatePool(NonPagedPool, info->DataLength + 2);
            if (DisplayName) {
                memset(DisplayName, 0x00, info->DataLength + 2);
                memcpy(DisplayName,(PUCHAR)info + info->DataOffset,info->DataLength);
            }
        }
        else {
            DbgPrint("%S: ZwQueryValueKey Status: 0x%08x\n", DRIVER_NAME, Status);
        }
        ExFreePool(info);
    } else {
        Status = STATUS_INSUFFICIENT_RESOURCES;
    }
    ZwClose(hKey);
} else {
    DbgPrint("%S: IoOpenDeviceInterfaceRegistryKey Status: 0x%08x\n", DRIVER_NAME, Status);
}

现在我们可以获取设备的正确名称并进行显示。

旧版即插即用驱动程序的问题

总之,我们不仅针对摄像头设备,还需要了解整个机制。通知在上面示例的捕获设备上工作正常。但是如果我们想检测一个特定的驱动程序,例如 DbgView 工具的驱动程序:dbgv.sys,它存在我之前文章中讨论过的驱动程序卸载问题怎么办?

例如,我们从上一个示例中获取简单的旧版驱动程序,并在应用程序中,我们尝试使用该驱动程序的句柄使用 RegisterDeviceNotification API 设置移除通知。在这种情况下,RegisterDeviceNotification API 将失败,错误代码为 1066(ERROR_SERVICE_SPECIFIC_ERROR)。

出现此问题是因为我们的驱动程序不支持即插即用功能,并且是手动安装的——而不是由即插即用管理器安装的。因此,IO管理器无法通知设备已添加或移除。

即插即用驱动程序实现

要在驱动程序中添加即插即用支持,必须为 IRP_MJ_PNPIRP_MJ_POWER 通知添加 PnP 分派例程。此外,新设备的添加不应直接在驱动程序入口例程中进行,而应通过特殊的 AddDevice 处理程序函数进行。

DriverObject->DriverExtension->AddDevice = DriverAddDevice;
DriverObject->MajorFunction[ IRP_MJ_PNP ] = DriverDispatchPnp;
DriverObject->MajorFunction[ IRP_MJ_POWER ] = DriverDispatchPower;

IO 设备管理器将调用 AddDevice 例程。在此例程中,我们以常规方式创建设备(使用 IoCreateDevice),并通过调用 IoAttachDeviceToDeviceStack 将其放入设备堆栈。

Extension->TopOfStack = IoAttachDeviceToDeviceStack(DeviceObject, PhysicalDeviceObject);
DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

if (!Extension->TopOfStack) {
    IoDeleteSymbolicLink( &LinkName );
    IoDeleteDevice( DeviceObject );
    DbgPrint("IoAttachDeviceToDeviceStack Failed ");
    return STATUS_UNSUCCESSFUL;
}

IRP_MJ_POWER 处理程序中的 IRP 数据包可以通过调用 PoStartNextPowerIrp 来跳过,并调用堆栈中的下一个驱动程序,因为我们不依赖于实际硬件。

PoStartNextPowerIrp(Irp);
IoSkipCurrentIrpStackLocation(Irp);
return PoCallDriver(Extension->TopOfStack, Irp);

主要功能放在 IRP_MJ_PNP 处理程序例程中。此处理程序负责启动和停止设备,我们可以在设备管理器控制台中执行这些操作。该分派例程还处理设备移除及相关请求。通常在启动设备请求时,驱动程序会使接口可用于通信。这是通过 IoSetDeviceInterfaceState 函数完成的。在此期间,已注册接口通知的应用程序会收到带有 DBT_DEVICEARRIVAL 作为参数的 WM_DEVICECHAGE。在设备移除的情况下,接口使用上述相同函数禁用,应用程序会收到每个接口的 DBT_DEVICEREMOVE 通知。这就是为什么对于单个设备,我们可以收到多个接口添加或移除的通知。如果应用程序注册了设备移除通知(针对设备句柄),并且设备在设备管理器控制台中被禁用,那么首先会发送 DBT_DEVICEQUERYREMOVE,在此期间驱动程序会收到 IRP_MN_QUERY_REMOVE_DEVICE 请求——以检查驱动程序是否可以从系统中移除。之后,驱动程序会收到 IRP_MN_REMOVE_DEVICE。此时,在驱动程序中,我们从堆栈中分离设备。然后,应用程序收到 DBT_DEVICEREMOVECOMPLETE 通知。如果设备在驱动程序中被拔出,我们会收到 IRP_MN_SURPRISE_REMOVAL,然后是 IRP_MN_REMOVE_DEVICE

从驱动程序实现的角度来看,设备启动是通过 IRP_MN_START_DEVICE PnP 通知执行的。

case IRP_MN_START_DEVICE:

IoCopyCurrentIrpStackLocationToNext (Irp);
KeInitializeEvent(&Extension->StartEvent, NotificationEvent, FALSE);
IoSetCompletionRoutine (Irp,
    DriverPnPComplete,
    Extension,
    TRUE,
    TRUE,
    TRUE); // No need for Cancel

Irp->IoStatus.Status = STATUS_SUCCESS;
Status = IoCallDriver (TopDeviceObject, Irp);
if (STATUS_PENDING == Status) {
    KeWaitForSingleObject(
        &Extension->StartEvent,
        Executive, // Waiting for reason of a driver
        KernelMode, // Waiting in kernel mode
        FALSE, // No allert
        NULL); // No timeout

    Status = Irp->IoStatus.Status;
}

if (NT_SUCCESS(Status)) {

    DriverStartDevice(Extension->DriverObject);
}
else {
    DbgPrint("IRP_MN_START_DEVICE Failed 0x%08x", Status);
}

Irp->IoStatus.Status = Status;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);

break;

在设备启动期间,我们应该通知堆栈中的所有驱动程序,然后才更改我们的状态。为此,我们使用 IoSetCompletionRoutine API 设置一个完成例程,并通过 IoCallDriver 调用堆栈中的下一个驱动程序。在完成回调中,我们设置通知事件,一旦完成,直到此时我们在函数中等待此事件,如果我们从 IoCallDriver 调用获得了 STATUS_PENDING。之后,我们的设备启动,我们可以调用我们的函数 DriverStartDevice

设备移除请求 PnP 通知通过 IRP_MN_REMOVE_DEVICE 发送。

case IRP_MN_REMOVE_DEVICE:

    IoAcquireRemoveLock(&Extension->RemoveLock,NULL);

    IoReleaseRemoveLockAndWait(&Extension->RemoveLock,NULL);

    DriverStopDevice(Extension->DriverObject);

    // Unload the callbacks from the kernel to this driver
    IoDetachDevice(Extension->TopOfStack); 
    IoDeleteDevice(Extension->Self);    

    Irp->IoStatus.Status = Status;
    Irp->IoStatus.Information = 0;
    IoSkipCurrentIrpStackLocation(Irp);
    Status = IoCallDriver(TopDeviceObject, Irp);
    break;

DriverStopDeviceDriverStartDevice 函数对于 PnP 和非 PnP 驱动程序共享相同的功能,因为我将它们的实现放在了一个 cpp 文件中。测试应用程序也是如此。PnP 实现只有单独的项目,它有 PNP_DRIVER 定义,允许构建另一种驱动程序类型和测试它的应用程序。

由于设备启动和停止是通过设备管理器执行的,因此我们需要一个安装脚本 *.inf 文件来设置驱动程序。它也可以基于 PnP 驱动程序项目中的 *.inx 文件模板自动生成。

要安装驱动程序,您可以使用 DevCon 工具 或手动添加驱动程序。由于我们的驱动程序不依赖于实际硬件,我们应该在设备管理器控制台的“**操作**”菜单中使用“**添加旧版硬件**”来安装它。选择后,指定 *.inf 文件的路径,您将看到设备选择对话框。

安装后,您可以在设备管理器树的“**示例设备**”下找到该设备。

现在您可以启动测试应用程序。它无法在启动时手动安装驱动程序,并且应该检测到已安装的驱动程序。现在您可以看到 RegisterDeviceNotification API 的调用没有失败。

当设备在设备管理器中禁用时,应用程序可以正确接收设备移除通知。

检测我们的驱动程序是否正在运行

要能够处理添加或移除的驱动程序,我们需要检查驱动程序是否正在系统上运行。如果我们知道驱动程序的符号链接或其接口,那不是问题。我们通过接口枚举实例并检查其硬件 ID。但是如果我们只有驱动程序文件名,例如旧版驱动程序怎么办?有几种方法可以找出。

使用 ntdll

要检测指定驱动程序是否正在系统上运行,可以使用从 ntdll 库导出的 NtQuerySystemInformation API。在这种情况下,我们应该通过将 SystemModuleInformation 类型作为 SYSTEM_INFORMATION_CLASS 参数传递给上述函数来枚举系统加载的模块。函数调用的结果将是 RTL_PROCESS_MODULES 结构,其中填充了系统模块信息。此结构声明如下

typedef struct _RTL_PROCESS_MODULES {
    ULONG NumberOfModules;
    RTL_PROCESS_MODULE_INFORMATION Modules[ 1 ];
} RTL_PROCESS_MODULES, *PRTL_PROCESS_MODULES;

NumberOfModules 包含以下 RTL_PROCESS_MODULE_INFORMATION 结构的编号,该结构具有以下声明。

typedef struct _RTL_PROCESS_MODULE_INFORMATION {
    HANDLE Section;
    PVOID MappedBase;
    PVOID ImageBase;
    ULONG ImageSize;
    ULONG Flags;
    USHORT LoadOrderIndex;
    USHORT InitOrderIndex;
    USHORT LoadCount;
    USHORT OffsetToFileName;
    UCHAR  FullPathName[ 256 ];
} RTL_PROCESS_MODULE_INFORMATION, *PRTL_PROCESS_MODULE_INFORMATION;

在此结构中,我们感兴趣的是 FullPathName 字段,它包含驱动程序的完整路径,包括其文件名。

需要调用 NtQuerySystemInformation 函数两次:第一次获取分配输入缓冲区所需的内存量,第二次检索数据。

ULONG Length = 0x1000;
PVOID p = NULL;
while (TRUE) {
    ULONG Size = Length;
    Status = STATUS_NO_MEMORY;
    p = realloc(p,Size);
    if (p) {
        Status = NtQuerySystemInformation(
            (SYSTEM_INFORMATION_CLASS)SystemModuleInformation, p, Size, &Length);
        if (Status == STATUS_INFO_LENGTH_MISMATCH) {
            Length = (Length + 0x1FFF) & 0xFFFFE000;
            continue;
        }
    }
    break;
}

之后,在循环中,我们枚举收到的模块信息,并从中格式化路径字段以进行正确显示。

RTL_PROCESS_MODULES * pm = (RTL_PROCESS_MODULES *)p;
ULONG idx = 0; 
while (idx < pm->NumberOfModules) {
    PRTL_PROCESS_MODULE_INFORMATION mi = &pm->Modules[idx++];
    char path[512] = {0};
    if (strlen((const char*)mi->FullPathName)) {
        char * s = (char *)mi->FullPathName;
        if (_strnicmp(s,"\\??\\",4) == 0) s += 4;
        if (_strnicmp(s, "\\SystemRoot\\", 12) == 0) {
            sprintf_s(path,"%%SystemRoot%%\\%s",s + 12);
            char temp[512] = {0};
            if (ExpandEnvironmentStringsA(path, temp, _countof(temp))) {
                strcpy_s(path,temp);
            }
        }
        else {
            sprintf_s(path,"%s",s);
        }
        printf("\"%s\"\n", path);
    }
}

为了检查此实现的正确输出,我们可以启动非 PnP 驱动程序测试应用程序,该应用程序安装并加载驱动程序。然后,此测试应用程序枚举系统模块。您可以在下一个屏幕截图中看到结果。它显示驱动程序已加载到系统中。

使用服务 API

另一种方法是使用服务 API 来检查指定驱动程序是否正在系统上运行。正如我们所见,驱动程序由服务管理器启动,特别是我们从测试应用程序安装和加载的旧版非 PnP 驱动程序。服务函数 EnumServicesStatusEx 可以根据指定的类型标志枚举驱动程序和 Windows 服务。它也应该被调用几次,第一次用于缓冲区大小请求,第二次用于实际数据。

DWORD Type = SERVICE_KERNEL_DRIVER | SERVICE_WIN32_OWN_PROCESS;
while (true) {
    if (!EnumServicesStatusExW(
        hServiceManager,
        SC_ENUM_PROCESS_INFO,
        Type,
        SERVICE_ACTIVE,
        (LPBYTE)Services,
        cbServices,
        &cb,
        &nbServices,
        NULL,
        NULL
    )) {
        if (GetLastError() == ERROR_MORE_DATA && cb) {
            cbServices = cb;
            Services = (LPENUM_SERVICE_STATUS_PROCESSW)realloc(Services, cbServices);
            continue;
        }
    }
    break;
}

该函数调用的结果是 ENUM_SERVICE_STATUS_PROCESS 结构的数组。这样的结构不包含服务或驱动程序二进制文件的路径。要请求完整路径,我们应该通过 ENUM_SERVICE_STATUS_PROCESS 结构中的名称打开服务句柄,并调用 QueryServiceConfig 函数。此函数填充 QUERY_SERVICE_CONFIG 结构。

auto p = &Services[i];
if (Config) {
    memset(Config, 0x00, cbConfig);
}
SC_HANDLE hService = OpenServiceW(hServiceManager,
    p->lpServiceName,SERVICE_QUERY_CONFIG | SERVICE_QUERY_STATUS);
if (hService) {
    while (true) {
        if (!QueryServiceConfigW(hService, Config, cbConfig, &cb)) {
            if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
                cbConfig = cb;
                Config = (LPQUERY_SERVICE_CONFIGW)realloc(Config, cbConfig);
                continue;
            }
        }
        break;
    }
    CloseServiceHandle(hService);
}

收到的结构包含二进制文件的路径,但我们应该对其进行格式化以正确显示。

WCHAR path[512] = {0};
WCHAR * s = Config && Config->lpBinaryPathName ? Config->lpBinaryPathName : L"";
if (_wcsnicmp(s,L"\\??\\",4) == 0) s += 4;
if (_wcsnicmp(s, L"System32\\", 9) == 0) {
    swprintf_s(path,L"%%SystemRoot%%\\%s",s);
}
if (_wcsnicmp(s, L"\\SystemRoot\\", 12) == 0) {
    swprintf_s(path,L"%%SystemRoot%%\\%s",s + 12);
}
if (!wcslen(path)) {
    swprintf_s(path,L"%s",s);
}
WCHAR temp[512] = {0};
if (ExpandEnvironmentStringsW(path, temp, _countof(temp))) {
    wcscpy_s(path,temp);
}
if (Config && Config->dwServiceType == SERVICE_KERNEL_DRIVER) {
    wprintf(L"'%s' \"%s\"\n", 
        p->lpServiceName, 
        path);
}
else {
    wprintf(L"'%s' %d \"%s\" %d\n", 
        p->lpServiceName, 
        Config ? Config->dwServiceType : 0,
        path,
        p->ServiceStatusProcess.dwProcessId);
}

测试应用程序的结果显示在下一个屏幕截图中。

检测旧版驱动程序的卸载或安装

现在,既然我们可以枚举已安装的驱动程序和服务,我们就可以将这些驱动程序的列表与已更改的列表进行比较,从而检测已安装或移除的驱动程序。这也适用于旧版驱动程序,因为它们有自己的可执行文件。我们只需要找出使用什么来获取此类通知。设置回调的函数是 SubscribeServiceChangeNotifications API。在该函数中,我们应该指定 SC_EVENT_DATABASE_CHANGE 通知类型,以便在服务启动或停止时收到通知。在这种情况下传递的第一个参数是通过 OpenSCManager API 打开的服务句柄。

SC_HANDLE hServiceManager = OpenSCManager(
    NULL,
    SERVICES_ACTIVE_DATABASE,
    SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_CONNECT
);

PSC_NOTIFICATION_REGISTRATION Registration = NULL;

// Subscribe ro notifications
DWORD Result = SubscribeServiceChangeNotifications(
      hServiceManager,SC_EVENT_DATABASE_CHANGE,NotificationCallback,NULL,&Registration);

在通知回调中,我们只需获取服务和驱动程序的实际列表,并将其与之前保存的列表进行比较。要获取这样的列表,我们可以使用前面描述的方法之一。

// Notification Callback
VOID CALLBACK NotificationCallback(DWORD dwNotify, PVOID pCallbackContext) {
    // Get Current list of services
    ENUM_SERVICE_STATUS_PROCESSW * Services = NULL;
    DWORD nbServices = GetServices(&Services);

    if (Services) {
        std::vector<ENUM_SERVICE_STATUS_PROCESSW*> added;
        std::vector<ENUM_SERVICE_STATUS_PROCESSW*> removed;
        EnterCriticalSection(&s_Lock);
        // Check current list for added new services
        for (DWORD i = 0; i < nbServices; i++) {
            BOOLEAN bFound = FALSE;
            for (DWORD j = 0; j < s_nbServices && !bFound; j++) {
                bFound = (_wcsicmp(Services[i].lpServiceName, s_pServices[j].lpServiceName) == 0);
            }
            if (!bFound) {
                added.push_back(&Services[i]);
            }
        }
        // Check current list for removing services
        for (DWORD i = 0; i < s_nbServices; i++) {
            BOOLEAN bFound = FALSE;
            for (DWORD j = 0; j < nbServices && !bFound; j++) {
                bFound = (_wcsicmp(Services[j].lpServiceName, s_pServices[i].lpServiceName) == 0);
            }
            if (!bFound) {
                removed.push_back(&s_pServices[i]);
            }
        }
        // Save new list 
        Services = (ENUM_SERVICE_STATUS_PROCESSW *)InterlockedExchangePointer(
                            (volatile PVOID*)&s_pServices, Services);
        s_nbServices = nbServices;
        LeaveCriticalSection(&s_Lock);

        while (added.size()) {
            auto it = added.begin();
            wprintf(L"Service '%s' added!\n", (*it)->lpServiceName);
            added.erase(it);
        }
        while (removed.size()) {
            auto it = removed.begin();
            wprintf(L"Service '%s' removed!\n", (*it)->lpServiceName);
            removed.erase(it);
        }
        if (Services) free(Services);
    }
}

一旦我们检查了驱动程序或服务的添加或移除,我们就保存更新的列表。要停止接收通知,应使用 UnsubscribeServiceChangeNotifications API。我们可以在下一个屏幕截图中看到测试应用程序的结果。

当我们启动驱动程序测试应用程序时,它会收到一个设备添加事件,然后当应用程序退出时,它会引发移除事件。

检测目标旧版驱动程序的卸载

现在回到目标设备,我们有了一个旧版的非 PnP 驱动程序,它可以被独立于我们的应用程序的其他应用程序使用,我们想检测这个驱动程序即将被移除,例如另一个应用程序将其卸载。要做到这一点,我们也可以使用上面提到的 SubscribeServiceChangeNotifications API。

为了在我们的测试应用程序中检查这一点,我们通过命令行参数添加了安装、卸载和加载此驱动程序的功能。要使用 SubscribeServiceChangeNotifications API,我们使用 OpenService 服务管理器 API 打开驱动程序服务。我们将该服务句柄传递给 SubscribeServiceChangeNotifications API,而不是前一个示例中的服务管理器句柄,并使用 SC_EVENT_STATUS_CHANGE 通知值作为事件类型参数。

SC_HANDLE hServiceManager = OpenSCManager(
    NULL,
    SERVICES_ACTIVE_DATABASE,
    SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_CONNECT
);
if (hServiceManager) {

    hService = OpenServiceW(hServiceManager,
        DriverName, SERVICE_QUERY_CONFIG | SERVICE_QUERY_STATUS);

    if (hService) {

        DWORD Result = SubscribeServiceChangeNotifications(hService, 
                        SC_EVENT_STATUS_CHANGE, NotificationCallback, NULL, &Registration);
        if (Result) {
            _tprintf(_T("SubscribeServiceChangeNotifications Failed %d\n"), Result);
        }
    }
    CloseServiceHandle(hServiceManager);
}

之后,在 NotificationCallback 函数中,当注册的驱动程序即将被移除时,我们可以收到 SERVICE_NOTIFY_DELETE_PENDING 通知。

VOID CALLBACK NotificationCallback(DWORD dwNotify, PVOID pCallbackContext) {
    // Legacy Device Removal Pending
    if (dwNotify == SERVICE_NOTIFY_DELETE_PENDING) {
        // Signal that device is gone
        SetEvent(g_hNotify);
    }
}

与 PnP 通知实现一样,我们关闭对我们驱动程序的所有句柄,并使用 UnsubscribeServiceChangeNotifications API 移除通知。

// Notification
if (WaitForSingleObject(g_hNotify, 0) == WAIT_OBJECT_0) {
    // Close The Driver Handle 
    CloseHandle(hDevice);
    hDevice = NULL;
    if (hService) {
        CloseServiceHandle(hService);
        hService = NULL;
    }
    if (Registration) {
        UnsubscribeServiceChangeNotifications(Registration);
        Registration = NULL;
    }
    if (notify) {
        UnregisterDeviceNotification(notify);
    }
    notify = NULL;
    ResetEvent(g_hNotify);
    _tprintf(_T("Driver '%s' removed from system, press any key for quit\n"),
        DriverName);
}

我们不带参数启动测试应用程序,然后它会安装旧版驱动程序并开始等待其被移除。之后,我们使用“uninstall”参数启动同一个应用程序来执行卸载该旧版驱动程序的操作。在先前运行的应用程序中,我们看到驱动程序变得不可用的信息。

不同技术中的通知特殊情况

某些技术有自己用于设备移除或添加通知的辅助实现,可以与上述方法一起使用。在这里,我们看其中一些。

DirectShow 通知

在 DirectShow 技术上,我们有两种通知方式,由两个接口表示:IMediaEventIMediaEventEx。第一个接口在同一线程中处理通知。我们只需要获取通知句柄并等待新事件的出现。使用第二个接口,我们设置窗口句柄并在窗口过程中接收通知。这两个接口都可以从过滤器图对象 IGraphBuilder 中获得。

HANDLE hEvent = NULL;
CComPtr<IMediaEventEx> _event;
hr = _graph->QueryInterface(&_event);
if (hr == S_OK) {
    if (IsWindow(hWnd)) {
        hr = _event->SetNotifyWindow((OAHWND)hWnd, WM_GRAPHNOTIFY, (LONG_PTR)_event.p);
    }
    else {
        hr = _event->GetEventHandle((OAEVENT*)&hEvent);
    }
    if (hr == S_OK) {
        _event->SetNotifyFlags(0);
        _event->CancelDefaultHandling(EC_DEVICE_LOST);
    }
}

两种情况下的事件处理都相似。一旦我们收到事件通知,我们就处理队列中的所有事件。我们对 EC_DEVICE_LOST 事件类型感兴趣。当捕获设备丢失时会发送此事件。

long evCode = 0;
LONG_PTR p1,p2;
while (S_OK == _event->GetEvent(&evCode,&p1,&p2,0)) {

    if (evCode == EC_DEVICE_LOST) {
        if (p2 == 0) {
            _tprintf(_T("Camera '%s' removed from system, press any key for quit\n"),
                g_szCameraName);
            CComPtr<IVideoWindow> _window;
            if (S_OK == _graph->QueryInterface(&_window)) {
                _window->put_Visible(OAFALSE);
            }
        }
        if (p2 == 1) {
            _tprintf(_T("Camera '%s' available again: necessary to rebuild the graph\n"),
                g_szCameraName);
        }
    }
    _event->FreeEventParams(evCode,p1,p2);
}

如果我们启动一个测试捕获应用程序并拔出屏幕上显示的 USB 摄像头,我们会看到以下结果。

这是在您拔出所选 USB 摄像头的情况下发生的,在设备管理器中禁用摄像头仍然会导致弹出重启请求,正如我们之前所见。因此,要在 DirectShow 中正确处理捕获设备移除,需要使用前面描述的通知方法,而不是依赖该技术提供的机制。

EC_DEVICE_LOST 可以通知应用程序之前丢失的设备再次可用。但在所有这些情况下,捕获图都应该被重建以继续播放。

媒体基础

Media Foundation 本身不提供特殊的移除通知机制。它建议使用我们之前描述的方法。尽管 Media Foundation 应用程序需要请求捕获设备源的符号链接,但它不使用实际的设备句柄来确定设备移除。在实现中,我们注册了一个针对 KSCATEGORY_CAPTURE 设备类别和 DBT_DEVTYP_DEVICEINTERFACE 类型的通知。

DEV_BROADCAST_DEVICEINTERFACE filter = { 0 };
memset(&filter, 0x00, sizeof(filter));
filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
filter.dbcc_classguid = KSCATEGORY_CAPTURE;

g_hNotify = RegisterDeviceNotification(hWnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE);

在 Windows 过程中,我们可以接收带有 DBT_DEVICEARRIVALDBT_DEVICEREMOVECOMPLETE 通知信息的 WM_DEVICECHANGE 消息。我们检查我们正在使用的设备的已保存符号链接,并以此方式检测我们的设备是否已被移除或添加。

DEV_BROADCAST_HDR *Header = (DEV_BROADCAST_HDR *)lParam;
if (DBT_DEVICEARRIVAL == wParam || DBT_DEVICEREMOVECOMPLETE == wParam) {
    if (Header->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) {
        DEV_BROADCAST_DEVICEINTERFACE * Interface = (DEV_BROADCAST_DEVICEINTERFACE *)Header;
        if (_wcsicmp(g_szSymbolicLink, Interface->dbcc_name)) {
            static bool removed = false;
            if (DBT_DEVICEREMOVECOMPLETE == wParam) {
                if (!removed) {
                    removed = true;
                    wprintf(L"Camera '%s' removed from system, press any key for quit\n", 
                            g_szCameraName);
                    ShowWindow(hWnd, SW_HIDE);
                }
            }
            else {
                if (removed) {
                    removed = false;
                    wprintf(L"Camera '%s' available again: necessary to rebuild the graph\n", 
                            g_szCameraName);
                }
            }
        }
    }
}

在测试应用程序中,我们使用上面列出的通知方法实现了 Media Foundation 的简单捕获设备播放。我们启动它并拔出所选摄像头的 USB 线以查看测试结果。

从屏幕截图可以看出,它工作正常。它也能在摄像头移除后正确检测到摄像头重新插入。

Media Foundation 在内部处理实际硬件,因此我们不需要访问设备句柄。因此,当摄像头在设备管理器中被禁用时,Media Foundation 也能正常工作,并且不会弹出重启请求。

在 Media Foundation 中,当我们拥有捕获设备的符号链接时,这并不意味着它是实际的设备接口。因为 Media Foundation 中的捕获设备由 FrameServer 服务管理。在那里,它可能会为实际设备创建符号链接的别名,以便摄像头可以被多个应用程序共享。这就是为什么需要在窗口过程中比较符号链接。也许以后,我会描述 Media Foundation 中的所有这些工作方式,因为它超出了本文的范围。

MM 设备 API

MM Device 技术操作音频设备。它跟踪新设备的到来和现有设备状态的变化。实际上,如果音频设备在系统中注册,它就会保存在注册表中,一旦被拔出,信息仍然保留,只是该设备的状态发生了变化。该技术不操作实际硬件,但它使用中间功能终结点层。每个终结点代表底层硬件的输入或输出。实际硬件位于设备管理器树的“**声音视频和游戏控制器**”下。

但是,您可以在设备管理器中的“**音频输入和输出**”部分找到终结点设备。

这样做是因为实际硬件可能有多个输入,如麦克风或线路输入,输出也一样:扬声器、S/PDIFF 等。

与实际硬件的通信是通过共享模式下的混音器完成的,并且仅在独占模式下直接完成。在独占模式下,我们还有一个用于与硬件通信的底层缓冲区。因此,与前面 DirectShow 摄像头的例子相比,我们只访问了系统提供的音频功能的一小部分。这意味着在设备管理器中禁用音频的实际硬件不会要求我们重启系统,因为系统组件会内部管理设备移除。

在应用程序中,一旦设备变得不可用,您只会从使用的音频组件中获得失败代码。但是,通过 MMDevice API,我们可以跟踪设备状态。让我们看一个实现示例。

MMDevice API 具有 IMMNotificationClient 接口,该接口能够接收我之前提到的通知。我们创建一个测试应用程序并在测试类上实现此接口。之后,我们将它传递给 IMMDeviceEnumerator 对象的 RegisterEndpointNotificationCallback 方法。

CComPtr<IMMDeviceEnumerator> enumerator = nullptr;
CComPtr<IMMNotificationClient> client = nullptr;

hr = enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL);
if (hr == S_OK) {

    CMMNotificationClient * notify = new CMMNotificationClient();
    if (S_OK == (hr = notify->QueryInterface(__uuidof(IMMNotificationClient), 
                (void**)&client))) {
        hr = enumerator->RegisterEndpointNotificationCallback(notify);
    }
    notify->Release();
}

在我们的回调接口实现中,我们应该处理 OnDeviceStateChanged 实现,该实现将终结点 ID 字符串及其状态作为参数接收。

STDMETHODIMP OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState)
{
    CHAR State[50] = "UNKNOWN";
#define CHECK_STATE(x) if (dwNewState == x) strcpy_s(State,&(#x)[13])
    CHECK_STATE(DEVICE_STATE_ACTIVE);
    CHECK_STATE(DEVICE_STATE_DISABLED);
    CHECK_STATE(DEVICE_STATE_NOTPRESENT);
    CHECK_STATE(DEVICE_STATE_UNPLUGGED);
    wprintf(L"MMDevice [%s] Device State Changed: %d [%S]\n",pwstrDeviceId,dwNewState,State);
#undef CHECK_STATE
    return S_OK;
}

当终结点状态改变时,会调用此方法,并传递新状态。在我们的实现中,我们只是将信息输出到控制台窗口。

要进行测试,我们启动一个应用程序,并在设备管理器中启用或禁用音频捕获设备。

在输出中,您可以看到应用程序正确处理目标捕获设备的状态更改。但是,如果您在控制面板中禁用终结点,则实际设备不会从系统中移除,并且在输出中我们会得到“disabled”状态。

通过在音频控制面板中禁用终结点,只有该终结点会从设备管理器中的“音频输入和输出”树中移除。

媒体卷驱动程序与 Shell API

这是一种特殊情况,可用于确定闪存卡或任何硬盘驱动器何时插入或拔出系统。此方法使用 Shell API。要使用它,我们应该为添加或移除媒体驱动器注册 Shell 通知。这可以通过 SHChangeNotifyRegister API 来完成。

const int Sources = SHCNRF_InterruptLevel | SHCNRF_ShellLevel | SHCNRF_NewDelivery;
const int Events = SHCNE_DRIVEADD | SHCNE_DRIVEREMOVED; 
ULONG Register = SHChangeNotifyRegister(hWnd, Sources, Events, WM_SHELLNOTIFY, 1, &entry);

此类通知会通过我们定义的并且在注册函数中指定的标识符消息传递到窗口过程中。由于我们在注册期间设置了 SHCNRF_NewDelivery 标志,因此要访问数据,我们应该调用 SHChangeNotification_Lock API,并且在数据访问完成后,我们应该调用 SHChangeNotification_Unlock API。通过这些,我们可以检索通知事件和 PIDLIST 参数。从这些数据中,我们可以获得插入或移除的驱动器的路径。

LRESULT CALLBACK WindowProcHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (WM_SHELLNOTIFY == uMsg) {

        PIDLIST_ABSOLUTE *list = NULL;
        LONG Event = 0;
        HANDLE hLock = SHChangeNotification_Lock((HANDLE)wParam, 
                                                (DWORD)lParam, &list, &Event);
        if (hLock)
        {
            if (list && (Event == SHCNE_DRIVEADD || Event == SHCNE_DRIVEREMOVED)) {
                CComPtr<IShellItem2> item;
                WCHAR Path[MAX_PATH] = { 0 };
                if (S_OK == SHCreateItemFromIDList(list[0], 
                            __uuidof(IShellItem2), (void**)&item)) {
                    LPOLESTR name = NULL;
                    if (S_OK == item->GetDisplayName(SIGDN_FILESYSPATH, &name) && name) {
                        wcscpy_s(Path, name);
                        CoTaskMemFree(name);
                    }
                }
                wprintf(L"Event %s %s\n", Path, 
                        Event == SHCNE_DRIVEADD ? L"Added" : L"Removed");
            }
            SHChangeNotification_Unlock(hLock);
        }
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

要注销,您应该使用 SHChangeNotifyDeregister API 并传递先前从注册例程中返回的值。

// Unregister Notification
if (Register) {
    SHChangeNotifyDeregister(Register);
}

例如,我们启动一个测试应用程序,然后插入和拔出闪存驱动器。结果显示在下一个屏幕截图中。

结论

当我们直接在应用程序中使用硬件时,我们应该注意该设备可能会被移除。有一些技术可以帮助我们避免重启请求,但其中一些并不能完全保证这一点。因此,我们必须记住硬件可能会被移除,并在您的实现中测试这种情况。

历史

  • 2023年10月13日:初始版本
© . All rights reserved.