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

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

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

0 コメント :