1. 基本
Win32的には、Windowsサービスは大体以下のような仕組みになっているようです。
- まずサービス名を指定してCreateService関数でサービスを生成する。
- このサービス名とコールバック関数を指定してRegisterServiceCtrlHandler関数を実行すると、サービス自体の管理用(開始、停止等)のコントロールコードがこのコールバック関数に流れてくるようになる。また、この関数の実行時にサービスステータスハンドルが返ってくるので、これをサービスステータスの管理等に利用する。
- または、このサービス名とコールバック関数を指定してRegisterServiceCtrlHandlerEx関数を実行すると、2のコントロールコードに加えて、デバイスイベントを含めたシステムイベントのコントロールコードも流れてくるようになる(デバイスイベントの場合は、さらにサービスステータスハンドルを指定してイベントへの登録が必要。この部分はWindowを持つアプリでWindowメッセージを処理するのと基本同じ)。具体的なイベントについては、このコールバック関数(LPHANDLER_FUNCTION_EX)を参照。
2. 実装
デバイスイベントの捕捉については、ざっと探して以下の先例がありました。
このコードをダウンロードして見ると、ServiceBaseの派生クラス上で上記の3を行い、イベントを独自のコールバック関数で捕捉するようにしています。つまり、ServiceBase内と二重にRegisterServiceCtrlHandlerEx関数を実行しているわけですが、こうするとServiceBase内のコールバック関数へのポインターが上書きされるようで、ServiceBase内のサービスステータス管理用の処理が実行されなくなります。このため、独自のコールバック関数の方でSERVICE_CONTROL_STOPを捕捉してServiceBase.Stopメソッドを呼んでいます。
とりあえず確認したところ、デバイスイベントの捕捉は問題なくできましたが、致命的な問題が。サスペンド時(Windows 10では、標準では「シャットダウン」がサスペンドになる)にAccessViolationExceptionが発生します。これは少し確認したところ、RegisterServiceCtrlHandlerEx関数を実行しただけで起こり、たぶんアンマネージドなハンドル絡みだと思うので、ほぼ対処不能。
ではどうするかというと、既にServiceBase内にコールバックの仕組みはあるので、これを利用できないかとソースを改めて見ると、コールバック関数に来たコントロールコードは途中で捕捉されなければ最終的にServiceBase.OnCustomCommandメソッドに流れてくるので、これを捕捉すればよいと発見(LPHANDLER_FUNCTION_EXにおけるdwControl = ServiceBase.OnCustomCommandにおけるcommand)。カスタムコマンドは128から255までという決まりがあり、SERVICE_CONTROL_DEVICEEVENTは11なので、この範囲に入りませんが、わざわざフィルターしているわけでもないので。
ということで、これなら簡単に実装できます。デバイスイベントへの登録はWindowメッセージの場合とDEVICE_NOTIFY_SERVICE_HANDLE以外は同じ。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Runtime.InteropServices; | |
using System.ServiceProcess; | |
public partial class DeviceDetectService : ServiceBase | |
{ | |
public DeviceDetectService() | |
{ | |
InitializeComponent(); | |
} | |
private static readonly Guid GUID_DEVINTERFACE_USB_DEVICE = new Guid("A5DCBF10-6530-11D2-901F-00C04FB951ED"); | |
private IntPtr _notificationHandle; | |
protected override void OnStart(string[] args) | |
{ | |
DeviceNotification.Unregister(_notificationHandle); | |
_notificationHandle = DeviceNotification.Register(this.ServiceHandle, GUID_DEVINTERFACE_USB_DEVICE); | |
} | |
protected override void OnStop() | |
{ | |
DeviceNotification.Unregister(_notificationHandle); | |
_notificationHandle = IntPtr.Zero; | |
} | |
protected override void OnCustomCommand(int command) | |
{ | |
switch (command) | |
{ | |
case DeviceNotification.SERVICE_CONTROL_DEVICEEVENT: | |
Debug.WriteLine("USB device event received!"); | |
this.EventLog.WriteEntry("USB device event received!"); | |
break; | |
} | |
} | |
} | |
public static class DeviceNotification | |
{ | |
[DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)] | |
private static extern IntPtr RegisterDeviceNotification( | |
IntPtr hRecipient, | |
IntPtr NotificationFilter, | |
uint Flags); | |
[DllImport("User32.dll")] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
private static extern bool UnregisterDeviceNotification(IntPtr Handle); | |
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | |
private struct DEV_BROADCAST_DEVICEINTERFACE | |
{ | |
public uint dbcc_size; | |
public uint dbcc_devicetype; | |
public uint dbcc_reserved; | |
public Guid dbcc_classguid; | |
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)] | |
public string dbcc_name; | |
} | |
private const int DEVICE_NOTIFY_SERVICE_HANDLE = 0x00000001; | |
private const int DBT_DEVTYP_DEVICEINTERFACE = 0x00000005; | |
public const int SERVICE_CONTROL_DEVICEEVENT = 0x0000000B; | |
/// <summary> | |
/// Registers to device events. | |
/// </summary> | |
/// <param name="serviceHandle">Service status handle</param> | |
/// <param name="classGuid">Device interface class Guid</param> | |
/// <returns>Device notification handle</returns> | |
public static IntPtr Register(IntPtr serviceHandle, Guid classGuid) | |
{ | |
var dbcc = new DEV_BROADCAST_DEVICEINTERFACE | |
{ | |
dbcc_size = (uint)Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>(), | |
dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE, | |
dbcc_classguid = classGuid | |
}; | |
var buffer = IntPtr.Zero; | |
try | |
{ | |
buffer = Marshal.AllocHGlobal((int)dbcc.dbcc_size); | |
Marshal.StructureToPtr(dbcc, buffer, true); | |
return RegisterDeviceNotification( | |
serviceHandle, | |
buffer, | |
DEVICE_NOTIFY_SERVICE_HANDLE); | |
} | |
catch (Exception ex) | |
{ | |
Debug.WriteLine($"Failed to register.\r\n{ex}"); | |
return IntPtr.Zero; | |
} | |
finally | |
{ | |
if (buffer != IntPtr.Zero) | |
Marshal.FreeHGlobal(buffer); | |
} | |
} | |
/// <summary> | |
/// Unregisters from device events. | |
/// </summary> | |
/// <param name="notificationHandle">Device notification handle</param> | |
public static void Unregister(IntPtr notificationHandle) | |
{ | |
if (notificationHandle != IntPtr.Zero) | |
UnregisterDeviceNotification(notificationHandle); | |
} | |
} |
惜しむらくは、一緒に来るdwEventType(WM_DEVICECHANGEにおけるwParamと同じ)とlpEventData(同じくlParamと同じ)は流れてこないので、デバイスイベントが起きたことしか分からないことですが、イベントがあったことが分かればやりようはあるので。
このサンプルのレポジトリは以下。 以上で用には足りますが、ServiceBaseクラスは.NETでサービスを使うには必須の基底クラスなので、もう少し拡張にオープンにしてほしかったというのが率直な印象でした。
0 コメント :
コメントを投稿