EternalWindows
サービス / SCMデータベース

前節で述べたように、サービスをインストールするとは、 そのサービスの情報をSCMデータベースに書き込むことですが、 具体的にはどのような情報が書き込まれているのでしょうか。 次の図は、前節でインストールしたサービス(ServiceName)のキーを レジストリエディタで開いたものです。

個々のエントリの意味と取りうるエントリの種類は以下のようになります。

エントリ 意味
Start サービスの起動形態。 CreateServiceのdwServiceTypeに相当。
ErrorControl サービスの開始エラー時に行う処理の度合い。 CreateServiceのdwErrorControlに相当。
Type サービスのタイプ。 CreateServiceのdwStartTypeに相当。
Group サービスが所属するグループ。 CreateServiceのGroupに相当。
ImagePath サービスのフルパス。 CreateServiceのlpBinaryPathNameに相当。
DepandOnGroup サービスが依存するグループ。 サービスは、このグループに属しているサービスが実行されない限り、 実行されることはない。 CreateServiceのlpDependenciesに相当。
DepandOnService サービスが依存するサービス。 ここで指定したサービスが実行されない限り、サービスは実行されることはない。 CreateServiceのlpDependenciesに相当。
ObjectName サービスが動作するアカウント。 このエントリが存在しない場合は、ローカルシステムアカウントで動作する。 CreateServiceのlpServiceStartNameに相当。
DisplayName サービスの表示名。 CreateServiceのlpDisplayNameに相当。
Description サービスの説明文。 CreateServiceで指定することはできない。
FailureActions
(FailureCommand, RebootMessage)
サービスが失敗したときに行うアクション。 CreateServiceで指定することはできない。
Enum(サブキー) CreateServiceの呼び出しによって自動的に作成される。 詳細は不明。
Security(サブキー) サービスのセキュリティ記述子。 CreateServiceを呼び出した時点で自動的にこのキーは作成されるが、 SetServiceObjectSecurityで独自のセキュリティ記述子に変更することもできる。
Parameters(サブキー) そのサービス固有のデータをレジストリに書き込みたいような場合、 サービス名のサブキーとしてこのキーを作成する。 このキーを作成するには、明示的にレジストリ関数を呼び出さなくてはならない。

上記エントリのDescription及び、FailureActionsはCreateServiceで指定することができません。 SCMデータベースの実体がレジストリキーであることから、 レジストリ関数を使うことで自らエントリを作成することも可能ではありますが、 エントリ作成用の関数があるならば、それを呼び出したほうが効率的です。 実はChangeServiceConfig2という関数を呼び出せば、Description及び、 FailureActionsを作成することができます。 (ChangeServiceConfigという関数もありますが、 この関数は既存のパラメータを変更するときに使います。)

BOOL ChangeServiceConfig2(
  SC_HANDLE hService,
  DWORD dwInfoLevel,
  LPVOID lpInfo
);

hServiceは、サービスのハンドルを指定します。 大抵の場合、サービスのハンドルはOpenServiceで取得しますが、 インストールついでにこの関数を呼び出したならば、 CreateServiceの戻り値を指定することになるでしょう。 dwInfoLevelは、以下の定数のいずれかを指定することになります。

定数 意味
SERVICE_CONFIG_DESCRIPTION サービスの説明文を設定するためのSERVICE_DESCRIPTION構造体のアドレスを指定する。
SERVICE_CONFIG_FAILURE_ACTIONS サービスの失敗時に行うアクションのためのSERVICE_FAILURE_ACTIONS構造体のアドレスを指定する。

SERVICE_DESCRIPTION構造体は、以下のように定義されています。

typedef struct _SERVICE_DESCRIPTION {
  LPTSTR lpDescription;
} SERVICE_DESCRIPTION, *LPSERVICE_DESCRIPTION;

lpDescriptionに、サービスの説明文を指定します。 次のコードは、SERVICE_CONFIG_DESCRIPTIONを指定する例を示しています。

SERVICE_DESCRIPTION desc;

desc.lpDescription = TEXT("自作サービスです。");
	
ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &desc);

