EternalWindows
サービス / サービスのインストール

今回は、サービスのインストールについて説明します。 サービスをインストールするとは、そのサービスを起動するための情報を 次のレジストリキーに書き込むことに相当します。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services

このレジストリキーはSCMデータベースと呼ばれ、 SCMはこのキーのサブキーをサービス名として扱い、 サービスを起動するときなどにキーから情報を取得しています。

サービスのインストールに限らず、サービスの列挙や操作といった行為を行うには、 まず、SCMデータベースのハンドルを取得しなければなりません。

SC_HANDLE OpenSCManager(
  LPCTSTR lpMachineName,
  LPCTSTR lpDatabaseName,
  DWORD dwDesiredAccess
);

lpMachineNameは、コンピュータ名を指定します。 ここで指定したコンピュータ上のSCMと接続が確立されることになります。 NULLを指定すると、ローカルコンピュータと解釈されます。 lpDatabaseNameは、NULLを指定します。 dwDesiredAccessは、SCMデータベースのハンドルを用いて どのような操作を行いたいかを示すアクセス権を指定します、 戻り値は、SCMデータベースのハンドルです。 dwDesiredAccessに指定できるアクセス権は、次のようなものがあります。

アクセス権 意味
SC_MANAGER_ALL_ACCESS この表で紹介している全てのアクセス権と STANDARD_RIGHTS_REQUIRED を指定したことと同じ意味になる。
SC_MANAGER_CONNECT SCMとの接続を有効にする。 このアクセス権は明示的に設定されていなくても常に有効である。
SC_MANAGER_CREATE_SERVICE SCMデータベースにサービスを追加するために必要な CreateServiceの呼び出しを有効にする。
SC_MANAGER_ENUMERATE_SERVICE SCMデータベースにインストールされているサービスを列挙するために必要な EnumServicesStatus(Ex)の呼び出しを有効にする。
SC_MANAGER_LOCK SCMを停止し、サービス動作を開始できないようにするために必要な LockServiceDatabaseの呼び出しを有効にする。
SC_MANAGER_QUERY_LOCK_STATUS SCMデータベースをロックしているユーザーを確認するために必要な QueryServiceLockStatusの呼び出しを有効にする。

このことから、サービスをインストールする場合には、 dwDesiredAccessにSC_MANAGER_CREATE_SERVICEを指定することになります。

サービスをインストールするには、CreateServiceを呼び出します。

SC_HANDLE CreateService(
  SC_HANDLE hSCManager,
  LPCTSTR lpServiceName,
  LPCTSTR lpDisplayName,
  DWORD dwDesiredAccess,
  DWORD dwServiceType,
  DWORD dwStartType,
  DWORD dwErrorControl,
  LPCTSTR lpBinaryPathName,
  LPCTSTR lpLoadOrderGroup,
  LPDWORD lpdwTagId,
  LPCTSTR lpDependencies,
  LPCTSTR lpServiceStartName,
  LPCTSTR lpPassword
);

大変引数の多い関数ですが、大半の引数の意味は直感的に理解できると思います。 hSCManagerは、OpenSCManagerが返したハンドルを指定します。 lpServiceNameはサービス名で、ここで指定した文字列がSCMデータベースでのサービスの識別名となります。 lpDisplayNameは、SCPがサービスを識別するために使う表示名です。 dwDesiredAccessは、戻り値となるサービスのハンドルを用いてどのような操作を行いたいかを示すアクセス権を指定します。 dwServiceTypeは、サービスのタイプを指定します。 dwStartTypeは、サービスの起動形態を指定します。 dwErrorControlは、エラーの深刻度を指定します。 lpBinaryPathNameは、サービスのEXEが存在するフルパスを指定します。 残りの引数については、必要になったときに説明します。

CreateServiceで特に重要といえる引数は、 lpServiceNameとdwServiceType、そしてdwStartTypeでしょう。 まずlpServiceNameですが、これはサービス名を示す文字列を指定します。 サービス名はサービスを識別するためのもので、 これまで紹介してきたプログラムでも使われていました。

SERVICE_TABLE_ENTRY serviceTable[] = {
	{TEXT("ServiceName"), ServiceMain}, {NULL, NULL}
}; // SERVICE_TABLE_ENTRY構造体の最初のメンバはサービス名

