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.
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.
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
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
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.
The service control handler's signature is like this:
public delegate int ServiceControlHandlerEx(int control,
int eventType, IntPtr eventData, IntPtr context);
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
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
Using this function allows us to capture the
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.
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.
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.
- 25th December, 2008: Initial post
- 14th March, 2009: Updated source code