これは、特に問題はないと思われます。 hServiceには、SERVICE_CHANGE_CONFIGアクセス権が割り当てられてなければなりません。

さて、SERVICE_CONFIG_FAILURE_ACTIONSですが、 このFAILURE_ACTIONSというのは、サービスがSERVICE_STOPPEDを報告せずに終了した ようなとき(つまり、失敗)に、SCMによって行われるアクションのことを指しています。 このアクションには、サービスの再開、コンピュータの再起動、指定したプログラムの実行、 の3つがあり、この中の1つ又は複数を指定することができます。 たとえば、サービスの再開を指定していた場合、 そのサービスが他のプロセスによって強制終了されたとしても、 サービスはSCMによって再び実行されることになります。

SCMはサービスに対して、そのサービスが失敗した回数をカウントとしており、 この失敗した回数によって実行するアクションを決定します。 このため、アクションを複数指定するような場合は、 1回目の失敗時にはサービスの再開、2回目の失敗にはプログラムの実行などという要領で、 アクションを受け取る配列を初期化します。

typedef struct _SERVICE_FAILURE_ACTIONS {
  DWORD dwResetPeriod;
  LPTSTR lpRebootMsg;
  LPTSTR lpCommand;
  DWORD cActions;
  SC_ACTION* lpsaActions;
} SERVICE_FAILURE_ACTIONS, *LPSERVICE_FAILURE_ACTIONS;

dwResetPeriodは、失敗した回数をリセットする時間を秒単位で指定します。 サービスが実行されており、さらにdwResetPeriod秒を経過したならば、 そのときに失敗した回数が0になります。 INFINITEを指定した場合、失敗した回数がリセットされることはありません。 lpRebootMsgは、コンピュータの再起動するときに、 サーバーに送信する文字列を指定します。 lpCommandは、プログラムの実行するときの文字列です。 これは、CreateProcess関数の第2引数に相当します。 cActionsは、実行するアクションの数です。 lpsaActionsは、cActionsを要素数とした配列です。 個々の要素がアクションに相当します。

typedef struct _SC_ACTION {
  SC_ACTION_TYPE Type;
  DWORD Delay;
} SC_ACTION, *LPSC_ACTION;

Typeは、アクションの種類を表す定数を指定します。

定数 意味
SC_ACTION_NONE アクションは行わない。
SC_ACTION_REBOOT コンピュータを再起動する。 この場合の再起動とは特別なダイアログが表示されるわけではなく、 Delayで指定した時間後にいきなり再起動が開始される。 この定数を指定する場合は、呼び出し側トークンの SeShundownName特権を有効にしておかなければならない。
SC_ACTION_RESTART サービスを再開する。 この定数を指定する場合は、ChangeServiceConfig2のhServiceに SERVICE_STARTアクセス権を割り当てておく必要がある。
SC_ACTION_RUN_COMMAND lpCommandで指定したプログラムを実行する。

Delayは、アクションを実行するまでの待ち時間をミリ秒で指定します。

