EternalWindows
サービス / ハンドラ関数

今回は、ServiceMainの内部について検討していきます。 前節で説明したように、ServiceMainを実行するのはサービススレッドであり、 サービススレッドはそのサービス特有の処理を行うことになります。 しかし、ServiceMainではそれら一連の処理を行う前に、 RegisterServiceCtrlHandlerExによるハンドラ関数の登録が義務付けられています。

SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerEx(
  LPCTSTR lpServiceName,
  LPHANDLER_FUNCTION_EX lpHandlerProc,
  LPVOID lpContext
);

lpServiceNameは、サービスの内部名を指定します。 lpHandlerProcは、ハンドラ関数のアドレスを指定します。 lpContextは、ユーザー定義のデータを指定します。 この引数は、ハンドラ関数の第4引数に相当することになります。 戻り値のSERVICE_STATUS_HANDLEについては、次節で説明します。

ハンドラ関数の役割を理解するためには、今一度サービスとSCMの関係を考える必要があります。 前節のメインスレッドが呼び出していたStartServiceCtrlDispatcherは、 内部でループに入り、SCMからの制御コードを受信すると説明しました。 制御コードには、一時停止や再開といったサービスの状態変更を促すものがあり、 サービスはそれに応じた処理を取らねばなりません。 StartServiceCtrlDispatcherは、この受信した制御コードをハンドラ関数へ通知します。 つまり、ハンドラ関数とはSCMからの制御コードを処理する関数です。

ハンドラ関数のプロトタイプは、次のようになります。

DWORD WINAPI HandlerEx(
  DWORD dwControl,
  DWORD dwEventType,
  LPVOID lpEventData,
  LPVOID lpContext
);

HandlerExという名前はプレースホルダであり、自由に決めて構いません。 dwControlが正に、SCMから送られてきた制御コードになります。 制御コードは、以下に示す表のいずれかになります。

制御コード 意味
SERVICE_CONTROL_STOP サービスの停止要求。 サービスの停止とは、サービススレッドがServiceMainから制御を返すことである。 HandlrExを実行するのはプライマリスレッドであるため、 実際にサービススレッドを停止するにはスレッド間通信を用いて、 サービススレッド自らが停止処理を行うべきである。
SERVICE_CONTROL_PAUSE サービスの一時停止要求。 ここで述べている一時停止とは、たとえばSuspendThreadでサービスレッドを 止めろということではなく、サービス関連の処理を行うなということである。 そのため、一時停止の処理とはサービスによってまばらといえる。
SERVICE_CONTROL_CONTINUE 一時停止を再開する要求。 サービス関連の処理を再開することになる。
SERVICE_CONTROL_INTERROGATE サービスの更新要求。 この制御コードは必ず処理しなければならず、 現在のサービスの状態をSCMに報告することになる。
SERVICE_CONTROL_SHUTDOWN システムがシャットダウンしようとしていることの通知。 サービスが何らかのデータをファイルに保存したいよう場合は、 このメッセージを処理することになる。
SERVICE_CONTROL_DEVICEEVENT デバイスイベントの通知。 dwEventTypeが、DBT_DEVICEARRIVAL、DBT_DEVICEREMOVECOMPLETE、 DBT_DEVICEQUERYREMOVE、DBT_DEVICEQUERYREMOVEFAILED、 DBT_DEVICEREMOVEPENDING、DBT_CUSTOMEVENTのいずれかになり、 lpEventDataがWM_DEVICECHANGEのlParamとなる。 この制御コードを受け取るには、事前にRegisterDeviceNotificationを呼び出しておかなければならない。
SERVICE_CONTROL_HARDWAREPROFILECHANGE ハードウェアプロファイルの変更通知。 dwEventTypeは、DBT_CONFIGCHANGED、DBT_QUERYCHANGECONFIG、 DBT_CONFIGCHANGECANCELEDのいずれかになる。
SERVICE_CONTROL_POWEREVENT 電源イベントの通知。 dwEventTypeが、WM_POWERBROADCASTのwParamとなり、 lpEventDataが同メッセージのlParamとなる。
SERVICE_CONTROL_SESSIONCHANGE セッションの変更通知。 dwEventTypeが、WM_WTSSESSION_CHANGEのwParamとなり、 lpEventDataはWTSSESSION_NOTIFICATION構造体へのアドレスを格納している。 サービスが自動起動している場合、この制御コードを受信することにより、 ユーザーのログオン等を検出するようなことができる。 ログオンしたユーザー名を取得したいような場合は、 WTSQuerySessionInformation等を呼び出すことになる。 この制御コードはWindowsXP以降のみ送られる。

このように大変多くの制御コードがありますが、 実際にはこれら全てを処理する必要はありませんし、 全ての制御コードが必ず送られるわけでもありません。 プログラムでは、SetServiceStatus(次節で説明)という関数で 処理したい任意の制御コードを指定することになるため、 実際に処理するのはその制御コードのみとなります。 上記の表を注意深く見ると分かりますが、制御コードは要求と通知のどちらかになります。 制御コードが要求の場合は、必ずSetServiceStatusを呼び出すことになります。

以下に、ServiceMainとハンドラ関数の実装例を示します。 SetServiceStatus(次節説明)では、一時停止とシャットダウンを指定したものと仮定します。

// この関数は、StartServiceCtrlDispatcherが作成したスレッドが呼び出す。
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
	RegisterServiceCtrlHandlerEx(TEXT("ServiceName"), HandlerEx, NULL);

	SetServiceStatus(...) // 簡単のため、引数は省略 

	// サービス特有の処理を行う
}

// この関数は、StartServiceCtrlDispatcherが呼び出す。
// つまり、メインスレッドによって実行される。
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
	switch (dwControl) {

	case SERVICE_CONTROL_STOP: // 停止
		break;
	
	case SERVICE_CONTROL_SHUTDOWN: // シャットダウン
		break;

	case SERVICE_CONTROL_INTERROGATE:
		break;

	}

	return NO_ERROR;
}

まず、ServiceMainはRegisterServiceCtrlHandlerExを呼び出して、 ハンドラ関数を登録します。 続いて、SetServiceStatusを呼び出して一時停止とシャットダウンの 制御コードを受け取るよう、SCMに伝えます。 実際には、SetServiceStatusが行うことはさらに複雑なのですが、 現段階ではRegisterServiceCtrlHandlerExの後には、 SetServiceStatusを呼ばなければならないことを覚えてください。

HandlerExでは、dwControlに応じて制御コードを処理します。 実際にどのような処理を行うことになるのかは次節で説明しますが、 HandlerExには30秒以内に制御を返さなければならないという制約が あることは押さえておいてください。 これは、HandlerExであまりにも時間を消費してしまうと、 次に送られてくるSCMからの制御コードを処理できないためです。 たとえば、dwControlがSERVICE_CONTROL_STOPであったとして、 もし、関数が30秒以内に制御を返さなければ、 SCMはSCPに制御が失敗したことを通知します。 具体的に述べると、SCPはサービスをControlServiceという関数で制御できますが、 このControlServiceがエラーを返すことになります。


戻る