Click here to Skip to main content
15,887,135 members
Articles / Desktop Programming / System
Article

Reboot or Not to Reboot?

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
13 Oct 2023CPOL30 min read 4.1K   67   17   4
This is a guide on how to configure your application, service or driver to handle appearing the new devices or device removal avoiding reboot request.
Why and who can request to restart your PC? Why can my application cause the system restart request? How to avoid system restarting if your application causes that? How to detect that hardware used by your application is about to be removed? Is that possible to do in the system service as long as in regular application? What if such things are necessary in the kernel mode? How to detect that new hardware with the type which you are interested is plugged in or unplugged? That is all the aspects and even more interesting system things are covered in this article.

Table of Contents

Introduction

Sometimes, when you update software for the device, it can request to reboot your PC. This happens because one or few applications hold the driver for the device which is updating. Actually, the system informs all applications that this device is required to be released, but most of the software does not handle such situations. For example, we have a capture device: the simple web camera. Put it into the GraphEdit tool just without starting playback or even without any connections, and in the device manager, try to disable that capture device with the right mouse click menu. Yes, you got the system restart request like in the screenshot below.

Image 1

This happens as the device object is held by the application: the GraphEdit in our case, and this application does not handle device removal requests.

Simple Application

Now we can try to reproduce those steps of the creating camera device object programmatically and try to properly handle the above situation. For that purpose, we can use the DirectShow device enumerator for accessing devices from the video capture category.

C++
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))
    {
        //...
    }
}

We enumerate all devices and search for the usb camera by examining the moniker string of each device. This way, we skip virtual devices, as we need the real hardware with which we can test future implementation. For that, we check the device interface path with the next code.

C++
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#"))
        {
            //...
        }
    }
}

Once we find a suitable device moniker, we try to initiate it and bind into the IBaseFilter object.

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

We can hold that object in the application and release it on the quit only. Now you can start the application and see: when you try to disable the selected camera in the device manager, our application holds an instance of that hardware and restarts request dialog pops up on the screen.

Image 2

In the regular windows application, this issue can be easily solved by using the device notifications. To be able to receive device removal notification, we should use the RegisterDeviceNotification API. For this API, we should have the window object and prepare the loop for processing windows messages along with the windows notification handler procedure.

C++
// 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);

The notification is sent with the WM_DEVICECHANGE windows message. The actual window can be hidden in the application, as we are only interested in the notifications handling. The RegisterDeviceNotification API may receive two different types of the structures as arguments for the registering notifications. First one is the DEV_BROADCAST_DEVICEINTERFACE which allows to receive notifications for the devices with the specified interface or class guid. And the second one is the DEV_BROADCAST_HANDLE where we need to specify the target device handle to receive notification about that device only. The second one we are going to use for our case.

C++
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);

Other arguments in our call of the RegisterDeviceNotification API are the window handle and the DEVICE_NOTIFY_WINDOW_HANDLE flag. It signals that we are using the window loop for processing notifications.

Last thing is to retrieve the device handle for the received IBaseFilter object. If we have the driver for the real hardware capture device, then it supports the IKsObject interface. By using this interface, we are able to receive the underlying hardware object handle with the KsGetObjectHandle() method. After that, we just can duplicate this handle and store it for future use.

C++
// 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());
        }
    }
}

Right now, in the windows message handler procedure, we need to process the WM_DEVICECHANGE message for the device notifications. The WPARAM argument, which is passed with it, can be DBT_DEVICEQUERYREMOVE in case we are requesting the device manager to remove the device. This can be due to reinstalling a new driver for the hardware or disabling the device manually, like we did previously. Another value of the WPARAM argument, which we are interested in, is the DBT_DEVICEREMOVECOMPLETE. That value is sent in case the device is surprisingly removed - for example usb camera unplugged. In those both values of WPARAM we should release all device resources used by the application.

C++
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);
}

