EternalWindows
サービス / サービスの実装

前節までで、サービス開発のための要点は説明しました。 今回は、実際にサーバー側のコードを書き、 クライアントからの要求を受信したいと思います。 このサーバーの処理というのは、サービス系の関数でなんとかするというものではなく、 ネットワーク系の関数やスレッド間通信の関数を利用したコードで構成されます。 今回のプログラムでは、ネットワーク系の関数として名前付きパイプを、 スレッド間通信としてイベントオブジェクトを利用していますが、 どのような方法を用いるかはサービスの内容によって異なるでしょう。

今回作成するサービスは、まず名前付きパイプとイベントオブジェクトの 初期化を済まし、その後クライアントからの接続を待ちます。 クライアントが接続を試みてきたら、サーバーはサーバーのコンピュータ名を クライアントに返し、次のクライアントが接続してくるまで待機します。 このサーバーは正式にはクライアントの要求に応えているわけではなく、 また、クライアントに送信するデータがコンピュータ名だけであることから、 実際にこのようなサーバーを作成することはないと思われます。 しかし、サービスでサーバーを実装するという課題を乗り越えるには、 初歩的なサーバーのプログラムから学習するべきでしょう。

#include <windows.h>

HANDLE                g_hEventControl = NULL;
SERVICE_STATUS        g_serviceStatus = {0};
SERVICE_STATUS_HANDLE g_hServiceStatus = NULL;

VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv);
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	SERVICE_TABLE_ENTRY serviceTable[] = {
		{TEXT("ServiceName"), ServiceMain}, {NULL, NULL}
	};
	
	StartServiceCtrlDispatcher(serviceTable);

	return 0;
}

VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
	DWORD      dwEventNo;
	TCHAR      szBuf[256];
	HANDLE     hPipe;
	HANDLE     hEventConnect;
	HANDLE     hEventArray[2];
	OVERLAPPED ov;

	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);

	hPipe = CreateNamedPipe(TEXT("\\\\.\\pipe\\GetComputer"), PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, 
		PIPE_TYPE_BYTE, 1, sizeof(szBuf), sizeof(szBuf), 1000, NULL);
	hEventConnect = CreateEvent(NULL, FALSE, FALSE, NULL);
	g_hEventControl = CreateEvent(NULL, TRUE, FALSE, NULL);

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

	for (;;) {
		ZeroMemory(&ov, sizeof(OVERLAPPED));
		ov.hEvent = hEventConnect;

		ConnectNamedPipe(hPipe, &ov);

		hEventArray[0] = hEventConnect;
		hEventArray[1] = g_hEventControl;

		dwEventNo = WaitForMultipleObjects(2, hEventArray, FALSE, INFINITE) - WAIT_OBJECT_0;
		if (dwEventNo == 0) {
			DWORD dwSize;
			DWORD dwResult;

			dwSize = sizeof(szBuf);
			GetComputerName(szBuf, &dwSize);

			WriteFile(hPipe, szBuf, sizeof(szBuf), &dwResult, NULL);
			FlushFileBuffers(hPipe);
			DisconnectNamedPipe(hPipe);
		}
		else if (dwEventNo == 1) {
			CloseHandle(hPipe);
			CloseHandle(hEventConnect);
			CloseHandle(g_hEventControl);
			
			g_serviceStatus.dwCurrentState = SERVICE_STOPPED;
			g_serviceStatus.dwCheckPoint   = 0;
			g_serviceStatus.dwWaitHint     = 0;
			SetServiceStatus(g_hServiceStatus, &g_serviceStatus);

			break;
		}
		else
			break;
	}
}

DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
	switch (dwControl) {

	case SERVICE_CONTROL_STOP:
		g_serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
		g_serviceStatus.dwCheckPoint   = 0;
		g_serviceStatus.dwWaitHint     = 50000;
		SetServiceStatus(g_hServiceStatus, &g_serviceStatus);

		SetEvent(g_hEventControl);

		break;
	
	case SERVICE_CONTROL_INTERROGATE:
		SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
		break;

	default:
		break;

	}

	return NO_ERROR;
}

