2021/05/03

Windowsサービスでデバイスイベントを捕捉する

実行ファイルをWindowsサービスとして動かすために.NETではSystem.ServiceProcess名前空間にServiceContollerServiceBaseクラスが用意されていて、これらに則れば比較的容易にサービスを実装できますが、このServiceBaseの派生クラスからデバイスイベント(USBデバイスの着脱)を捕捉するにはどうすればいいか、鉄板的なものが見当たらなかったので、メモしておきます。

1. 基本

Win32的には、Windowsサービスは大体以下のような仕組みになっているようです。
  1. まずサービス名を指定してCreateService関数でサービスを生成する。
  2. このサービス名とコールバック関数を指定してRegisterServiceCtrlHandler関数を実行すると、サービス自体の管理用(開始、停止等)のコントロールコードがこのコールバック関数に流れてくるようになる。また、この関数の実行時にサービスステータスハンドルが返ってくるので、これをサービスステータスの管理等に利用する。
  3. または、このサービス名とコールバック関数を指定してRegisterServiceCtrlHandlerEx関数を実行すると、2のコントロールコードに加えて、デバイスイベントを含めたシステムイベントのコントロールコードも流れてくるようになる(デバイスイベントの場合は、さらにサービスステータスハンドルを指定してイベントへの登録が必要。この部分はWindowを持つアプリでWindowメッセージを処理するのと基本同じ)。具体的なイベントについては、このコールバック関数(LPHANDLER_FUNCTION_EX)を参照。
ServiceBaseはこの2と、3のシステムイベントについては電源イベント(SERVICE_CONTROL_POWEREVENT)とセッションイベント(SERVICE_CONTROL_SESSIONCHANGE)まで実装していますが、デバイスイベント(SERVICE_CONTROL_DEVICEEVENT)は実装していません。というか、ソースを見ると、実装しかけたまま放置されたようで、これも実装してあれば手間が省けたのですが。

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以外は同じ。
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);
}
}
なお、OnStartでRegisterの前にUnregisterしているのは、OnStartの後に必ずOnStopが来るとは限らないため。

惜しむらくは、一緒に来るdwEventType(WM_DEVICECHANGEにおけるwParamと同じ)とlpEventData(同じくlParamと同じ)は流れてこないので、デバイスイベントが起きたことしか分からないことですが、イベントがあったことが分かればやりようはあるので。

このサンプルのレポジトリは以下。 以上で用には足りますが、ServiceBaseクラスは.NETでサービスを使うには必須の基底クラスなので、もう少し拡張にオープンにしてほしかったというのが率直な印象でした。

0 コメント :