It is possible to register a few notifications for the different devices. Those notifications can be filtered with the LPARAM argument, which is initially casted to the DEV_BROADCAST_HDR structure pointer. And based on the dbch_devicetype field, it can be casted into a specific type. In our case, the dbch_devicetype equals the DBT_DEVTYP_HANDLE and the LPARAM contains the pointer to the DEV_BROADCAST_HANDLE structure. The dbch_hdevnotify field of that structure is set equal to the notification handle which was received as the result of the RegisterDeviceNotification API call. So we compare them to be sure that we get proper notification.

To stop receiving notifications, it is necessary to call UnregisterDeviceNotification API and pass the handle which returned from the previously called RegisterDeviceNotification API.

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

Once the application starts, it will be able to receive notifications then the camera, which we hold, is about to be removed or surprisingly removed from the system. So in the application on remove request, we release all hardware resources of the camera we are holding and do not get the restart request from the system.

Image 3

PeekMessage and Device Notifications

Right now in the test application, we have an additional windows messages procedure callback which we specify during window creation. You may say that we don’t need that additional procedure for the message loop to handle windows messaging, as we can handle that while translating the messages. Something like this:

C++
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);
}

But if you start an application, you are unable to see “WM_DEVICECHANGE” text in the console window, even if the device becomes disabled and enabled back or usb cable unplugged.

Image 4

This is happening because the WM_DEVICECHANGE notification ignores the message queues and sends directly to the windows dispatch routine, So, if you design an application with usage of default one, you will be unable to receive those notifications.

What about Windows Service?

Now it is clear how the device removing notification should be handled in the application. But what about the windows services? I have seen the windows services of some capture devices vendors which hold the cameras and do not release them on requests. It is fine if your device is USB and you just unplug it, but this does not work in other cases, and you will receive a reboot request. How to avoid such notifications on the system if the reason is in the windows service, but the windows services do not contain the window messages loop? For that, the windows service should use the extended API RegisterServiceCtrlHandlerEx to register its control function callback.

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

if (!m_ServiceHandle) {
    return;
}

That callback will be able to receive device control notifications with the SERVICE_CONTROL_DEVICEEVENT event.

C++
// 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;
}

For receiving device removal notification for the specified device in the service, we should also use the RegisterDeviceNotification API. But pass the service handle as an argument, which is returned from the RegisterServiceCtrlHandlerEx API, instead of the window handle. Along with it, we should set the flag parameter to the DEVICE_NOTIFY_SERVICE_HANDLE value to signal that we are passing the service handle. Selection of the target capture device we implement in the same way as for regular windows applications.

C++
// 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);

After we are able to receive notification that the selected device is removed or about to be removed. We process such notification in the same way as for regular windows applications which was discussed previously. Just we have the EventData argument instead of the LPARAM in windows dispatch which we cast to the DEV_BROADCAST_HDR type, and pass the output information with OutputDebugString API.

C++
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;
                }
            }
        }
    }
}

Once we install and start our service, we see the information in DbgView tool about the device which is selected for the removal waiting. And when we disable this device in the device manager or unplug its cable connection, the DbgView tool shows specified notification information.

Image 5

Notifications in the Kernel Mode

We had to deal with notification in the user mode applications and services. Now it’s time to handle that in the kernel mode. In the driver implementation, we also can open any devices and communicate with them. For example, we can open the camera capture device and read frames or communicate with the Bluetooth endpoints or perform something else which requires us to open the device handle, even we can use a USB flash card in the driver. Anyway, if the driver holds something which is going to be removed, we also receive notification with a reboot request. To handle device removal in the driver, we have the IoRegisterPlugPlayNotification API. This function can set the callback routine which will be executed once the device, which passed to it as an argument, is about to be removed.

