Click here to Skip to main content
Click here to Skip to main content

Capturing Device Events in a C# Windows Service

By , 15 Mar 2009
 

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.

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

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.

History

  • 25th December, 2008: Initial post
  • 14th March, 2009: Updated source code

License

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

About the Author

Alien282
Bulgaria Bulgaria
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralSizeConst for dbcc_name should be longermemberWest Dakota18 Mar '11 - 18:14 
The MarshalAs attribute for DEV_BROADCAST_DEVICEINTERFACE.dbcc_name only allows 128 characters in a path name. Many USB devices have a longer path, which causes the code to crash since IndexOf can't find the null-terminator. Bump this value up to 512 to avoid crashing.
QuestionHow to capture new device info inside private void RegisterDeviceNotification()?memberavishekrc27 Feb '11 - 19:06 
Hi,
I am a newbie.
Can you please help me how can I retrieve the newly inserted device[USB]related info,like device name,type etc inside your private void RegisterDeviceNotification()?
It will be really helpful to me.
Thanks in advance.
GeneralMy vote of 4memberCSharpner.com19 Aug '10 - 2:47 
Great article, but code doesn't work quite right, otherwise, I'd have given 5 stars.
GeneralOne potential gotchamemberizogi8 Apr '10 - 13:57 
Hello.
 
Thanks for an excellent article and example. I won't fault it at all, but I just tried to adapt it to my own service and thought I'd mention what's probably already be obvious to some people, and especially to me in hindsight.
 
When I ran my own service, it kept occasional shutting down for no obvious reason with the following reported in the event log:
 
Faulting application MyAppName.exe, version 1.1.2.0, time stamp
0x4bbe43c1, faulting module MSVCR80.dll, version 8.0.50727.4027, time
stamp 0x49cd0c61, exception code 0xc0000005, fault offset 0x0001500a,
process id 0x%9, application start time 0x%10.
 
I spent hours trying to track it down figuring I had an unmanaged memory leak or a race condition somewhere, but the actual crashes often appeared to be happening outside any of the code I'd written. It turned out it was because I'd tried to be smart early on when adapting things without paying attention. I noticed the 'myCallback' variable was only referenced from within RegisterDeviceNotification(), and I made it a local variable without thinking, of course, that the unmanaged Win32.RegisterServiceCtrlHandlerEx() function wouldn't pin a reference to it and so as soon as it went out of scope, the DotNet garbage handler would simply come along and clean it up.
 
So, er, yeah. Be sure that the call-back delegate variable that's passed to the registration function remains within scope for the lifetime of the service. Silly me of course, but I thought I'd point it out just in case anyone falls into a similar trap.
GeneralRe: One potential gotchamemberalhambra-eidos23 Jul '10 - 6:49 
any solution, please ??
AE

GeneralRe: One potential gotchamemberizogi23 Jul '10 - 11:50 
Uh, yes. The solution was in my original post that you've just replied to.
 
"Be sure that the call-back delegate variable that's passed to the registration function remains within scope for the lifetime of the service."

 
If this doesn't fix your problem then you have a different problem than I did, but you've not described your problem at all.
GeneralProblems with ServiceControlHandler-FunctionmemberLeo Tiger2 Aug '09 - 22:27 
Hi,
I tried the downloadable source code but the service were stopped every time i connected a usb device. After 'debugging' the service, i found out, that the both GetVolumePathNamesForVolumeNameW-Functions in the ServiceControlHandler-Function returns empty values. The service runs both times into the if-query:

uint stringReturnLength = 0;
string driveLetter = "";
Win32.GetVolumePathNamesForVolumeNameW(stringBuilder.ToString(), driveLetter, (uint) driveLetter.Length, ref stringReturnLength);
if (stringReturnLength == 0)
{
File.AppendAllText("C:\\Debugging.log", "Error Nr. 1");
}
 
driveLetter = new string(new char[stringReturnLength]);
 
if (!Win32.GetVolumePathNamesForVolumeNameW(stringBuilder.ToString(), driveLetter, stringReturnLength, ref stringReturnLength))
{
File.AppendAllText("C:\\Debugging.log", "Error Nr. 2");
}

 
The only thing i changed was an added Installer at the end of the service so that i could see the installed service in the service management console.
 