StartServiceCtrlDispatcher(serviceTable);

上記コードの他にも、以下の関数でも使われていました。

// 第1引数はサービス名
RegisterServiceCtrlHandlerEx(TEXT("ServiceName"), HandlerEx, NULL);

これから分かるように、CreateServiceで指定したサービス名は、 上記コードで利用しているサービス名と必ず一致しておくべきといえます。

次にdwServiceTypeですが、このサービスタイプという言葉も聞き慣れていると思います。 サービスタイプはいわば、プロセスが1つのサービスを提供するのか、 複数のサービスを提供するのかというもので、 1つのサービスを提供する場合は、SERVICE_WIN32_OWN_PROCESSを指定します。

dwStartTypeには、サービスの起動形態に関する定数を指定します。 次の定数のどちらかを指定することになると思われます。

定数 意味
SERVICE_AUTO_START 自動起動。 システム起動時と共にサービスは実行される。
SERVICE_DEMAND_START 手動起動。 StartService関数の呼び出しにより、サービスは実行される。

サーバーをサービスとして実装しているならば、 dwStartTypeにはSERVICE_AUTO_STARTを指定することが多いと思われます。

ところで、CreateServiceの戻り値の型ですが、SC_HANDLE型となっています。 実は、SC_HANDLEはSCMデータベースを表すだけでなく、 サービスそのものを表すときにも使われます。 CreateServiceやOpenServiceが返すハンドルはサービスを表していますから、 このハンドルがあればサービスを開始したり停止したりすることができます。 使い終えたSC_HANDLEは、CloseServiceHandleで閉じることになります。

今回のプログラムは、サービスのインストール及び、アンインストールを実装しています。 実際にこのプログラムを起動してサービスを実行するまでには、 いくつかの手順が必要になりますが、それに関しては後ほど説明します。

#include <windows.h>

BOOL InstallService(LPTSTR lpszServiceName);
BOOL UninstallService(LPTSTR lpszServiceName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	int   id;
	TCHAR szServiceName[] = TEXT("ServiceName");

	id = MessageBox(NULL, TEXT("インストールを行う場合は「はい」、アンインストールを行う場合は「いいえ」を選択してください。"), TEXT("OK"), MB_YESNOCANCEL);
	if (id == IDYES)
		InstallService(szServiceName);
	else if (id == IDNO)
		UninstallService(szServiceName);
	else
		;

	return 0;
}