To use the IoRegisterPlugPlayNotification API for the target device, we should have a file object of the device we want to wait for the removal: nothing different from the user mode. For the kernel example implementation, like in the previous examples, we just use the first usb webcam device as the removal target. Initially, we should get the file object of that device. The original step here is to enumerate existing cameras. Each capture device can register its interface in one or few different categories. The interface category guid you can see in the capture device moniker string in the GraphEdit tool.

Image 6

Usually, there are three categories for the video capture devices. There are registers, their interfaces: KSCATEGORY_VIDEO - for the video devices, KSCATEGORY_CAPTURE - for the capture devices, and KSCATEGORY_VIDEO_CAMERA - for the camera devices. The capture category along with the video devices also contains the audio. The Video Camera category is used for the registering cameras. This interfaces category is used by the Media Foundation capture devices enumeration. The common video category is KSCATEGORY_VIDEO, which is able to access devices with the DirectShow enumerator. The tool which can help you to see the kernel device under each category is the KSStudio utility from the WDK package.

Image 7

So, in our kernel implementation, we can check one of those categories for the proper device interface. For the testing, we use the common video category: KSCATEGORY_VIDEO. To get all registered interfaces from the category, we call the IoGetDeviceInterfaces API. This API returns an allocated array of strings with the interfaces which should be freed by the caller.

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

Each interface symbolic link string has a zero ending. In the implementation, we also check for the usb devices only, just like we do in previous examples.

C++
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);
    }
}

Once we found the proper interface, we can open the target device file object. This can be done by the IoGetDeviceObjectPointer API or by the ZwCreateFile API. When we got the file object, we pass it to the IoRegisterPlugPlayNotification API along with our prepared callback function. We should specify the EventCategoryTargetDeviceChange as the first argument to this function, as we are interested in the removal notification of the specified device.

To properly display the device name of selected camera in the DbgPring output, we request the DevicePropertyFriendlyName property of the target file object by using the IoGetDeviceProperty function.

In the callback notification function, we receive the PLUGPLAY_NOTIFICATION_HEADER structure with the Event field equals to the GUID_TARGET_DEVICE_REMOVE_COMPLETE or the GUID_TARGET_DEVICE_QUERY_REMOVE values. In such cases, we close our file object with the ObDereferenceObject API and unregister the notification by using the IoUnregisterPlugPlayNotificationEx API.

C++
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;
}

We pass the notification handle to the IoUnregisterPlugPlayNotificationEx function which previously received as the result of the registration.

To test implementation, you should start the driver test application with the “target” argument without quotas.

The result of the code execution displayed on the next log from the DbgView application.

Image 8

Handle the New Device Appear

In most cases, along with detection of device removal requests, it is necessary to detect that a new device is added in the system. As an example: in the application which captures data from the usb camera, that camera surprisingly unplugged. Okay, we are handling that, but we want to continue capturing data from that camera and need to start doing that once this camera is plugged in back. There are also different methods to handle such things depending on what we are developing: regular application, windows service or a driver.

WM_DEVICECHANGE

In particular applications, we can use the WM_DEVICECHANGE notification in the message loop like we did for the previous case. With that message, we receive the value of DBT_DEVNODES_CHANGED as wParam argument which indicates that the device was added or removed in the system. To detect what device was added or removed, we can have the initial list of the devices which we are interested in, and check whatever devices from that list changed. For example, we can implement detection of the camera added or removed in the system next way.

Initial build the list of the devices.

C++
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;
}

We call the building list of the devices initially and in the window message loop once WM_DEVICECHANGE with the DBT_DEVNODES_CHANGED is received. Next step is to compare the list from the notification with the initial to detect devices which are added or removed.

C++
{
    // 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++;
    }
}

Once we start the application and enable or disable the camera device, we see the next result in the console window.

Image 9

Once we enable the camera device right after, we see:

Image 10

Windows Service