What´s the problem with it?
GeneralRe: Problems with ServiceControlHandler-FunctionmemberAlien28214 Aug '09 - 23:46 
Hello,
 
Hm, I really have no idea why this might be happening. You might try calling GetLastError in the first "if" (where you should have received in stringReturnLength the buffer size) and see what went wrong. MSDN can help:
 
GetVolumePathNamesForVolumeNameW documentation
 
Quote from there:
 
"If the function fails, the return value is zero. To get extended error information, call GetLastError . If the buffer is not large enough to hold the complete list, the error code is ERROR_MORE_DATA and the lpcchReturnLength parameter receives the required buffer size."
 
Hope you find what it was. Smile | :)
GeneralRe: Problems with ServiceControlHandler-FunctionmemberMember 804220729 Jun '11 - 7:10 
Having the same issue, wonder if I could get a bit more help here, thanks
GeneralRe: Problems with ServiceControlHandler-Functionmembernearperfect17 Jan '12 - 19:00 
I have the same problem. GetVolumeNameForVolumeMountPoint always failed. Any suggestion?
Questionrunning on Vista ok?membercharle_hsin24 Apr '09 - 17:08 
I haven't tried the codes on Vista yet. However, with Vista's new session 0 isolation feature, does this service still get USB events? Another question, my service loads a COM DLL where winproc() is used to get USB message. On XP, it runs fine. However, on Vista, the loaded COM DLL cannot get the USB message. Is there a good way to capture the USB message on Vista?
GeneralGood articlememberPlamen Velikov22 Mar '09 - 2:11 
Hi,
 
Very good article. Congratulations!
 
Cheers
GeneralError with UnregisterDeviceNotificationmemberSpooler12 Mar '09 - 4:11 
Hi,
thanks for your great article, I had a similar problem figuring out how to tap into the message loop without a Window. I just wanted to point out that something is wrong with your code.
In the ServiceControlHandler function, you use the ServiceHandle to unregister the device event notification, that is not right. It should be the device notification registration pointer you got from RegisterDeviceNotification.
 
Your code :
private int ServiceControlHandler(int control, int eventType, IntPtr eventData, IntPtr context)
{
  if (control == Win32.SERVICE_CONTROL_STOP || control == Win32.SERVICE_CONTROL_SHUTDOWN)
  {
    UnregisterHandles();
    Win32.UnregisterDeviceNotification(this.ServiceHandle);
 
    base.Stop();
  }
 
Fixed :
class Service : BaseService
{
   private IntPtr deviceNotificationHandle = IntPtr.Zero;
...
 
        private void RegisterDeviceNotification()
        {
...
            r = Win32.RegisterDeviceNotification(this.ServiceHandle,
                buffer, Win32.DEVICE_NOTIFY_SERVICE_HANDLE | Win32.DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
 
            if (r != IntPtr.Zero)
                deviceNotificationHandle = r;
        }
...
 
        private int ServiceControlHandler(int control, int eventType, IntPtr eventData, IntPtr context)
        {
            if (control == Win32.SERVICE_CONTROL_STOP || control == Win32.SERVICE_CONTROL_SHUTDOWN)
            {
                UnregisterHandles();
                Win32.UnregisterDeviceNotification(this.deviceNotificationHandle );
 
                base.Stop();
...
  }
...
}
 
 
Cheers Big Grin | :-D
Thanks again
GeneralRe: Error with UnregisterDeviceNotificationmemberAlien28214 Mar '09 - 1:47 
Of course, you are right. Interestingly enough, I have done that for the unregistering of the device file handle, but not for the device events itself. Must have been thinking for something else. Smile | :) Thanks for the tip! Smile | :) The code should be updated soon.
GeneralAttachments corruptmemberhuwsimpson12 Jan '09 - 18:55 
The file attachment which is meant to contain the source code is corrupt / zero length.
 
The article is great though and I look forward to being able to get the source code. Big Grin | :-D
GeneralRe: Attachments corruptmemberAlien28219 Jan '09 - 9:20 
Are you sure? I have no problem downloading it... I could send it to your mail, if you still have problems.
GeneralRe: Attachments corruptmemberhuwsimpson19 Jan '09 - 21:21 
It's working now, thanks very much.

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

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 15 Mar 2009
Article Copyright 2008 by Alien282
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid