Introduction

I was writing a C# Windows Service, and I needed to capture some device events. While this may seem an easy task, strangely, it isn't. Here is what I needed and what I did.

Prerequisites

A Windows Service is nothing more than a Windows program which is started by the Service Control Manager (SCM). A service can be manually started or scheduled for the SCM to start immediately after user logon. If you are unfamiliar with them, you can get more information on MSDN.

I have tested the code in Visual Studio 2008 Professional SP1, .NET 3.5 SP1, Windows XP SP3. It should work on any framework above 2.0, and it just might work in 1.1. I would suggest you go through the code as you read the text as it will become a lot more clear to you. Jump through it and see what happens.

Problem Description

Communication in Windows is done via messages. When a device (like a USB stick) is plugged in a computer, Windows sends messages to all who would listen for such an event. A Windows Forms application, for example, could easily intercept Windows messages by just overriding the virtual WndProc from the Form base class and using its Message parameter to its needs. However, a Windows Service typically is not meant to interact with the desktop or IO devices, so there is no easy way of knowing when something interesting has happened. And, if you would like to handle the DBT_DEVICEQUERYREMOVE event, things get complicated.

Service Control Handler (SCH)

Services have a control handler which receives all messages from Windows. These might include codes to stop or pause the service, or as in our case, device events. A managed service will abstract this control handler and give you only the OnStart, OnStop, and so on methods, which you can implement in order to achieve your needed functionality. We need, however, to register our own service handler, so we could catch device events. Note that this would disable all callbacks like OnStop except OnStart, which is called before we tell Windows to use our handler.

The Windows API function for this is RegisterServiceCtrlHandlerEx, which accepts a service name and a callback function to call when a message is received. We will call it in the OnStart function in our service. The managed version returns an IntPtr which is a service handle, but we already have a property of our class called ServiceHandle, so we don't need to save it.

Now, we can go implement our "OnStop" callback by capturing the SERVICE_CONTROL_STOP event, which is received in the "control" parameter of the handler. But, in order to handle SERVICE_CONTROL_DEVICEEVENT, we need to do something else.

Register for Device Notifications

In the OnStart method, apart from registering for a control handler, we will register for device notifications by using the Win32 API function RegisterDeviceNotification. We give it our service's handle, a pointer to a DEV_BROADCAST_DEVICEINTERFACE struct (telling the function to register for a class of devices), and some flags, among which is the DEVICE_NOTIFY_SERVICE_HANDLE, which specifies that the caller is a service and not a window, for example. It returns a handle, which we must preserve in order to unregister when we don't need device messages anymore (for example, we could do this in the SERVICE_CONTROL_STOP event).

Using this function allows us to capture the DBT_DEVICEARRIVAL and DBT_DEVICEREMOVECOMPLETE event types. We get them through the eventType parameter of our SCH. There, we can handle the SERVICE_CONTROL_DEVICEEVENT and do anything we like.

DBT_DEVICEQUERYREMOVE

So far so good. But I needed more. I needed to use the FileSystemWatcher control which Microsoft supplied with the .NET Framework on the newly plugged USB. I could start the watcher when I get the device arrive event, but then when I attempted to "safely remove hardware", it always said that it couldn't remove it. It was obviously because of the watcher. I searched and found that there was an event DBT_DEVICEQUERYREMOVE, which is being risen just before a device is about to be removed. The only problem was that I couldn't get it with what I have done so far. Interestingly enough, Windows Forms applications work the same, so everything below should be done for them too.

The solution is to create a handle to the device itself, use it in a (this time) DEV_BROADCAST_HANDLE structure, and register with it to our SCH. All of this we could do in the DEVICEARRIVAL event. In order to accomplish all this, it takes a couple of things - find which drive letter the device got with the GetVolumeNameForVolumeMountPoint (which gets its parameters from the name field in the DEVICEINTERFACE struct) and GetVolumePathNamesForVolumeName in order to get a device handle using the CreateFile function. Only after that are we able to get the QUERYREMOVE event, disable the FileSystemWatcher, and allow the USB to be freely removed.

Conclusion

There are a ton of other small things that when not written exactly the way I have written them, do not work. If you need this kind of functionality, I would strongly suggest that you browse through the code I have supplied, and if do not want to understand it, at least just copy and paste. :) It will save you a lot of headaches.