8

I'd like to write a service that listens for device notifications (USB media plugged in, removed). The problem of listening for device notifications in a C# service is, that System.Windows.Forms.Control.WndProc isn't available because a windows service doesn't have any windows.
I found this HowTo on how to write such a service. The author of that article found a workaround which lets the service listen for device notifications instead of service control messages and therefore, the service doesn't support OnStop() anymore.

(Update 26.01.13:)
Sadly, I don't really understand the service control manager and the windows API. I'm wondering if it is possible to register to both, service control messages AND usb device notifications or if this is really the only option for a service to listen to device notifications. I haven't yet found any (understandable for me) information which solves my problem.
Might it be possible to use the System.Windows.Forms.Control.WndProc without generating windows (I'd just have to add the System.Windows.Forms assembly, right?).

(Update 27.01.13:)
I just found this question: Cannot start desktop application from Windows service on Windows 7
The second answer there says, that Windows services received a security-centric makeover in Windows Vista and GUI elements are now created in Session 0 even if the "Allow service to interact with desktop" is checked. Does that mean, that I CAN create a Windows Form which then receives the USB device events (and therefore, I don't need to mess with the ServiceControlHandler? Are there any caveats or problems doing this?

In short, I need a solution that does one of the following:

  1. Make OnStop available again, or
  2. provide another method of listening for usb device notifications in a Windows C# service

My source code is currently the following. It is almost identical to the source code offered by the HowTo I linked in the first paragraph. The only difference I made is removing the FileSystemWatcher private field and all usages of the same because I don't need the FileSystemWatcher.

USBBackup.cs (the service itself - using statements excluded but complete in my source code):

namespace USBBackup
{
    public partial class USBBackup : ServiceBase
    {

        private IntPtr deviceNotifyHandle;
        private IntPtr deviceEventHandle;
        private IntPtr directoryHandle;
        private Win32.ServiceControlHandlerEx myCallback;

        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(deviceEventHandle);

                base.Stop();
            }
            else if (control == Win32.SERVICE_CONTROL_DEVICEEVENT)
            {
                switch (eventType)
                {
                    case Win32.DBT_DEVICEARRIVAL:
                        Win32.DEV_BROADCAST_HDR hdr;
                        hdr = (Win32.DEV_BROADCAST_HDR)Marshal.PtrToStructure(eventData, typeof(Win32.DEV_BROADCAST_HDR));
                        if (hdr.dbcc_devicetype == Win32.DBT_DEVTYP_DEVICEINTERFACE)
                        {
                            Win32.DEV_BROADCAST_DEVICEINTERFACE deviceInterface;
                            deviceInterface = (Win32.DEV_BROADCAST_DEVICEINTERFACE)Marshal.PtrToStructure(eventData, typeof(Win32.DEV_BROADCAST_DEVICEINTERFACE));
                            string name = new string(deviceInterface.dbcc_name);
                            name = name.Substring(0, name.IndexOf('\0')) + "\\";

                            StringBuilder stringBuilder = new StringBuilder();
                            Win32.GetVolumeNameForVolumeMountPoint(name, stringBuilder, 100);

                            uint stringReturnLength = 0;
                            string driveLetter = "";

                            Win32.GetVolumePathNamesForVolumeNameW(stringBuilder.ToString(), driveLetter, (uint)driveLetter.Length, ref stringReturnLength);
                            if (stringReturnLength == 0)
                            {
                                // TODO handle error
                            }

                            driveLetter = new string(new char[stringReturnLength]);

                            if (!Win32.GetVolumePathNamesForVolumeNameW(stringBuilder.ToString(), driveLetter, stringReturnLength, ref stringReturnLength))
                            {
                                // TODO handle error
                            }

                            RegisterForHandle(driveLetter[0]);
                        }
                        break;
                    case Win32.DBT_DEVICEQUERYREMOVE:
                        UnregisterHandles();
                        break;
                }
            }

            return 0;
        }

        private void UnregisterHandles()
        {
            if (directoryHandle != IntPtr.Zero)
            {
                Win32.CloseHandle(directoryHandle);
                directoryHandle = IntPtr.Zero;
            }
            if (deviceNotifyHandle != IntPtr.Zero)
            {
                Win32.UnregisterDeviceNotification(deviceNotifyHandle);
                deviceNotifyHandle = IntPtr.Zero;
            }
        }

        private void RegisterForHandle(char c)
        {
            Win32.DEV_BROADCAST_HANDLE deviceHandle = new Win32.DEV_BROADCAST_HANDLE();
            int size = Marshal.SizeOf(deviceHandle);
            deviceHandle.dbch_size = size;
            deviceHandle.dbch_devicetype = Win32.DBT_DEVTYP_HANDLE;
            directoryHandle = CreateFileHandle(c + ":\\");
            deviceHandle.dbch_handle = directoryHandle;
            IntPtr buffer = Marshal.AllocHGlobal(size);
            Marshal.StructureToPtr(deviceHandle, buffer, true);
            deviceNotifyHandle = Win32.RegisterDeviceNotification(this.ServiceHandle, buffer, Win32.DEVICE_NOTIFY_SERVICE_HANDLE);
            if (deviceNotifyHandle == IntPtr.Zero)
            {
                // TODO handle error
            }
        }

        private void RegisterDeviceNotification()
        {
            myCallback = new Win32.ServiceControlHandlerEx(ServiceControlHandler);
            Win32.RegisterServiceCtrlHandlerEx(this.ServiceName, myCallback, IntPtr.Zero);

            if (this.ServiceHandle == IntPtr.Zero)
            {
                // TODO handle error
            }

            Win32.DEV_BROADCAST_DEVICEINTERFACE deviceInterface = new Win32.DEV_BROADCAST_DEVICEINTERFACE();
            int size = Marshal.SizeOf(deviceInterface);
            deviceInterface.dbcc_size = size;
            deviceInterface.dbcc_devicetype = Win32.DBT_DEVTYP_DEVICEINTERFACE;
            IntPtr buffer = default(IntPtr);
            buffer = Marshal.AllocHGlobal(size);
            Marshal.StructureToPtr(deviceInterface, buffer, true);
            deviceEventHandle = Win32.RegisterDeviceNotification(this.ServiceHandle, buffer, Win32.DEVICE_NOTIFY_SERVICE_HANDLE | Win32.DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
            if (deviceEventHandle == IntPtr.Zero)
            {
                // TODO handle error
            }
        }

        public USBBackup()
        {
            InitializeComponent();
        }

        public static IntPtr CreateFileHandle(string driveLetter)
        {
            // open the existing file for reading
            IntPtr handle = Win32.CreateFile(
                  driveLetter,
                  Win32.GENERIC_READ,
                  Win32.FILE_SHARE_READ | Win32.FILE_SHARE_WRITE,
                  0,
                  Win32.OPEN_EXISTING,
                  Win32.FILE_FLAG_BACKUP_SEMANTICS | Win32.FILE_ATTRIBUTE_NORMAL,
                  0);

            if (handle == Win32.INVALID_HANDLE_VALUE)
            {
                return IntPtr.Zero;
            }
            else
            {
                return handle;
            }
        }

        protected override void OnStart(string[] args)
        {
            base.OnStart(args);

            RegisterDeviceNotification();
        }
    }
}

Win32.cs:

namespace USBBackup
{
    public class Win32
    {
        public const int DEVICE_NOTIFY_SERVICE_HANDLE = 1;
        public const int DEVICE_NOTIFY_ALL_INTERFACE_CLASSES = 4;

        public const int SERVICE_CONTROL_STOP = 1;
        public const int SERVICE_CONTROL_DEVICEEVENT = 11;
        public const int SERVICE_CONTROL_SHUTDOWN = 5;

        public const uint GENERIC_READ = 0x80000000;
        public const uint OPEN_EXISTING = 3;
        public const uint FILE_SHARE_READ = 1;
        public const uint FILE_SHARE_WRITE = 2;
        public const uint FILE_SHARE_DELETE = 4;
        public const uint FILE_ATTRIBUTE_NORMAL = 128;
        public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
        public static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

        public const int DBT_DEVTYP_DEVICEINTERFACE = 5;
        public const int DBT_DEVTYP_HANDLE = 6;

        public const int DBT_DEVICEARRIVAL = 0x8000;
        public const int DBT_DEVICEQUERYREMOVE = 0x8001;
        public const int DBT_DEVICEREMOVECOMPLETE = 0x8004;

        public const int WM_DEVICECHANGE = 0x219;

        public delegate int ServiceControlHandlerEx(int control, int eventType, IntPtr eventData, IntPtr context);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern IntPtr RegisterServiceCtrlHandlerEx(string lpServiceName, ServiceControlHandlerEx cbex, IntPtr context);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetVolumePathNamesForVolumeNameW(
                [MarshalAs(UnmanagedType.LPWStr)]
                    string lpszVolumeName,
                [MarshalAs(UnmanagedType.LPWStr)]
                    string lpszVolumePathNames,
                uint cchBuferLength,
                ref UInt32 lpcchReturnLength);

        [DllImport("kernel32.dll")]
        public static extern bool GetVolumeNameForVolumeMountPoint(string
           lpszVolumeMountPoint, [Out] StringBuilder lpszVolumeName,
           uint cchBufferLength);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr RegisterDeviceNotification(IntPtr IntPtr, IntPtr NotificationFilter, Int32 Flags);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern uint UnregisterDeviceNotification(IntPtr hHandle);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr CreateFile(
              string FileName,                    // file name
              uint DesiredAccess,                 // access mode
              uint ShareMode,                     // share mode
              uint SecurityAttributes,            // Security Attributes
              uint CreationDisposition,           // how to create
              uint FlagsAndAttributes,            // file attributes
              int hTemplateFile                   // handle to template file
              );

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr hObject);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct DEV_BROADCAST_DEVICEINTERFACE
        {
            public int dbcc_size;
            public int dbcc_devicetype;
            public int dbcc_reserved;
            [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 16)]
            public byte[] dbcc_classguid;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128)]
            public char[] dbcc_name;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct DEV_BROADCAST_HDR
        {
            public int dbcc_size;
            public int dbcc_devicetype;
            public int dbcc_reserved;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct DEV_BROADCAST_HANDLE
        {
            public int dbch_size;
            public int dbch_devicetype;
            public int dbch_reserved;
            public IntPtr dbch_handle;
            public IntPtr dbch_hdevnotify;
            public Guid dbch_eventguid;
            public long dbch_nameoffset;
            public byte dbch_data;
            public byte dbch_data1;
        }
    }
}
TylerH
  • 20,799
  • 66
  • 75
  • 101