このプログラムは実際にコンパイル可能で、実行ファイルは作成されます。 しかし、サービスを実行するにはサービスをインストールしなければならず、 また、サービスを実行するのはあくまでSCMであることから、 ただ単に実行ファイルをクリックしてもサービスは実行されません。 サービスを実行させる方法については、後の節で説明します。

今回のプログラムを見てみると、これまでに取り上げたコードの多くが記述されています。 それもそのはずで、実際のサービスというのはServiceMainのことであるため、 それ以外の部分はどのサービスでも似たような処理になるからです。 また、実際にサービス固有の処理を行うのはServiceMainのループ内になるでしょうから、 ループに入る前のRegisterServiceCtrlHandlerExやSetServiceStatusの呼び出しも、 どのサービスでも行うことになります。 SetServiceStatusでペンディングを報告した後、サービスは自身の初期を行います。

hPipe = CreateNamedPipe(TEXT("\\\\.\\pipe\\GetComputer"), PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, 
	PIPE_TYPE_BYTE, 1, sizeof(szBuf), sizeof(szBuf), 1000, NULL);
hEventConnect = CreateEvent(NULL, FALSE, FALSE, NULL);
g_hEventControl = CreateEvent(NULL, TRUE, FALSE, NULL);

これらの関数については深く取り上げませんが、要点のみ押さえてください。 CreateNamedPipeで作成したパイプは、ConnectNamedPipeに指定することができます。 ConnectNamedPipeは、パイプにクライアントが接続してくるまで制御を返さないため、 これでは、前節で説明したHandlerExからの通知を検出することができません。 そこで、CreateNamedPipeにFILE_FLAG_OVERLAPPEDを指定して、 パイプを非同期モードに設定します。 この状態でConnectNamedPipeを呼び出した場合、OVERLAPPED構造体のhEventは、 クライアントが接続したときにシグナル状態となります。 一方、g_hEventControlは、HandlerExに停止の制御コードが送られた場合にシグナル状態になります。 この両方のイベントオブジェクトをWaitForMultipleObjectsに指定すれば、 クライアントの接続と停止の通知をどちらも検出できるようになります。

hEventArray[0] = hEventConnect;
hEventArray[1] = g_hEventControl;

dwEventNo = WaitForMultipleObjects(2, hEventArray, FALSE, INFINITE) - WAIT_OBJECT_0;

hEventArrayは、WaitForMultipleObjectsにイベントオブジェクトを指定するために用意した配列です。 WaitForMultipleObjectsの第3引数にFALSEを指定した場合、 第2引数のいずれかのイベントオブジェクトがシグナル状態になった場合に制御を返します。 どのイベントオブジェクトがシグナル状態になったかは、 WaitForMultipleObjectsの戻り値からWAIT_OBJECT_0を引くことで分かります。 これが0である場合は、hEventArray[0]のイベントオブジェクトであり、 1である場合はhEventArray[1]のイベントオブジェクトになります。 hEventArray[0]はhEventConnectを指定しているため、 dwEventNoが0の場合はクライアントがパイプに接続してきたことを意味します。

if (dwEventNo == 0) {
	DWORD dwSize;
	DWORD dwResult;

	dwSize = sizeof(szBuf);
	GetComputerName(szBuf, &dwSize);

	WriteFile(hPipe, szBuffer, sizeof(szBuf), &dwResult, NULL);
	FlushFileBuffers(hPipe);
	DisconnectNamedPipe(hPipe);
}

コンピュータ名を取得し、それをWriteFileでパイプに書き込みます。 この書き込んだ時点では、クライアントがデータを読み取ったかどうかは分からないので、 FlushFileBuffersを呼び出して、データが読み取られるまで待機します。 とはいっても、次節のクライアントはパイプに接続した後にReadFileを呼び出すため、 FlushFileBuffersは直ぐに制御を返すと思われます。 DisconnectNamedPipeでクライアントの接続を切断すれば、 同じパイプを次のクライアントとの接続に再利用することができます。

else if (dwEventNo == 1) {
	CloseHandle(hPipe);
	CloseHandle(hEventConnect);
	CloseHandle(g_hEventControl);

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

	break;
}

