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
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.
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.
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.
CComPtr<ICreateDevEnum> _dev;
if (S_OK == _dev.CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER))
{
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.
CComPtr<IBindCtx> _context;
CreateBindCtx(0, &_context);
CComPtr<IMoniker> _moniker;
ULONG cFetched = 0;
while (g_hDevice == INVALID_HANDLE_VALUE
&& S_OK == _enum->Next(1, &_moniker, &cFetched)
&& _moniker)
{
LPOLESTR pszTemp = NULL;
if (S_OK == _moniker->GetDisplayName(_context, NULL, &pszTemp) && pszTemp)
{
_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.
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.
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.
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);
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.
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.
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.
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) {
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.
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.
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:
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.
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.
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.
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.
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);
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.
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.
VOID ServiceDeviceEvent(DWORD dwEventType, LPVOID lpEventData) {
if (dwEventType == DBT_DEVICEQUERYREMOVE || dwEventType == DBT_DEVICEREMOVECOMPLETE) {
DEV_BROADCAST_HDR * hdr = (DEV_BROADCAST_HDR *)lpEventData;
if (hdr->dbch_devicetype == DBT_DEVTYP_HANDLE) {
DEV_BROADCAST_HANDLE * _handle = (DEV_BROADCAST_HANDLE *)hdr;
if (_handle->dbch_hdevnotify == g_hNotify) {
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());
}
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.
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.
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.
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 string
s with the interfaces which should be freed by the caller.
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.
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.
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;
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.
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.
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.
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)))
{
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 (S_OK == _enum->Next(1, &_moniker, &cFetched) && _moniker)
{
LPOLESTR pszTemp = NULL;
if (S_OK == _moniker->GetDisplayName(_context, NULL, &pszTemp) && pszTemp)
{
std::wstring name;
std::wstring moniker;
moniker = pszTemp;
_wcslwr_s(pszTemp,wcslen(pszTemp) + 1);
if (wcsstr(pszTemp, L"@device:pnp:\\\\?\\"))
{
CComPtr< IPropertyBag > _bag;
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.
{
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++;
}
}
{
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.
Once we enable the camera device right after, we see:
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.
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;
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.
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.
VOID ServiceDeviceEvent(DWORD dwEventType, LPVOID lpEventData) {
if (dwEventType == DBT_DEVICEARRIVAL || dwEventType == DBT_DEVICEREMOVECOMPLETE) {
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.
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.
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.
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.
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
.
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.
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.
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;
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.
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.
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.
PWCHAR DisplayName = NULL;
HANDLE hKey = NULL;
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.
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.
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.
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
.
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.
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.
case IRP_MN_START_DEVICE:
IoCopyCurrentIrpStackLocationToNext (Irp);
KeInitializeEvent(&Extension->StartEvent, NotificationEvent, FALSE);
IoSetCompletionRoutine (Irp,
DriverPnPComplete,
Extension,
TRUE,
TRUE,
TRUE);
Irp->IoStatus.Status = STATUS_SUCCESS;
Status = IoCallDriver (TopDeviceObject, Irp);
if (STATUS_PENDING == Status) {
KeWaitForSingleObject(
&Extension->StartEvent,
Executive, KernelMode, FALSE, NULL);
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
.
case IRP_MN_REMOVE_DEVICE:
IoAcquireRemoveLock(&Extension->RemoveLock,NULL);
IoReleaseRemoveLockAndWait(&Extension->RemoveLock,NULL);
DriverStopDevice(Extension->DriverObject);
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.
After installation, you can find the device at the “Example Devices” in the Device Manager tree.
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.
And the application properly receives device removal notification once the device disables in the Device Manager.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
SC_HANDLE hServiceManager = OpenSCManager(
NULL,
SERVICES_ACTIVE_DATABASE,
SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_CONNECT
);
PSC_NOTIFICATION_REGISTRATION Registration = NULL;
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.
VOID CALLBACK NotificationCallback(DWORD dwNotify, PVOID pCallbackContext) {
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);
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]);
}
}
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]);
}
}
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.
When we start the driver test application, it gets a device added event, and then when the application quits, it raises the removal event.
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.
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.
VOID CALLBACK NotificationCallback(DWORD dwNotify, PVOID pCallbackContext) {
if (dwNotify == SERVICE_NOTIFY_DELETE_PENDING) {
SetEvent(g_hNotify);
}
}
Like for the pnp notification implementation, we close all handles to our driver and remove notification with the UnsubscribeServiceChangeNotifications
API.
if (WaitForSingleObject(g_hNotify, 0) == WAIT_OBJECT_0) {
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.
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.
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
.
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.
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.
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.
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.
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.
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.
From the screenshot, we can see that it works fine. It also properly detects when the camera is plugged back again after removal.
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.
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.
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.
But the endpoint devices you can find in the “audio inputs and output“ part also in the device manager.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 13th October, 2023: Initial version