EternalWindows
サービス / ステータス報告

今回は、SetServiceStatus関数の使い方を見ていきます。 この関数の目的は、サービスの状態をSCMに報告することです。

BOOL SetServiceStatus(
  SERVICE_STATUS_HANDLE hServiceStatus,
  LPSERVICE_STATUS lpServiceStatus
);

hServiceStatusは、RegisterServiceCtrlHandlerExの戻り値を指定します。 lpServiceStatusは、SERVICE_STATUS構造体のアドレスです。 この構造体は、以下のように定義されています。

typedef struct _SERVICE_STATUS {
  DWORD dwServiceType;
  DWORD dwCurrentState;
  DWORD dwControlsAccepted;
  DWORD dwWin32ExitCode;
  DWORD dwServiceSpecificExitCode;
  DWORD dwCheckPoint;
  DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;

個々のメンバの意味を以下の表に示します。

メンバ 意味
dwServiceType サービスのタイプとは、そのプロセスが1つのサービスを提供するか、 複数のサービスを提供するかどうかのことだと考えると分かりやすい。 SERVICE_WIN32_OWN_PROCESSを指定すると1つのサービスのみ提供し、 SERVICE_WIN32_SHARE_PROCESSならば複数のサービスということになる。
dwCurrentState サービスの現在の状態を示すメンバ。 たとえば、サービスが初期化作業をまだ済ませておらず、初期化ための時間がさらに必要な場合は、 ペンディング状態であることを報告すべくSERVICE_START_PENDINGを指定する。 その後、サービスが無事に実行段階に入ったならばSERVICE_RUNNINGを指定する。
dwControlsAccepted SCMから受信したい制御コードを指定する。 たとえば、SERVICE_ACCEPT_SHUTDOWNを指定すれば、 HandlerExにSERVICE_CONTROL_SHUTDOWNが送られることになる。 一般に、ここで受信する旨を指定してない制御コードはHandlerExに送られることはない。
dwWin32ExitCode サービスの開始時、もしくはサービスの停止時にエラーが発生してしまい、 実際に処理を行うことができないような場合、このメンバにWin32に準じた エラーコード(GetLastErrorが返すようなコード)を指定する。 サービスがエラーを報告した場合、SCMデータベースで該当サービスの検索が行われ、 そのサービスのErrorControl値によってしかるべき処理がSCMによって行われる。 エラーが発生していない場合は、NO_ERRORを指定する。
dwServiceSpecificExitCode Win32のエラーコードではなく、オリジナルのエラーコードを報告したいような場合、 dwWin32ExitCodeにERROR_SERVICE_SPECIFIC_ERRORを指定することにより、 dwServiceSpecificExitCodeにオリジナルのエラーコードを指定できる。
dwCheckPoint サービスがペンディングを報告した場合、dwCheckPoint以内に再びSetServiceStatusを 呼ぶことになるが、未だ処理が未解決(つまり、ペンディング)のような場合は、 今回のSetServiceStatusでもペンディングを報告するようなことになる。 このような状態が続くとき、dwCheckPointを1つずつカウントしていくことにより、 どのステップまでは処理が進んでいるのかを把握できるようにする。
dwWaitHint dwCurrentStateにて、SERVICE_START_PENDINGやSERVICE_STOP_PENDINGなどを 指定した場合、実際の処理を何ミリ秒後に行うかをこのメンバに指定する。 SERVICE_START_RUNNINGを報告するときなどは既に処理を終えているため、 そのときはこのメンバとdwCheckPointは0にすべきである。

この中で理解しにくいメンバはdwCurrentState及び、dwCheckPointとdwWaitHintでしょう。 これらのメンバの意味を理解するには、 SetServiceStatusの目的が サービスの状態をSCMに通知することであったことを意識する必要があります。 1つの例を考えてみましょう。 今、HandlerExにSERVICE_CONTROL_STOP要求が送られたとします。 HandlerExはこの要求を受けとったらサービスを停止すると共に、 停止要求を処理したことをSetServiceStatusでSCMに報告することになります。 以下にコード例を示します。

case SERVICE_CONTROL_STOP:
	// ここでサービスを停止するための処理を行う

	g_serviceStatus.dwCurrentState = SERVICE_STOPPED;
	g_serviceStatus.dwCheckPoint   = 0;
	g_serviceStatus.dwWaitHint     = 0;
	SetServiceStatus(g_hServiceStatus, &g_serviceStatus); // サービスが停止したことをSCMに報告

	break;

まず、サービスを停止するための処理を行います。 その後、dwCurrentStateにSERVICE_STOPPEDを指定してSetServiceStatusを呼び出します。 このSERVICE_STOPPEDが停止を意味しており、これを確認したSCMは、 サービスが停止したものと解釈するようになります。

サービスを停止するための処理というのは、具体的にはどのようなものになるのでしょうか。 まず、サービスとはServiceMain内における処理の事です。 これを停止するとは、ServiceMainから制御を返すということを意味するため、 そのような処理を促すコードを実行すればよいことになります。 つまり、上記コードは実は間違っており、SERVICE_CONTROL_STOPで行うことは停止処理でもなければ、停止の報告でもありません。 ここで本当に行うべきことは、ServiceMainに停止処理を行うよう伝えることであり、 それと同時に、今この時点で直ちに停止は行わないということをSCMに対して報告することなのです。

case SERVICE_CONTROL_STOP:
	g_serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
	g_serviceStatus.dwCheckPoint   = 0;
	g_serviceStatus.dwWaitHint     = 50000;
	SetServiceStatus(g_hServiceStatus, &g_serviceStatus); // 停止処理はまだ完了していないことをSCMに報告

	// ここでServiceMainに停止処理を行うよう伝える

	break;

SERVICE_STOP_PENDINGのPENDINGは未解決という意味で、 今はまだ要求された処理を行えないことを表しています。 つまり、後で処理を行うから、もうしばらく待ってほしいということです。 このようなときは、dwWaitHintに今から何秒以内に処理を行うかの値を代入します。 上記コードでは50000となっているため、実際にサービスを停止させる処理は 50秒以内に行うことSCMに報告していることになります。 しかし、50秒以内にサービス停止をするとSCMに報告しておきながら、 何らかの事情により50秒以内に処理をできないような場合は、 dwCheckPointを1つ増やして未だペンディング状態であることを報告します。 dwCheckPointは開発者のためのメンバであり、この値を参照することにより、 サービスがどのステップまで処理を行っているかを確認することができます。

さて、前節ではServiceMain内部で、 SCMからどの制御コードを受信するかを選択するためにSetServiceStatusを呼び出しましたが、 このときのdwCurrentStateには何を指定するのでしょうか。 実はSCMはサービススレッドの初期化として、 dwCurrentStateをSERVICE_START_PENDING、 dwWaitHintを2000、dwCheckPointを0に設定しています。 これはつまり、ServiceMain開始から2秒以内にSERVICE_START_RUNNINGを、 SCMに報告しなければならないという制約が生じていると解釈できます。 SERVICE_START_RUNNINGを報告するというのは、そのサービスが既に初期化を済まし、 サービス特有の処理を行える体制に入ったということなのですが、 2秒以内にサービスの初期化を行えそうもない場合はどうするのでしょうか。 実はこれも先に示したような、ペンディングの報告で時間を延長することができます。 次のコードは、ServiceMainの基本となる処理を全て行っています。

VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
	g_hServiceStatus = RegisterServiceCtrlHandlerEx(TEXT("ServiceName"), HandlerEx, NULL);

	g_serviceStatus.dwServiceType             = SERVICE_WIN32_OWN_PROCESS;
	g_serviceStatus.dwCurrentState            = SERVICE_START_PENDING;
	g_serviceStatus.dwControlsAccepted        = SERVICE_ACCEPT_STOP;
	g_serviceStatus.dwWin32ExitCode           = NO_ERROR;
	g_serviceStatus.dwServiceSpecificExitCode = 0;
	g_serviceStatus.dwCheckPoint              = 1;
	g_serviceStatus.dwWaitHint                = 30000;

	SetServiceStatus(g_hServiceStatus, &g_serviceStatus);

	// ここにサービスを初期化するためのコードを書く
	
	g_serviceStatus.dwCurrentState = SERVICE_RUNNING;
	g_serviceStatus.dwCheckPoint   = 0;
	g_serviceStatus.dwWaitHint     = 0;
	SetServiceStatus(g_hServiceStatus, &g_serviceStatus);

	for (;;) {
		// サービス固有の処理を行う
	}
}

