Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ServiceControlHandler used for usb device notifications, OnStop() not implementable

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;
        }
    }
}
like image 388
wullxz Avatar asked Jan 25 '13 01:01

wullxz


1 Answers

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.

like image 97
Oliver Salzburg Avatar answered Nov 13 '22 22:11

Oliver Salzburg