EternalWindows
サービス / サービスの制御

前節のようなサービスの状態を表示するという機能もSCPの1つですが、 やはりSCPの意義とは、サービスを開始したり停止させたりすることでしょう。 サービスを開始するには、StartServiceを呼び出します。

BOOL StartService(
  SC_HANDLE hService,
  DWORD dwNumServiceArgs,
  LPCTSTR* lpServiceArgVectors
);

hServiceは、SERVICE_STARTアクセス権を割り当てたサービスのハンドルです。 dwNumServiceArgsとlpServiceArgVectorsは、 目的のサービスのServiceMainの第1引数と第2引数に渡ります。 一般に、サービスは自身に必要な情報をレジストリのParametersキーから 読み取るため、この2つの引数は0とNULLを指定することになるでしょう。

StartServiceが失敗する理由の1つとして、SCMデータベースのロックがあります。 これは、StartServiceでサービスが開始しようとすると同時に、 他のプログラムがSCMデータベースを変更するような事態が起こるのを防ぐためです。 ChangeServiceConfig(2)でSCMデータベースに変更を加えるようなプログラムは、 変更対象となるサービスを一時的に開始できないようするために、 LockServiceDatabaseを呼び出すべきとされています。

LockServiceDatabase(hSCManager); // SCMデータベースをロックする

ChangeServiceConfig(...); // SCMデータベースを更新する

UnlockServiceDatabase(hSCManager); // ロックを解除する

hSCManagerには、SC_MANAGER_LOCKアクセス権を割り当てていなければなりません。 SCMデータベースの状態を取得したい場合は、 QueryServiceLockStatusを呼び出すとよいでしょう。 この関数を呼ぶ場合は、SC_MANAGER_QUERY_LOCK_STATUSアクセス権が必要です。

次に、サービスを制御する方法を説明します。 サービスを制御するとは、目的のサービスに制御コードを送ることです。

BOOL ControlService(
  SC_HANDLE hService,
  DWORD dwControl,
  LPSERVICE_STATUS lpServiceStatus
);

hServiceは、サービスのハンドルです。 dwControlは、制御コードです。 この制御コードは、SERVICE_CONTROL_STOPやSERVICE_CONTROL_PAUSE、 SERVICE_CONTROL_INTERROGATEといった、サービスの開発で処理した定数です。 ここで指定した定数が、サービスのHandlerExに送られることになります。 lpServiceStatusは、目的のサービスのステータスが返ります。

今回のプログラムは、マウスの左ボタンの押下によって、 サービスの開始、又は停止を行います。

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static SC_HANDLE hService = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		SC_HANDLE hSCManager;
		
		hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
		if (hSCManager == NULL) {
			MessageBox(NULL, TEXT("SCMデータベースのオープンに失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}
		
		hService = OpenService(hSCManager, TEXT("ServiceName"), SERVICE_START | SERVICE_STOP | SERVICE_QUERY_STATUS);
		if (hService == NULL) {
			MessageBox(NULL, TEXT("サービスのオープンに失敗しました。"), NULL, MB_ICONWARNING);	
			CloseServiceHandle(hSCManager);
			return -1;
		}
		
		CloseServiceHandle(hSCManager);
	
		return 0;
	}


	case WM_LBUTTONDOWN: {
		SERVICE_STATUS ss;

		QueryServiceStatus(hService, &ss);

		if (ss.dwCurrentState == SERVICE_STOPPED) {
			if (StartService(hService, 0, NULL))
				SetWindowText(hwnd, TEXT("開始"));
		}
		else if (ss.dwCurrentState == SERVICE_RUNNING) {
			if (ControlService(hService, SERVICE_CONTROL_STOP, &ss))
				SetWindowText(hwnd, TEXT("停止"));
		}
		else
			;
		
		return 0;
	}


	case WM_DESTROY:
		if (hService != NULL)
			CloseServiceHandle(hService);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

プログラムは、QueryServiceStatusの結果によって呼び出すべき関数が変化します。 目的のサービスが停止していたときはStartServiceを呼び出し、 成功したらウインドウタイトルを「開始」とします。 逆にサービスが開始状態のときは、ControlServiceでSERVICE_CONTROL_STOPを送り、 サービスを停止させます。 これに成功した場合は、ウインドウタイトルを「停止」とします。 これら3つの関数の呼び出しのため、与えるアクセス権も多くなります。

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

SERVICE_STARTはStartServiceのため、SERVICE_STOPはSERVICE_CONTROL_STOPを送るため、 SERVICE_QUERY_STATUSは、QueryServiceStatusの呼び出しに必要です。 ところで、第2引数のServiceNameですが、 これは以前に作成したサービスのサービス名です。 既存のサービスを停止すると、何らかの問題が生じるかもしれないので、 ここでは自作サービスを対象としています。

case WM_LBUTTONDOWN: {
	SERVICE_STATUS ss;

	QueryServiceStatus(hService, &ss);

	if (ss.dwCurrentState == SERVICE_STOPPED) {
		if (StartService(hService, 0, NULL))
			SetWindowText(hwnd, TEXT("開始"));
	}
	else if (ss.dwCurrentState == SERVICE_RUNNING) {
		if (ControlService(hService, SERVICE_CONTROL_STOP, &ss))
			SetWindowText(hwnd, TEXT("停止"));
	}
	else
		;
	
	return 0;
}

前節では、QueryServiceStatusExを呼び出していましたが、 今回はdwCurrentStateのみを取得するのが目的であり、 プロセスIDやフラグは必要ないので、QueryServiceStatusで十分といえます。 StartServiceやControlServiceは、成功したら0以外の値を返すため、 その時にウインドウタイトルを更新します。 ところで、ControlServiceの第3引数にはサービスのステータスが返りますが、 これは何か使い道があるのでしょうか。 たとえば、サービスはSERVICE_CONTROL_STOPを受信したら、 SERVICE_STOPPED、又はSERVICE_STOP_PENDINGを報告しなければなりませんから、 dwCurrentStateでそれを確かめるとよいでしょう。

ControlService(hService, SERVICE_CONTROL_STOP, &ss);
if (ss.dwCurrentState == SERVICE_STOPPED || ss.dwCurrentState == SERVICE_STOP_PENDING)
	MessageBox(NULL, TEXT("サービスは停止処理に入っていません。"), NULL, MB_ICONWARNING);

SERVICE_STOP_PENDINGを報告してきたような場合、 本当にサービスが停止したのかを知りたいのであれば、 QueryServiceStatusを繰り返して呼び出す必要があるでしょう。 しかし、このQueryServiceStatusは1つの問題を抱えています。 実は、QueryServiceStatusが返すサービスのステータスは、 SCMが内部でキャッシュしている情報なのです。 そのため、このキャッシュされた情報が、 そのサービスの最新の情報を反映しているとは限りません。 キャッシュされた情報を更新し、さらにサービスの現在のステータスを 正確に取得するには、以下のようにします。

ControlService(hService, SERVICE_CONTROL_INTERROGATE, &ss);

SERVICE_CONTROL_INTERROGATEは、 サービスがSCMに現在のステータスを報告するもので、 この制御コードでのSetServiceStatus呼び出しに限っては、 キャッシュされた情報が更新されることと思われます。 ただ、ControlServiceの呼び出しというのは一種の処理要求ですから、 サービスが停止中のときはその要求に応答することはできません。 このようなとき、ControlServiceが制御を返すのは30秒後なり、 勿論、関数は失敗することになるため、十分に注意するべきことといえます。


戻る