この処理は、g_hEventControlがシグナル状態になったときに実行されます。 まず、一連のリソースを開放し、その後にSERVICE_STOPPEDをSCMに報告します。 これらの処理に、手順前後が生じてはいけません。 SERVICE_STOPPEDを報告すると、そのサービスは30秒以内にServiceMainから 制御を返さなければならない制約があるため、SERVICE_STOPPEDを報告した後に、 メモリの開放やデータの保存を行うべきではありません。 もし、30秒以内にServiceMainから制御を返さなかった場合、 SCMはサービスを提供するプロセスを強制終了させます。 最後に、HandlerExの内部を確認しておきます。

DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
	switch (dwControl) {

	case SERVICE_CONTROL_STOP:
		g_serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
		g_serviceStatus.dwCheckPoint   = 0;
		g_serviceStatus.dwWaitHint     = 50000;
		SetServiceStatus(g_hServiceStatus, &g_serviceStatus);

		SetEvent(g_hEventControl);

		break;
	
	case SERVICE_CONTROL_INTERROGATE:
		SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
		break;

	default:
		break;

	}

	return NO_ERROR;
}

ServiceMainでSetServiceStatusを呼び出した際、 dwControlsAcceptedに指定したのはSERVICE_ACCEPT_STOPのみであったため、 ここで処理するのはSERVICE_CONTROL_STOPのみであり、 一時停止の要求やシャットダウンの通知が送られることはありません。 ただし、SERVICE_CONTROL_INTERROGATEに限っては例外です。 この制御コードはサービスの最新の状態を報告するためのもので、 必ずSetServiceStatusを呼び出すようにしなければなりません。

大抵のサービスは、通常のアプリケーションより重要な処理を行うことになるでしょうから、 エラーチェック等もより厳密に行う必要が出てくると思います。 エラーが発生した場合はファイルに保存することもできますが、 できることならイベントログを使用したほうがよいでしょう。 これならば、リモートの管理者はイベント ビューア経由でエラーを確認できるようになるからです。 もし、エラーをUIで表示したい場合は、WTSSendMessageを呼び出すようにします。

void ShowMessage(LPTSTR lpszMessage, LPTSTR lpszTitle)
{
	DWORD dwSessionId, dwMessageSize, dwTitleSize, dwResult;

	dwMessageSize = (lstrlen(lpszMessage) + 1) * sizeof(TCHAR);
	dwTitleSize = (lstrlen(lpszTitle) + 1) * sizeof(TCHAR);
	dwSessionId = WTSGetActiveConsoleSessionId();
	WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, dwSessionId, lpszTitle, dwTitleSize, lpszMessage, dwMessageSize, MB_OK, 0, &dwResult, FALSE);
}

WTSSendMessageは、原則としてテスト段階だけ呼び出してください。 サービスが実際に運用された際には、常にコンピュータの前に人が立っているとは限りませんし、 UIに応答できなければ後続の処理を実行できなくなるからです。 リファレンスでは、最終引数にFALSEを指定した場合は関数が直ちに制御を返すと記述されていますが、 そのようには動作しないようです。

MessageBoxではなくWTSSendMessageを呼び出すのは、ウインドウステーション(及びセッション)に関係する問題です。 ウインドウステーションの詳細については別の章で取り上げる予定ですが、 少しだけ例を示します。

DWORD dwSize;
TCHAR szName[256];

dwSize = sizeof(szName);
GetUserObjectInformation(GetProcessWindowStation(), UOI_NAME, szName, dwSize, NULL);

ShowMessage(szName, TEXT("OK"));

GetProcessWindowStationを呼び出せば、現在のプロセスに関連するウインドウステーションのハンドルを取得できます。 そして、これをGetUserObjectInformationにUOI_NAMEを指定すれば、ウインドウステーションの名前を取得できます。 通常のアプリケーションならばこの値はwinsta0になるはずですが、 サービスの場合は異なった値が表示されるはずです。 これは簡単にいえば、通常のアプリケーションとサービスが異なるウインドウステーション上で実行しているということであり、 自分とは別のウインドウステーションにUIを表示したければ、 そのウインドウステーションにプロセスを関連付けると共に、 ウインドウステーションへのアクセス権を設定する必要があるのです。 しかし、WTSSendMessageを呼び出した場合は、そうした処理を行わなくても特別にUIを表示できます。

サービスからプロセスの作成