We already discussed that we do not have a windows message loop in windows service. But for hardware controlling, we also can use the RegisterDeviceNotification API. It has a specified flag argument DEVICE_NOTIFY_ALL_INTERFACE_CLASSES. It is compatible only with the passed DEV_BROADCAST_DEVICEINTERFACE structure. By using that flag, we are able to receive DBT_DEVICEARRIVAL and DBT_DEVICEREMOVECOMPLETE notifications to determine what device is installed or removed.

C++
// 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());
}

Like for the previous application, we can build an initial list of the devices we are interested in, and once notified, just checklist changes of those devices in the same way.

Image 11

Another way here is to use the passed arguments to our callback, this way we are not required to build an additional list of the devices and compare it each time. The notification of the DBT_DEVICEARRIVAL or DBT_DEVICEREMOVECOMPLETE once we register to receive all interfaces notifications pass the pointer to the DEV_BROADCAST_DEVICEINTERFACE structure as an argument. By using this structure, we can determine the device interface and its properties without building a list of the devices which we did previously. First, we cast input structure to the base DEV_BROADCAST_HDR and check whatever dbch_devicetype field equals to the DBT_DEVTYP_DEVICEINTERFACE and then we are able to cast it to the DEV_BROADCAST_DEVICEINTERFACE structure to get device interface path.

C++
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());
            }
        }
    }
}

Then we were able to see the events and we got the full interface path in the DbgView tool.

Image 12

We see the multiple same events for the single device as such devices just have a couple registered interfaces. We can add functionality to the code to skip the repeated notifications. More of that we can get the name of the devices from the interface path just by using the SetupAPI library functions.

C++
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);
}

And if the name of the previously installed or removed device is the same as we received previously, then skip that for the output into debug.

C++
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());
}

Now we got much better notification.

Image 13

The method listed for window service also works for particular windows applications. In case with the application, we just do not register additional notifications like we do for the service and just use existing DBT_DEVNODES_CHANGED.

Kernel Mode

In some implementations, we need to address devices from the kernel mode in our driver implementation. Yes, in that case, we also can detect that a new device appears on the system. In the Windows Service implementation, we receive all interfaces which are raised notification once the device is added or removed. In the driver, we address the device by its interface so we can set up a notification once the new interface with the specified type appears or disappears in the system. This can also be done with the function IoRegisterPlugPlayNotification which we mentioned earlier. In that function, we can specify the interface guid which we want to check. Interface guid is like the category of the devices, for example, we can check that a new removable media was added or a new Bluetooth device.

Like in previous examples, we check the video capture devices. So the interface category we are interested in is KSCATEGORY_VIDEO. The notification registration code looks next.

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

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

We pass the callback function which will be invoked once the device interface is added or removed. With the callback, we are also able to receive the symbolic link of the device interfaces which was added or removed; and display them with the DbgPrint, like we did for windows service implementation.

C++
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);
            }
        }
    }
}

To unregister notifications, the IoUnregisterPlugPlayNotificationEx API is used.

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

To test implementation, you should start the driver test application with the “all” argument without quotes.

The result we can see in the DbgView application.

Image 14

Like for the Windows Service, we should also improve displaying and make it a little pretty with the device name. We can use the registry for getting device name information. The function for initializing the device instance will not work as the callback is called once the device is already removed and you are not able to initiate it. So the function IoGetDeviceObjectPointer just failed in case of removal notification. But in Windows Service implementation, we are able to retrieve device name properly even on device remove callback. In the Windows Service implementation, we were using the SetupDiGetDeviceInterfaceProperty function which is access properties specified interface. Those properties are located in the registry and we also may access them even if the device is removed. The property we are interested in is the “FriendlyName” which represents the capture device name.

C++
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);
}

Now we are able to get the proper name of the device and display it.

Image 15

Issue with Legacy Non-pnp Drivers

Anyway, we are not targeting only the camera devices, we need to understand the mechanism at all. The notification works properly with the capture devices from the examples above. But what if we need to detect a specific driver, for example, the driver of the DbgView tool: dbgv.sys, which has the issue with driver unloading which I discuss in my previous articles.