wullxz
  • 17,830
  • 8
  • 32
  • 51
  • You possibly have this solved right now, if not, let me know. I have written a C# application that ran as a Windows service and would listen for exactly these messages. I've used the process which is outlined in this article: http://msdn.microsoft.com/en-us/magazine/cc163417.aspx – Oliver Salzburg Nov 02 '13 at 14:53
  • I put that project on hold. The last test was successful somehow but I'll definitely look at that article when I continue on that project. Thanks for the link :) – wullxz Nov 02 '13 at 15:57
  • In my opinion ServiceControlHandler should call `OnStop`, not `base.Stop`. Stop is for sending a stop notification, OnStop is for receiving the notification (which your ServiceControlHandler did). – Werner Henze Nov 20 '15 at 13:03
  • 1
    @DerHochstapler I found a webarchive version of the link you posted. It's not really nicely formatted but I'm sure it helps if someone is searching for more information: https://web.archive.org/web/20130503230316/http://msdn.microsoft.com/en-us/magazine/cc163417.aspx – wullxz Aug 18 '19 at 10:27

2 Answers2

1

I have previously worked on the same subject and the route I ultimately went with was to simply construct a Window and forward the messages. I'm sure I got the relevant code from some third party, as I commented on this question in 2013 referencing a link that is now dead.