既に述べたように、サービスはUIを表示するべきではありませんが、 どうしてもUIを表示したい場面もあるかと思います。 たとえば、自社の製品がアップデートしているかを調べるサービスは、 アップデートを検出した場合にユーザーへその旨を表示したいと思うかもしれません。 このような場合は、その結果を表示するためのクライアントを用意しておき、 それをサービスから起動するようにすればよいでしょう。 しかし、単純にCreateProcessを呼び出すだけでは、クライアントがサービスのアカウントで動作してしまうため、 現在ログオンしているユーザーのトークンを取得し、 それをCreateProcessAsUser(またはCreateProcessWithTokenW)に指定する必要があります。 クライアントがログオンしたタイミングは、ハンドラ関数でSERVICE_CONTROL_SESSIONCHANGEを検出することで分かります。

case SERVICE_CONTROL_SESSIONCHANGE:
	if (dwEventType == WTS_SESSION_LOGON)
		g_bLogon = TRUE;
	else
		g_bLogon = FALSE;
	break;

SERVICE_STATUS構造体のdwControlsAcceptedにSERVICE_ACCEPT_SESSIONCHANGEを指定していた場合、 SERVICE_CONTROL_SESSIONCHANGEが通知されるようになります。 dwEventTypeがWTS_SESSION_LOGONである場合はユーザーがログオンしたことを意味し、 この場合はログオンを示すフラグをTRUEに設定します。 サービスは、プロセスを起動したくなった段階でこの変数を確認し、 TRUEである場合はプロセスを起動します。

if (g_bLogon) {
	HANDLE              hToken;
	LPVOID              lpEnvironment;
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;

	WTSQueryUserToken(WTSGetActiveConsoleSessionId(), &hToken);

	CreateEnvironmentBlock(&lpEnvironment, hToken, TRUE);

	ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
	startupInfo.cb        = sizeof(STARTUPINFO);
	startupInfo.lpDesktop = TEXT("winsta0\\default");
	CreateProcessAsUser(hToken, L"C:\\Windows\\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &startupInfo, &processInformation);

	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);
}

WTSQueryUserTokenを呼び出せば、第1引数のセッションにログオンしているユーザーのトークンを取得できます。 セッションのIDはWTSGetActiveConsoleSessionIdで取得していますが、 先のSERVICE_CONTROL_SESSIONCHANGEでも取得できます。 この場合は、lpEventDataをPWTSSESSION_NOTIFICATIONでキャストし、 dwSessionIdメンバを保存するようにします。 トークンを取得したらCreateEnvironmentBlockでユーザーの環境ブロックを取得し、 これとトークンをCreateProcessAsUserに指定します。 WTS APIの呼び出しにはwtsapi32.hとwtsapi32.libが必要であり、 CreateEnvironmentBlockの呼び出しにはuserenv.hとuserenv.libが必要になります。

UACが有効である場合、現在ログオンしているユーザーが管理者であっても、 トークンの内容は制限されています。 つまり、作成されたプロセスは制限された状態で実行されることになります。 もし、UACが有効な場合でも管理者として動作させたいならば、次のような処理を加えます。

HANDLE               hTokenFull;
DWORD                dwLength;
TOKEN_LINKED_TOKEN   linkedToken;
TOKEN_ELEVATION_TYPE tokenElevationType;

WTSQueryUserToken(WTSGetActiveConsoleSessionId(), &hToken);

GetTokenInformation(hToken, TokenElevationType, &tokenElevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwLength);
if (tokenElevationType == TokenElevationTypeLimited) {
	GetTokenInformation(hToken, TokenLinkedToken, &linkedToken, sizeof(TOKEN_LINKED_TOKEN), &dwLength);
	hTokenFull = linkedToken.LinkedToken;
}
else
	hTokenFull = NULL;

GetTokenInformationにTokenElevationTypeを指定し、その結果がTokenElevationTypeLimitedである場合は、 トークンがUACによって制限されていることを意味します。 このようなとき、トークンには昇格されたトークンが関連付けられており、 GetTokenInformationにTokenLinkedTokenを指定することで取得できます。 よって、後はhTokenFullをCreateProcessAsUserに指定することで、プロセスを管理者として動作させることができます。 使い終えたhTokenFullは、CloseHandleで閉じるようにします。



戻る