For example, we take our simple legacy driver from the previous example and in the application, we try to set up the removal notification with the RegisterDeviceNotification API using the file handle of that driver. In that case, the RegisterDeviceNotification API failed with the error code 1066 (ERROR_SERVICE_SPECIFIC_ERROR).

This issue appears as our driver does not support plug and play features, and installed manually - not by the plug and play manager. Due to that, the IO manager is not able to notify that such device was added or removed.

Pnp Driver Implementation

To add plug and play support into your driver, it is necessary to add the pnp dispatch routines for the IRP_MJ_PNP and IRP_MJ_POWER notifications. Also, the adding new device should not be done right in the driver entry routine, but with the special AddDevice handler function.

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

The AddDevice routine will be called from the IO device manager. In this routine, we create the device in a regular way with IoCreateDevice and put it into the device stack by calling the IoAttachDeviceToDeviceStack.

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

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

The IRP packets in the IRP_MJ_POWER handler we can skip by calling PoStartNextPowerIrp and call the next driver in the stack as we are not relying on the actual hardware.

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

The main functionality is placed in the IRP_MJ_PNP handler routine. This handler is responsive for the starting and stopping device which we can perform in the Device Manager console. That dispatch routine also processes the device removal and related requests. Usually on the start device request the driver makes interfaces available for communication. This is done by the IoSetDeviceInterfaceState function. During that, the applications, which are registered for the interface notifications, received the WM_DEVICECHAGE with the DBT_DEVICEARRIVAL as an argument. In the device removal case, the interfaces are disabled with the same function mentioned above and the application receives DBT_DEVICEREMOVE notification for each interface. That's why we can receive multiple notifications of interface adding or removing for the single device. If in the application registered, the device removal notification for the device handle and device disabled in the device manager console then DBT_DEVICEQUERYREMOVE is sent first during that driver receives the IRP_MN_QUERY_REMOVE_DEVICE request - to check whatever driver can be now removed from the system. After the driver receives IRP_MN_REMOVE_DEVICE. In that time in the driver, we detach the device from the stack. And then, the application receives the DBT_DEVICEREMOVECOMPLETE notification. If the device is unplugged in the driver, we receive the IRP_MN_SURPRISE_REMOVAL and then IRP_MN_REMOVE_DEVICE.

From the driver implementation, the starting device performed in the IRP_MN_START_DEVICE PNP notification.

C++
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;

During the starting device, we should notify all drivers in the stack and only after that, change our state. For that, we set up a completion routine with the IoSetCompletionRoutine API and call the next driver in the stack with IoCallDriver. In the completion callback, we set the notification event once it completed till that time we wait for this event in our function if we got STATUS_PENDING from the IoCallDriver call. After that, our device starts and we can call our function DriverStartDevice.

The device removal request PNP notification sent with IRP_MN_REMOVE_DEVICE.

C++
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;

The DriverStopDevice and DriverStartDevice functions share the same functionality for pnp and non pnp drivers as I put implementation of both of them into a single cpp file. As well as the test application. There are just separate projects for pnp implementation; it has the PNP_DRIVER definition which allows it to build another driver type and application for testing it.

As the starting and stopping device performed with the Device Manager then we require an installation script the *.inf file to set up the driver. It also generates automatically based on the *.inx file template from the pnp driver project.

To install the driver, you can use the DevCon toolor add driver manually. As our driver does not rely on the actual hardware, we should use the “Add Legacy Hardware” in the “Action” menu of the Device Manager console to install it. On selection, specify the path to the *.inf file and you get the device selection dialog.

Image 16

After installation, you can find the device at the “Example Devices” in the Device Manager tree.

Image 17

Now you can start the test application. It is unable to install the driver manually at the startup and should detect the installed one. Now you can see that the call of the RegisterDeviceNotification API has not failed.

Image 18

And the application properly receives device removal notification once the device disables in the Device Manager.