まず、ハンドラ関数を登録してサービスのハンドルを取得します。 このハンドルがなければSetServiceStatusが呼び出せないため、 ServiceMainでの最初の作業は必ずRegisterServiceCtrlHandlerExの呼び出しとなります。 最初のSetServiceStatusの呼び出しは、次のようになっています。

g_serviceStatus.dwServiceType             = SERVICE_WIN32_OWN_PROCESS;
g_serviceStatus.dwCurrentState            = SERVICE_START_PENDING;
g_serviceStatus.dwControlsAccepted        = SERVICE_ACCEPT_STOP;
g_serviceStatus.dwWin32ExitCode           = NO_ERROR;
g_serviceStatus.dwServiceSpecificExitCode = 0;
g_serviceStatus.dwCheckPoint              = 1;
g_serviceStatus.dwWaitHint                = 30000;

SetServiceStatus(g_hServiceStatus, &g_serviceStatus);

SERVICE_STATUS構造体のメンバを初期化します。 プロセスは、サービスを1つ提供するものと仮定しているため、 dwServiceTypeはSERVICE_WIN32_OWN_PROCESSを指定します。 dwCurrentStateをSERVICE_START_PENDINGとしているのは、 先に述べた2秒という初期化の時間を延長させるためのものです。 そのため、dwWaitHintに30秒という時間を指定します。 また、どのステップまで処理できているかを明示しなくてはならないため、 dwCheckPointが1つ増やしています。 dwControlsAcceptedでは、SCMから受信する制御コードを停止のみと仮定し、 SERVICE_ACCEPT_STOPを指定しています。 2回目のSetServiceStatusの呼び出しは、次のようになっています。

g_serviceStatus.dwCurrentState = SERVICE_RUNNING;
g_serviceStatus.dwCheckPoint   = 0;
g_serviceStatus.dwWaitHint     = 0;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);

これらのコードは、サービスの初期化が終了した後に実行されます。 dwCurrentStateにSERVICE_RUNNINGを指定することにより、SCMはサービスが正常に実行できたことを理解できます。 このように特定の処理の完了したときには、dwWaitHintとdwCheckPointは共に0になります。

for (;;) {
	// サービス固有の処理を行う
}

初期化が終了したため、いよいよサービス特有の処理を行うことになります。 大抵の場合は、サービスが行うことはサーバー側の処理ですから、 クライアントとのコネクションを確立して要求に応える形になるでしょう。 ループから抜け出すのは大抵の場合、停止かシャットダウンの制御コードを受信したときになると思われます。


戻る