So let's look at the code.

First of all, this is the complete MessageWindow implementation:

using System;
using System.Threading;
using System.Windows.Forms;
using System.ComponentModel;
using System.Collections.Generic;

namespace Foo.Windows {
  public class MessageReceivedEventArgs : EventArgs {
    private readonly Message _message;

    public MessageReceivedEventArgs( Message message ) {
      _message = message;
    }

    public Message Message {
      get { return _message; }
    }
  }

  public static class MessageEvents {
    private static object _lock = new object();
    private static MessageWindow _window;
    private static IntPtr _windowHandle;
    private static SynchronizationContext _context;

    public static event EventHandler<MessageReceivedEventArgs> MessageReceived;

    public static void WatchMessage( int message ) {
      EnsureInitialized();
      _window.RegisterEventForMessage( message );
    }

    public static IntPtr WindowHandle {
      get {
        EnsureInitialized();
        return _windowHandle;
      }
    }

    private static void EnsureInitialized() {
      lock( _lock ) {
        if( _window == null ) {
          _context = AsyncOperationManager.SynchronizationContext;
          using( ManualResetEvent mre = new ManualResetEvent( false ) ) {
            Thread t = new Thread( (ThreadStart) delegate {
                                                   _window = new MessageWindow();
                                                   _windowHandle = _window.Handle;
                                                   mre.Set();
                                                   Application.Run();
                                                 } );
            t.Name = "MessageEvents message loop";
            t.IsBackground = true;
            t.Start();

            mre.WaitOne();
          }
        }
      }
    }

    private class MessageWindow : Form {
      private ReaderWriterLock _lock = new ReaderWriterLock();
      private Dictionary<int, bool> _messageSet = new Dictionary<int, bool>();