Image 19

Detecting That Our Driver is Running on the System

To be able to handle drivers added or removed, we require to check whatever driver is running or not on the system. In case we know the symbolic link of the driver or its interface this is not a problem. We enumerate instances by the interface and check its hardware id. But what if we have only the driver file name like for the legacy drivers? There are few ways to find out that.

Using ntdll

Detecting that specified driver is running on the system is possible by using the NtQuerySystemInformation API, which is exported from the ntdll library. In that case, we should enumerate system loaded modules by requesting the SystemModuleInformation type as SYSTEM_INFORMATION_CLASS as an argument in the mentioned function. The result of the function call will be the RTL_PROCESS_MODULES structure which is filled with the system modules information. This structure is declared as follows:

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

The NumberOfModules contains the number of the following RTL_PROCESS_MODULE_INFORMATION structures which has the next declaration.

C++
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;

In this structure, we are interested in the FullPathName field which contains the full path to the driver including its filename.

It is required to call the NtQuerySystemInformation function two times: first to get the amount of memory which is necessary to allocate for the input buffer and the second time to retrieve the data.

C++
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;
}

After in the loop, we enumerate the received modules information and format the path field from there for the proper view.

C++
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);
    }
}

To check proper output from this implementation, we can start the non pnp driver test application which installs and loads the driver. And this test application which enumerates the system modules. You can see the result on the next screenshot. It displays that the driver is loaded in the system.

Image 20

Using Services API

Another method, which allows us to check whatever specified driver is running on the system, is by using the services API. As we see, the driver starts with the service manager, especially the legacy non-pnp drivers which we install and load from the test application. The services function EnumServicesStatusEx can enumerate drivers and windows services depending on the type flag specified as an argument. It should also be called a few times first for buffer size requests and the second for the actual data.

C++
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;
}

The result of that function call is the array of the ENUM_SERVICE_STATUS_PROCESS structures. Such a structure does not contain a path to the binary file of the service or a driver. To request the full path, we should open the service handle by its name from the ENUM_SERVICE_STATUS_PROCESS structure and call the QueryServiceConfig function. This function fills the QUERY_SERVICE_CONFIG structure.

C++
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);
}

Received structure contains the path to the binary, but we should format it for properly displaying.

C++
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);
}

The result of the test application is shown in the next screenshot.

Image 21

Detecting that Legacy Drivers Removing or Installing

Now, as we can enumerate installed drivers and services, we can compare the lists of those drivers with the changed one and this way to detect installed or removed drivers. It will work for legacy drivers also as they have their own executable. We just need to find out what to use to get such notifications. The function which sets up a callback for that is the SubscribeServiceChangeNotifications API. We should specify the SC_EVENT_DATABASE_CHANGE notification type in that function to get informed once service started or stopped. The first argument passed in that case is the handle to the service manager opened by the OpenSCManager API.

C++
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);

In the notification callback, we just get the actual list of the services and drivers and compare it with previously saved. To get such a list, we can use one of the methods described earlier.

C++
// 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);
    }
}

Once we check for adding or removing drivers or service, we save an updated list. To stop receiving notifications, the UnsubscribeServiceChangeNotifications API should be used. We can see the result of the test application in the next screenshot.

Image 22

When we start the driver test application, it gets a device added event, and then when the application quits, it raises the removal event.

Detecting Removal of the Target Legacy Driver

Now get back to the target device, we have our legacy non-pnp driver which can be used by separate applications along with ours and we want to detect that this driver is about to be removed, for example another application uninstall it. To do that, we can also use the SubscribeServiceChangeNotifications API mentioned above.

For checking that in our test application, we make the ability to install, uninstall and load this driver by the command line arguments. To use the SubscribeServiceChangeNotifications API in the application, we open the driver service with OpenService service manager API. That service handle we pass to the SubscribeServiceChangeNotifications API instead of the service manager handle from the previous example and use the SC_EVENT_STATUS_CHANGE notification value as event type argument.