BOOL InstallService(LPTSTR lpszServiceName)
{	
	SC_HANDLE hSCManager;
	SC_HANDLE hService;
	TCHAR     szDisplayName[] = TEXT("MyService");
	TCHAR     szServiceExe[MAX_PATH];
	
	hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
	if (hSCManager == NULL) {
		MessageBox(NULL, TEXT("SCMデータベースのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return FALSE;
	}

	GetCurrentDirectory(sizeof(szServiceExe) / sizeof(TCHAR), szServiceExe);
	lstrcat(szServiceExe, TEXT("\\myservice.exe"));

	hService = CreateService(hSCManager, lpszServiceName, szDisplayName, 0,
		SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
		szServiceExe, NULL, NULL, NULL, NULL, NULL);
	if (hService == NULL) {
		if (GetLastError() == ERROR_SERVICE_EXISTS)
			MessageBox(NULL, TEXT("既にサービスはインストールされています。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("サービスのインストールに失敗しました。"), NULL, MB_ICONWARNING);
		CloseServiceHandle(hSCManager);
		return FALSE;
	}
	
	MessageBox(NULL, TEXT("インストールが終了しました。"), TEXT("OK"), MB_OK);

	CloseServiceHandle(hService);
	CloseServiceHandle(hSCManager);
	
	return TRUE;
}

BOOL UninstallService(LPTSTR lpszServiceName)
{
	SC_HANDLE hSCManager;
	SC_HANDLE hService;
	
	hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
	if (hSCManager == NULL) {
		MessageBox(NULL, TEXT("SCMデータベースのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return FALSE;
	}

	hService = OpenService(hSCManager, lpszServiceName, DELETE);
	if (hService == NULL) {
		MessageBox(NULL, TEXT("サービスのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		CloseServiceHandle(hSCManager);
		return FALSE;
	}
	
	DeleteService(hService);

	MessageBox(NULL, TEXT("アンインストールが終了しました。"), TEXT("OK"), MB_OK);

	CloseServiceHandle(hService);
	CloseServiceHandle(hSCManager);

	return TRUE;
}

まず、Install関数の内部を順に見ていきます。

hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);

SCMデータベースをオープンします。 サービスをインストールするため、SC_MANAGER_CREATE_SERVICEを指定します。

GetCurrentDirectory(sizeof(szServiceExe) / sizeof(TCHAR), szServiceExe);
lstrcat(szServiceExe, TEXT("\\myservice.exe"));

サービスのフルパスを作成します。 このプログラムでは、サービスのEXEがこのプログラムのカレントディレクトリと 同じディレクトリに位置していることを想定しています。 ここで指定したパスに実際にEXEが存在しなかったとしても、 CreateServiceは成功することに注意してください。

hService = CreateService(hSCManager, szServiceName, szDisplayName, 0,
	SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
	szServiceExe, NULL, NULL, NULL, NULL, NULL);

第4引数にはサービスハンドルに割り当てるアクセス権を指定すべきですが、 今回はサービスをインストールするだけで実際にハンドルは使わないため、 0を指定することにしました。 第6引数をSERVICE_DEMAND_STARTにしたため、 このサービスは手動で実行しなければなりません。 サービスのエラー時に特別な動作を行うつもりがない場合は、 第7引数はSERVICE_ERROR_IGNOREで構いません。

次に、Uninstall関数の内部を見ていきます。

hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);

Uninstall関数で行うべきことはサービスのアンインストールですが、 OpenSCManagerではアンインストールに必要なアクセス権というものはありません。 このように、単純にハンドルを取得したいだけの場合は、 SC_MANAGER_CONNECTを指定しておけばよいでしょう。

hService = OpenService(hSCManager, szServiceName, DELETE);

OpenServiceは、サービスのハンドルを取得する関数です。 第2引数のサービス名でサービスを識別します。 サービスをアンインストールするときには、 第3引数のアクセス権にはDELETEを指定します。

DeleteService(hService);

サービスをアンインストールします。 つまり、szServiceNameで識別されているサブキーが、レジストリから削除されます。 サービスが実行している状態でこの関数を呼び出した場合、 そのサービスが停止すると共にアンインストールが行われます。

それでは、実際にサービスをインストールして、 サービスを実行する処理を確認していきましょう。 まず、今回のプログラムはサービスがプログラムと同じカレントディレクトリに 存在することを想定していることから、以下のような配置にする必要があるでしょう。

myserviceが、サービスです。 勿論、このサービスは前々節で取り上げたサーバーアプリケーションです。 sampleが今回のプログラムです。 プログラムを起動して、インストールの終了を知らせるメッセージボックスが表示されたら、 コントロールパネル/システムとメンテナンス/管理ツール から、サービスという名前のプログラムをクリックしてください。 自作サービスが存在することが確認できるはずです。

このMyServiceというのはサービスの表示名であり、 CreateServiceの第3引数で指定した文字列が表示されます。 このサービスにはサービスの説明文がありませんが、 これを指定する方法は次節で取り上げます。 「サービスの開始」を選択するとサービスは実行され、 前節のクライアントでサーバーからのデータを取得することができます。 ちなみに、CreateServiceの第6引数でSERVICE_AUTO_STARTを指定しても、 サービスは直ちに実行されることはありません。 SERVICE_AUTO_STARTが意味する自動起動とは、 システムの起動時と共に起動するということですから、 インストールに伴って実行されるようなことはありません。

サービスはインストールしなくては実行できませんが、 サービスを作成するたびに常にインストールが伴うとは限りません。 たとえば、インストールしたサービスに問題があったとして、 そのサービスを修正版のサービスに差し替えたい場合、 EXEを差し替えるだけで修正作業は終了となります。 つまり、既存のサービスをアンインストールして、 新しいサービスをインストールするという手順を踏む必要はありません。


戻る