      public void RegisterEventForMessage( int messageID ) {
        _lock.AcquireWriterLock( Timeout.Infinite );
        _messageSet[ messageID ] = true;
        _lock.ReleaseWriterLock();
      }

      protected override void WndProc( ref Message m ) {
        _lock.AcquireReaderLock( Timeout.Infinite );
        bool handleMessage = _messageSet.ContainsKey( m.Msg );
        _lock.ReleaseReaderLock();

        if( handleMessage ) {
          MessageEvents._context.Send( delegate( object state ) {
            EventHandler<MessageReceivedEventArgs> handler = MessageEvents.MessageReceived;
            if( handler != null )
              handler( null, new MessageReceivedEventArgs( (Message)state ) );
          }, m );
        }

        base.WndProc( ref m );
      }
    }
  }
}

For completeness, these are the constants relevant to the device change detection process:

using System;
using System.Runtime.InteropServices;

namespace Foo.Windows {
  internal class NativeMethods {
    /// <summary>
    /// Notifies an application of a change to the hardware configuration of a device or the computer.
    /// </summary>
    public static Int32 WM_DEVICECHANGE = 0x0219;

    /// <summary>
    /// The system broadcasts the DBT_DEVICEARRIVAL device event when a device or piece of media has been inserted and becomes available.
    /// </summary>
    public static Int32 DBT_DEVICEARRIVAL = 0x8000;

    /// <summary>
    /// Serves as a standard header for information related to a device event reported through the WM_DEVICECHANGE message.
    /// </summary>
    [StructLayout( LayoutKind.Sequential )]
    public struct DEV_BROADCAST_HDR {
      public Int32 dbch_size;
      public Int32 dbch_devicetype;
      public Int32 dbch_reserved;
    }

    public enum DBT_DEVTYP : uint {
      /// <summary>
      /// OEM- or IHV-defined device type.
      /// </summary>
      DBT_DEVTYP_OEM = 0x0000,

      /// <summary>
      /// Logical volume.
      /// </summary>
      DBT_DEVTYP_VOLUME = 0x0002,

      /// <summary>
      /// Port device (serial or parallel).
      /// </summary>
      DBT_DEVTYP_PORT = 0x0003,

      /// <summary>
      /// Class of devices.
      /// </summary>
      DBT_DEVTYP_DEVICEINTERFACE = 0x0005,

      /// <summary>
      /// File system handle.
      /// </summary>
      DBT_DEVTYP_HANDLE = 0x0006
    }

    /// <summary>
    /// Contains information about a OEM-defined device type.
    /// </summary>
    [StructLayout( LayoutKind.Sequential )]
    public struct DEV_BROADCAST_VOLUME {
      public Int32 dbcv_size;
      public Int32 dbcv_devicetype;
      public Int32 dbcv_reserved;
      public Int32 dbcv_unitmask;
      public Int16 dbcv_flags;
    }
  }
}

Now all you have to do is to register the message you're interested in and handle the event when it happens. These should be the relevant parts for that process:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading;
using Foo.Windows;

namespace Foo.Core {
  class Daemon {

    private static void InternalRun() {
      MessageEvents.WatchMessage( NativeMethods.WM_DEVICECHANGE );
      MessageEvents.MessageReceived += MessageEventsMessageReceived;
    }

    private static void MessageEventsMessageReceived( object sender, MessageReceivedEventArgs e ) {
      // Check if this is a notification regarding a new device.);
      if( e.Message.WParam == (IntPtr)NativeMethods.DBT_DEVICEARRIVAL ) {
        Log.Info( "New device has arrived" );

        // Retrieve the device broadcast header
        NativeMethods.DEV_BROADCAST_HDR deviceBroadcastHeader =
          (NativeMethods.DEV_BROADCAST_HDR)
          Marshal.PtrToStructure( e.Message.LParam, typeof( NativeMethods.DEV_BROADCAST_HDR ) );

        if( (int)NativeMethods.DBT_DEVTYP.DBT_DEVTYP_VOLUME == deviceBroadcastHeader.dbch_devicetype ) {
          Log.Info( "Device type is a volume (good)." );

          NativeMethods.DEV_BROADCAST_VOLUME volumeBroadcast =
            (NativeMethods.DEV_BROADCAST_VOLUME)
            Marshal.PtrToStructure( e.Message.LParam, typeof( NativeMethods.DEV_BROADCAST_VOLUME ) );

          Log.InfoFormat( "Unit masked for new device is: {0}", volumeBroadcast.dbcv_unitmask );

          int driveIndex = 1;
          int bitCount = 1;
          while( bitCount <= 0x2000000 ) {
            driveIndex++;
            bitCount *= 2;

            if( ( bitCount & volumeBroadcast.dbcv_unitmask ) != 0 ) {
              Log.InfoFormat( "Drive index {0} is set in unit mask.", driveIndex );
              Log.InfoFormat( "Device provides drive: {0}:", (char)( driveIndex + 64 ) );

              int index = driveIndex;

              Thread spawnProcessThread = new Thread( () => SpawnDeviceProcess( string.Format( "{0}", (char)( index + 64 ) ) ) );
              spawnProcessThread.Start();
            }
          }

        } else {
          Log.InfoFormat( "Device type is {0} (ignored).", Enum.GetName( typeof( NativeMethods.DBT_DEVTYP ), deviceBroadcastHeader.dbch_devicetype ) );
        }
      }
    }
  }
}