C++
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);
}

After that, in the NotificationCallback function, we are able to receive SERVICE_NOTIFY_DELETE_PENDING notification when a registered driver is about to be removed.

C++
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);
    }
}

Like for the pnp notification implementation, we close all handles to our driver and remove notification with the UnsubscribeServiceChangeNotifications API.

C++
// 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);
}

We start the test application without arguments and then it installs the legacy driver and starts waiting for it to be removed. After that, we start the same application with the “uninstall” argument to perform uninstalling that legacy driver. In the previously running application, we see the information that the driver becomes unavailable.

Image 23

Special Cases of Notifications in Different Technologies

Certain technologies have their own helper implementations of the notifications of device removal or adding which can be used along with mentioned above ways. Here, we look into some of them.

DirectShow Notification

On the DirectShow technology, we have two ways of notifications which are represented by two interfaces: IMediaEvent and IMediaEventEx. First one processes notifications in the same thread. We just need to get the notification handle and wait until a new event appears. With the second interface, we set up the window handle and receive notification in the window procedure. Both those interfaces can be achieved from the filter graph object: IGraphBuilder.

C++
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);
    }
}

Processing events are similar in both cases. Once we have event notification, we process all events from the queue. We are interested in the EC_DEVICE_LOST event type. It is sent once the capture device has been lost.

C++
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);
}

If we start a test capture application and unplug the usb camera which is displayed on the screen, we see the next results.

Image 24

This is happening in case you unplug the selected usb camera, disabling the camera in the device manager still causes a reboot request to popup as we saw before. So, to properly handle the capture device removal in the DirectShow, it is required to use the notification method described earlier instead of relying on the mechanism which is supplied by that technology.

The EC_DEVICE_LOST can notify the application that the device which was lost previously available again. But in all those cases, the capture graph should be rebuilt to continue playback.

Image 25

Media Foundation

The Media Foundation itself does not provide a special mechanism for removal notification. It recommends using the way we described earlier. Although the Media Foundation application needs to request a symbolic link of the capture device source, it does not use the actual device handle to determine device removal. In the implementation, we register a notification for the KSCATEGORY_CAPTURE devices category and DBT_DEVTYP_DEVICEINTERFACE type.

C++
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);

In the windows procedure, we are able to receive WM_DEVICECHANGE messages with the DBT_DEVICEARRIVAL and DBT_DEVICEREMOVECOMPLETE notifications. We check for the saved symbolic link of the device we are using and this way, detect that our device is removed or added.

C++
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);
                }
            }
        }
    }
}

In the test application, we implement simple playback of the capture device with the Media Foundation with the notification method listed above. We start it and unplug the USB cable of the selected camera to see the test results.

Image 26

From the screenshot, we can see that it works fine. It also properly detects when the camera is plugged back again after removal.

Image 27

The Media Foundation takes care of the actual hardware internally so we don’t need the access to the device handle. Due to that, Media Foundation properly works when the camera is disabled in the device manager and there is no reboot request pop up.

Image 28

When we have a symbolic link of the capture device in the Media Foundation, it is not meaning that this is the actual device interface. As the capture devices in the Media Foundation are managed by the FrameServer service. In there, it may create aliases to the symbolic link for the real device so the camera can be shared within a couple applications. That’s why it is necessary to compare the symbolic links in the window procedure. Maybe later, I will describe how all those things work in the Media Foundation, as it is outside of this article.

MM Device API

The MM Device technology operates with the audio devices. It tracks the new device arriving and changing states of the existing ones. Actually, if the audio device is registered in the system, it is saved in the registry and once it's unplugged, the information is still kept, just the state of that device has been changed. The technology does not operate with the actual hardware, but it uses the intermediate functional endpoint layer. Each endpoint represents the inputs or outputs of the underlying hardware. The real hardware is located in the “Sound video and game controllers” of the device manager tree.