今回のプログラムは、サービスのFailureActionsとして、 サービスの再開とプログラムの実行の2つのアクションを追加します。 前節のプログラムでサービスを事前にインストールしておいてください。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	SC_HANDLE               hSCManager;
	SC_HANDLE               hService;
	SC_ACTION               action[2];
	SERVICE_FAILURE_ACTIONS failureActions;

	hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
	if (hSCManager == NULL) {
		MessageBox(NULL, TEXT("SCMデータベースのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	hService = OpenService(hSCManager, TEXT("ServiceName"), SERVICE_CHANGE_CONFIG | SERVICE_START);
	if (hService == NULL) {
		MessageBox(NULL, TEXT("サービスのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		CloseServiceHandle(hSCManager);
		return 0;
	}

	action[0].Type  = SC_ACTION_RESTART;
	action[0].Delay = 0;

	action[1].Type  = SC_ACTION_RUN_COMMAND;
	action[1].Delay = 0;
	
	failureActions.dwResetPeriod = INFINITE;
	failureActions.lpRebootMsg   = TEXT("");
	failureActions.lpCommand     = TEXT("c:\\sample.exe");
	failureActions.cActions      = 2;
	failureActions.lpsaActions   = action;

	ChangeServiceConfig2(hService, SERVICE_CONFIG_FAILURE_ACTIONS, &failureActions);

	MessageBox(NULL, TEXT("設定を変更しました。"), TEXT("OK"), MB_OK);
	
	CloseServiceHandle(hSCManager);
	CloseServiceHandle(hService);

	return 0;
}

まず、サービスのハンドルを取得する部分から見ていきます。

hService = OpenService(hSCManager, TEXT("ServiceName"), SERVICE_CHANGE_CONFIG | SERVICE_START);

第2引数のServiceNameは、前節でインストールしたサービスのサービス名です。 SERVICE_CHANGE_CONFIGは、ChangeServiceConfig2の呼び出しのために必要です。 SERVICE_STARTは、実行するアクションとしてサービスの再開を選択した場合は、 必ず指定しなければなりません。

action[0].Type  = SC_ACTION_RESTART;
action[0].Delay = 0;

action[1].Type  = SC_ACTION_RUN_COMMAND;
action[1].Delay = 0;

SC_ACTION構造体を初期化します。 まず、第1の要素はサービスの再開を意味する、SC_ACTION_RESTARTを指定しました。 これにより、サービスの1回目の失敗は、サービスの再開というアクションが行われます。 第2の要素には、SC_ACTION_RUN_COMMANDを指定しました。 これにより、サービスの2回目の失敗は指定したプログラムの実行となります。 サービスの失敗した回数が、実行するアクションの数を上回る場合、 最後のアクションが繰り返して実行されることになります。 Delayが共に0であることから、これらのアクションがサービスの失敗時に 直ちに実行されることがわかります。

failureActions.dwResetPeriod = INFINITE;
failureActions.lpRebootMsg   = TEXT("");
failureActions.lpCommand     = TEXT("c:\\sample.exe");
failureActions.cActions      = 2;
failureActions.lpsaActions   = action;

実行するアクションの数は2つであるため、cActionsは2となります。 そして、実行するアクションのうち、プログラムの実行を選択したため、 lpCommandに実行したいプログラムのパスを指定します。 lpRebootMsgは、コンピュータを再起動するときに使うメンバですが、 使わない場合は上記のように初期化してください。 dwResetPeriodがINFINITEであることから、サービスの失敗した回数はリセットされません。 つまり、初めて失敗した場合はサービスが再開されますが、 それ以降の失敗は常にプログラムの実行となります。

FailureActionsの目的は、サービスが失敗したときに何らかのアクションを実行することですが、 ここに、失敗した回数というものが絡んでくるのは理解しがたいことだと思います。 大抵の場合は、どれだけ失敗しても同じアクションを実行したいでしょうから、 失敗した回数を考慮する必要性はなかなか見出せないものです。 今回のプログラムでは、サンプルということもあり2つのアクションを指定しましたが、 実際にはcActionsは1とし、1つのアクションのみを実行することになると思います。

最後に、ChangeServiceConfig2で書き込んだレジストリエントリを 削除する方法を紹介します。 Descriptionを削除するには、以下のようにします。

SERVICE_DESCRIPTION desc;

desc.lpDescription = TEXT("");
	
ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &desc);

NULLを指定するのではなく、空の文字列を指定するのが重要なところです。

続いて、FailureActionsの削除方法です。 実は、SERVICE_CONFIG_FAILURE_ACTIONSを指定した場合、 FailureActionsの他にFailureCommandやRebootMessageといった エントリも追加されることがあります。 次のコードは、それらをもまとめて削除します。

failureActions.dwResetPeriod = 0;
failureActions.lpRebootMsg   = TEXT("");
failureActions.lpCommand     = TEXT("");
failureActions.cActions      = 0;
failureActions.lpsaActions   = &action;

ChangeServiceConfig2(hService, SERVICE_CONFIG_FAILURE_ACTIONS, &failureActions);

cActionsを0とし、lpsaActionsをNULLでない値を指定することにより、 FailureActionsは削除されます。 また、lpCommandとlpRebootMsgに空の文字列を指定することにより、 FailureCommandとRebootMessageのエントリが削除されます。


戻る