In my project, I was only interested in retrieving the drive letter for inserted USB keys. This code retrieves that drive letter and would then spawn a dedicated handler process for the device.

This was implemented in a C# service. System.Windows.Forms has to be referenced. Should work just fine.

I might be able to get the entire project onto GitHub, but it appears to be very time consuming to properly clean it up. I hope this is sufficient information to be able to replicate the result.

Oliver Salzburg
  • 21,652
  • 20
  • 93
  • 138
  • I kinda have burried the project but because it works for you with a similar goal, I'm going to accept your solution. Thanks for the hazzle :) – wullxz Aug 18 '19 at 10:29
  • @Der Hochstapler, I know this has been almost 8 months. But I am in the same situation. For some reason, for me `WndProc` is not getting fired. I am just creating a simple windows service with new instance of Form in it. That function gets called when service starts but not when the device is actually attached. Right now I am just logging the incoming message. Any ideas or suggestions? – Pratik Gaikwad Mar 27 '20 at 15:26
  • Just incase someone comes here and wants to use `Task` instead of `Thread`. I Created `Task` with `RunContinuationsAsynchronously` which works its own separate thread and solution simplifies to above one itself. – Pratik Gaikwad Mar 27 '20 at 18:46
  • @PratikGaikwad I don't think I can be of much help with that. If you aren't receiving any messages, then there's something wrong with the Window setup. If you're not receiving the device change notification, then there is a specific issue with that, which needs further investigation. The code I posted above worked for me at the time. I can't currently easily verify if it even still works. Although I'm somewhat confident the approach is still sound. – Oliver Salzburg Apr 01 '20 at 09:49
  • @DerHochstapler , I figured it out. I will create a gift and post link here just incase anyone runs into same situation – Pratik Gaikwad Apr 01 '20 at 10:27
0

The problem is that, due to the "ingenious" .Net Framework API design done by the "brilliant" Microsoft software engineers, OnCustomCommand() method from the ServiceBase class (which you could in theory override in your code to handle any service control code) only passes down the dwControl parameter -- it doesn't pass down dwEventType and lpEventData parameters from the native ServiceControlHandlerEx() callback, which are both required for proper handling of SERVICE_CONTROL_DEVICEEVENT, SERVICE_CONTROL_POWEREVENT, SERVICE_CONTROL_SESSIONCHANGE, and SERVICE_CONTROL_TIMECHANGE service control codes.

A workaround as already demonstrated in other answers is to create an invisible window, register it for specific notifications, and then forward them to your service code. However, that's a horrible cludge, and it adds a ton of totally unnecessary complexity and additional points of failure.

Better option would be to just forget ServiceBase and implement your own service class using Winodws API, P/Invoke, and marshaling, but then you are probably rightfully asking yourself "What's the point of having a framework to begin with?"

How did we get to .Net Framework version 4.8, and then to .Net Core / Standard all the way up to version 6.0 without correcting this blatant API design oversight is something to reflect upon while you are considering changing your career to something less retarded and less stressful than software development.

Glaring omissions in API design like this one (and this one is far from being the only one) are what makes .Net Framework / Core / Standard a toy API compared to native Windows API.

Igor Levicki
  • 1,017
  • 10
  • 17
  • Thanks for your answer. I read a lot of pain between the lines ^^ I wasn't really working as software developer but I am now. Thankfully not in the Windows world ;) – wullxz Feb 03 '22 at 10:28
  • @wullxz I had to get that off my chest because I just faced the same issue, and this is not the first time I've reached for the convenience of the framework only to be faced with yet another situation where the framework falls way too short of being useful. Seriously, best way to write a Windows Service in .Net Framework is -- don't do it unless you absolutely have to (self-hosted web services using OWin are a notable exception from that rule). – Igor Levicki Feb 14 '22 at 12:37