Image 29

But the endpoint devices you can find in the “audio inputs and output“ part also in the device manager.

Image 30

This is done because the actual hardware can have multiple inputs like microphones or line in, same for outputs: speakers, S/PDIFF and so on.

Communication with the real hardware is done through the mixer in the shared mode and directly only in the exclusive mode. In the exclusive mode, we also have the underlying buffer for the communication with the hardware. So, compared to the previous example with the DirectShow camera, we accessed only a subset of the audio functionality which is provided by the system. This means that disabling real hardware of the audio in the device manager does not request us to reboot the system, as the system components manage device removal internally.

Image 31

In the application, you just get the failure code from the used audio component once the device becomes unavailable. But with the MMDevice API, we are able to track device states. Let's look at the implementation example.

The MMDevice API has the IMMNotificationClient interface which is able to receive notifications I mentioned earlier. We create a test application and implement this interface on the test class. After we pass it into the RegisterEndpointNotificationCallback method of the IMMDeviceEnumerator object.

C++
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();
}

In our callback interface implementation, we should handle the OnDeviceStateChanged implementation which receives the endpoint id string and its state as an arguments.

C++
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;
}

This method called then the state of the endpoint has been changed and passes the new state. In our implementation, we just output the information into the console window.

To test, we start an application and enable or disable audio capture devices in the device manager.

Image 32

On the output, you can see that the application properly handles state changes of the target capture device. But if you disable the endpoint in the control panel, the actual device is not removed from the system and on output we get “disabled” state.

Image 33

By disabling the endpoint in the audio control panel, only that endpoint is removed from the “audio inputs and outputs” tree in the device manager.

Image 34

Media Volume Drives with Shell API

This is the special case which can be used to determine that a flash card or any hard drive is plugged or unplugged in the system. This method uses the Shell API. To use it, we should register shell notification for the adding or removing media drives. This can be done with the SHChangeNotifyRegister API.

C++
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);

Such notifications are passed into the window procedure with the message identifier which we define and specify as an argument in the registration function. As we set the SHCNRF_NewDelivery flag during registration, then to access data we should call the SHChangeNotification_Lock API, and once the accessing data finished, we should call SHChangeNotification_Unlock API. With that, we retrieve notification events and PIDLIST parameters. From that data, we can receive the path to the drive inserted or removed.

C++
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);
}

To unregister, you should use the SHChangeNotifyDeregister API and pass the value which returned previously from the registration routine.

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

As an example, we start a test application and plug and unplug the flash drive. The result is displayed on the next screen shot.

Image 35

Conclusion

When we use hardware directly in our application, we should take care that this device can be removed. There are some technologies which can help us to avoid reboot requests, but some of them do not fully guarantee that. So it is necessary to keep in mind that the hardware can be removed and test such situations in your implementation.

History

  • 13th October, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Russian Federation Russian Federation
I'm a professional multimedia developer (more than 10 years) in any kind of applications and technologies related to it, such as DirectShow, Direct3D, WinMM, OpenGL, MediaFoundation, WASAPI, Windows Media and other including drivers development of Kernel Streaming, Audio/Video capture drivers and audio effects. Have experience in following languages: C, C++, C#, delphi, C++ builder, VB and VB.NET. Strong knowledge in math and networking.

Comments and Discussions

 
QuestionGreat Info! Pin
Daniel Anderson 202116-Oct-23 8:36
Daniel Anderson 202116-Oct-23 8:36 
AnswerRe: Great Info! Pin
Maxim Kartavenkov16-Oct-23 20:33
Maxim Kartavenkov16-Oct-23 20:33 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA13-Oct-23 4:20
professionalȘtefan-Mihai MOGA13-Oct-23 4:20 
GeneralRe: My vote of 5 Pin
Maxim Kartavenkov15-Oct-23 6:09
Maxim Kartavenkov15-Oct-23 6:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.