CX5 SOFTWARE

Windowsサービス

2009.9.22


目次

1. はじめに
2. Windowsサービスの構成
3. Visual Studio 2008での新規プロジェクトの作成
4. ロギング
5. サービスメイン関数
6. サービスステータスの通知
7. コントロールハンドラ
8. scコマンドを用いたサービスの登録、削除

表の一覧

6.1. サービスステータス構造体(SERVICE_STATUS)の項目
7.1. 制御コード

第1章 はじめに

ここで扱う「Windowsサービス」ですが、コントロールパネルの管理ツールの「サービス」で一覧に表示されるサービスのことを指しています。MSDNライブラリでもServicesという名前で情報が載っています。ただ、このあまりにも一般的な「サービス」という単語ですと、検索してもなかなか情報が見つかりません。「Windowsサービス」や「NTサービス」というキーワードで検索するといくらか情報が見つかります。

それほど難しいわけではないですが、あまり情報がなかったのでまとめてみました。

ここで用いたサンプルのソースコードはここからダウンロードできます。

第2章 Windowsサービスの構成

Windowsサービスを統括しているのがSCM(Service Control Manager)です。SCMはレジストリ上にサービスを管理するためのデータベースを持っています。レジストリ上にあるデータベースですが、これを直接編集してはなりません。代わりに、Windows APIもしくはscコマンドを用いてサービスの登録、設定変更、削除等を行います。

Windowsサービスでは、以下の3つの異なる役割を持つプログラムで構成されます。

サービスプログラム(Service program)

サービス本体(EXE)です。

サービス構成プログラム(Service configuration program)

サービスの登録、設定変更、削除を行うプログラムです。scコマンドもその一つです。サービス本体(EXE)にオプションを指定するとサービスの登録・削除を行えるようにしているものもあります。

サービス制御プログラム(Service control program)

サービスを開始、停止するプログラムです。scコマンドやコントロールパネルのサービスもその一つです。

第3章 Visual Studio 2008での新規プロジェクトの作成

新しいWindowsサービスを作成してみるため、Visual Studioで新規プロジェクトを作成しましょう。ここではVisual Studio 2008を使用しています。

Windowsサービスは、新規の「Win32コンソールアプリケーション」として作成します。Visual Studioを起動したら「ファイル」-「新規作成」-「プロジェクト」を選択し、表示される以下のダイアログで「Win32コンソールアプリケーション」を選択します。プロジェクト名に適当な名前を指定します。

「OK」ボタンを押すと、以下のダイアログが表示されますので、「次へ」ボタンを押します。

「アプリケーションの設定」ダイアログでは、デフォルトのまま「コンソールアプリケーション」を選択します。

新規のプロジェクトが生成されたら、stdafx.hに、「windows.h」のインクルード宣言を追加します。

// stdafx.h : 標準のシステム インクルード ファイルのインクルード ファイル、または
// 参照回数が多く、かつあまり変更されない、プロジェクト専用のインクルード ファイル
// を記述します。
//

#pragma once

#include "targetver.h"

#include <stdio.h>
#include <tchar.h>

// TODO: プログラムに必要な追加ヘッダーをここで参照してください。

#include <windows.h>

第4章 ロギング

Windowsサービスを開発する上で大変なのが開発中のデバッグです。当然ながら、Visual Studioから、直接、Windowsサービスを起動することはできません。サービスを起動後に、Visual Studioのデバッガからプロセスにアタッチしてデバッグすることは可能ですが、ちょっと処理の流れを見たいだけのために、いちいちステップ実行やブレークポイントの設定をしたいとは思いません。普通のコンソールアプリケーションであればprintfでコンソールに出力しますが、Windowsサービスではそれもできません。

ですので、ここでは、簡易版のロギング関数を用意したいと思います。作りとしては、単純に、実行ファイル(EXE)と同じディレクトリに「実行ファイル名(EXE) + ".log"」というファイルを作成し、そこにログを出力するようにします。今回は性能を気にする必要がありませんので、ログの出力のたびにログファイルのオープン、クローズをすることとします。

#define BUFFER_LENGTH 4096

// 実行ファイル(EXE)のパスより、ログのパスを生成します。
// ログファイルの名前は、実行ファイル + ".log"になります。
void initLog(_TCHAR* executablePath)
{
    _tcscpy_s(logFileName, MAX_PATH + 1, executablePath);
    _tcscat_s(logFileName, MAX_PATH + 1, _T(".log"));
}

// ログファイルを開き、ログを出力します。
void log(const _TCHAR *format, ...)
{
    FILE *fp;
    if (_tfopen_s(&fp, logFileName, _T("a")) != 0) {
        return;
    }
    _TCHAR buff[BUFFER_LENGTH];
    va_list args;
    va_start(args, format);
    _vsntprintf_s(buff, BUFFER_LENGTH, _TRUNCATE, format, args);
    _ftprintf_s(fp, buff);
    fclose(fp);
}

使い方は、最初に実行ファイル(EXE)のパス(つまりargv[0])を引数にinitLog関数を呼び出します。initLogでは、実行ファイル名からログファイル名を生成しています。次にlog関数を呼び出します。引数はprintfと同様に、第一引数にフォーマット、第二引数以降に変数を指定します。

main関数に、initLog関数の呼び出しと、メイン関数の開始と終了がわかるようにログを出力するコードを、以下のように追加します。

int _tmain(int argc, _TCHAR* argv[])
{
    initLog(argv[0]);
    log(_T("Main method started\n"));

    log(_T("Main method ended\n"));
    return 0;
}

第5章 サービスメイン関数

Windowsサービスプログラムは、コンソールアプリケーションとして開発されます。ですので、SCMから起動されるとmain関数が呼び出されます。しかしながら、このmain関数は本当の意味でのサービスのメイン関数ではありません。Windowsサービスでは、別にサービスメイン関数と呼ばれるものがサービスの処理を実行します。そして、main関数では、そのサービスメイン関数をSCMに教えるためにStartServiceCtrlDispatcherを呼び出します。具体的には、以下のようにします。

#define SERVICE_NAME _T("test1")

int _tmain(int argc, _TCHAR* argv[])
{
    initLog(argv[0]);
    log(_T("Main method started\n"));

    SERVICE_TABLE_ENTRY dispatchTable[] = {
        {SERVICE_NAME, ServiceMain},
        {NULL, NULL}
    };

    if (!StartServiceCtrlDispatcher(dispatchTable)) {
        log(_T("StartServiceCtrlDispatcher failed. %u\n"), GetLastError());
        return -1;
    }

    log(_T("Main method ended\n"));
    return 0;
}

ここでまずSERVICE_TABLE_ENTRYにサービス名とサービスメイン関数のペアを指定します。SERVICE_TABLE_ENTRYには、複数のサービス名とサービスメイン関数のペアを指定できるようになっています。これは、一つのサービスプログラム(EXE)が複数のサービスを提供することもできるからです。複数のペアを指定できることから、終端を示すためにNULLのペアを最後に指定しなければなりません。今回は一つのサービスしか提供しませんので、サービス名とサービスメイン関数のペアを一つだけ指定します。

    SERVICE_TABLE_ENTRY dispatchTable[] = {
        {SERVICE_NAME, ServiceMain},
        {NULL, NULL}
    };

次に、StartServiceCtrlDispatcher関数の引数にSERVICE_TABLE_ENTRYを指定して呼びます。このStartServiceCtrlDispatcher関数は、エラーがなければサービスが停止するまで処理は戻ってきません。エラーが返ってきた場合は、戻り値に0を返します。エラーの場合、ここではGetLastErrorでエラーコードを取得してログに出力しています。

    if (!StartServiceCtrlDispatcher(dispatchTable)) {
        log(_T("StartServiceCtrlDispatcher failed. %u\n"), GetLastError());
        return -1;
    }

では、本題のサービスメイン関数ですが、以下のようになります。

VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv)
{
    log(_T("Service Main started\n"));
    // サービスステータス構造体の初期化
    SERVICE_STATUS serviceStatus;
    serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    serviceStatus.dwWin32ExitCode = NO_ERROR;
    serviceStatus.dwServiceSpecificExitCode = 0;
    serviceStatus.dwCheckPoint = 1;
    serviceStatus.dwWaitHint = 1000;
    serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;

    // サービスコントロールハンドラの登録
    serviceStatusHandle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, ServiceControlHandlerEx, NULL);
    if (serviceStatusHandle == 0) {
        log(_T("RegisterServiceCtrlHandlerEx failed. %u\n"), GetLastError());
        return;
    }
    
    // サービスの初期化中であることをSCMへ通知
    serviceStatus.dwCurrentState = SERVICE_START_PENDING;
    BOOL ret = SetServiceStatus(serviceStatusHandle, &serviceStatus);
    if (!ret) {
        log(_T("SetServiceStatus failed. %u\n"), GetLastError());
        return;
    }

    // サービスの初期化コードをここに記述
    // (今回は特になし)
    
    // サービスの実行が開始したことをSCMへ通知
    serviceStatus.dwCurrentState = SERVICE_RUNNING;
    serviceStatus.dwCheckPoint = 0;
    serviceStatus.dwWaitHint = 0;
    serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_STOP;
    ret = SetServiceStatus(serviceStatusHandle, &serviceStatus);
    if (!ret) {
        log(_T("SetServiceStatus failed. %u\n"), GetLastError());
        return;
    }

    // サービスの処理本体をここに記述
    while (!stopRequest) {
        log(_T("Running\n"));
        Sleep(1000);
    }
    log(_T("Service Main stopped\n"));
}

処理の流れは以下のようになります。

サービスステータス構造体の初期化

SCMへサービスの状態を通知するために用いるサービスステータス構造体を初期化します。個々の内容の詳細は、後述します。

コントロールハンドラの登録

後述するコントロールハンドラをSCMに登録します。コントロールハンドラは、SCMからのサービス停止依頼等を処理する関数です。

SetServiceStatus関数によるサービス初期化中のSCMへの通知

サービスが初期化処理を実行中であることをサービスステータス構造体とSetServiceStatus関数を用いてSCMへ通知します。ただし、本サンプルでは、特に初期化処理を実行していません。

SetServiceStatus関数によるサービス実行中のSCMへの通知

サービスが処理の実行中状態へ遷移したことを、サービスステータス構造体とSetServiceStatus関数を用いてSCMへ通知します。本サンプルでは、サービスの本体の処理として1秒おきにログにメッセージを出力しています。

第6章 サービスステータスの通知

Windowsサービスは、サービスの起動、停止、一時停止、再開時に時間のかかる処理をすることがよくあります。時間のかかる処理をする場合、その間、何もWindowsに対して状況を伝えないと、いつまでもWindowsはサービスの処理を待つことになり、都合がよくありません。そのため、Windowsサービスはサービスステータス構造体とSetServiceStatus関数を用いてSCMに対して現在の状態を伝える必要があります。

では、まずはサービスステータス構造体(SERVICE_STATUS)の項目を見ていきましょう。

表6.1 サービスステータス構造体(SERVICE_STATUS)の項目

項目名意味
dwServiceTypeサービスのタイプを指定します。Windowsサービスの場合、SERVICE_WIN32_OWN_PROCESSかSERVICE_WIN32_SHARE_PROCESSを指定します。一つの実行ファイル(EXE)内に一つのサービスしかない場合は、SERVICE_WIN32_OWN_PROCESSを指定すれば問題ありません。一つの実行ファイルに複数のサービスが含まれる場合、それぞれのサービスが別のプロセスで動作する必要がある場合はSERVICE_WIN32_OWN_PROCESSを指定し、複数のサービスが同じプロセスを共有できる場合はSERVICE_WIN32_SHARE_PROCESSを指定します。
dwCurrentStateサービスの状態をSCMへ通知します。具体的な内容は後述します。
dwControlsAcceptedコントロールハンドラがどのコントロールリクエストを受け入れられるかを指定します。例えば、初期化中は一時停止(PAUSE)や再開(CONTINUE)を受け入れられないなどを指定します。
dwWin32ExitCodeサービス内でエラーが発生した場合に、通知する際に用います。エラーがない場合はNO_ERRORを指定します。サービス固有のエラーが発生した場合は、ERROR_SERVICE_SPECIFIC_ERRORを指定し、dwServiceSpecificExitCodeにサービス固有のエラーコードを指定します。
dwServiceSpecificExitCodedwWin32ExitCodeにERROR_SERVICE_SPECIFIC_ERRORを指定した場合に、サービスの固有のエラーコードを指定します。
dwCheckPoint例えば、サービスの初期化に時間がかかり、複数回SetServiceStatus関数を呼び出す場合に、処理が進捗していることをSCMに通知するために用います。サービス側はSetServiceStatus関数を呼び出すたびにこの値を+1しなければなりません。
dwWaitHintサービスの初期化処理や停止処理にかかる予想時間をミリ秒で指定します。サービスは、ここで指定した時間が経過する前にSetServiceStatus関数で状態を更新する必要があります。SCMは、ここで指定した時間を経過してもサービスからの状態の更新がない場合、サービス内でエラーが発生したと判断してサービスを停止します。

dwCurrentStateは、サービスの4つの制御要求ごとに、以下のように指定します。

サービスの開始

サービスの開始を実行する前にSERVICE_START_PENDINGを指定し、サービスの開始が完了した際にSERVICE_RUNNINGを指定します。

サービスの停止

サービスの停止を実行する前にSERVICE_STOP_PENDINGを指定し、サービスの停止が完了した際にSERVICE_STOPPEDを指定します。

サービスの一時停止

サービスの一時停止を実行する前にSERVICE_PAUSE_PENDINGを指定し、サービスの一時停止が完了した際にSERVICE_PAUSEDを指定します。

サービスの再開

サービスの再開を実行する前にSERVICE_CONTINUE_PENDINGを指定し、サービスの再開が完了した際にSERVICE_RUNNINGを指定します。

第7章 コントロールハンドラ

サービスメイン関数がサービス自体の処理に専念している間も、コントロールパネルのサービスやscコマンドからサービスの停止、一時停止、再開等のリクエストとが送られてくることがあります。これらのリクエストを別スレッドで処理するのがコントロールハンドラです。

コントロールハンドラは、サービスメイン関数の開始直後にRegisterServiceCtrlHandlerEx関数を用いて登録します。

コントロールハンドラは以下の通りになります。

DWORD WINAPI ServiceControlHandlerEx(DWORD control, DWORD eventType, LPVOID eventData, LPVOID context )
{
    // サービスステータス構造体の初期化
    SERVICE_STATUS serviceStatus;
    serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    serviceStatus.dwWin32ExitCode = NO_ERROR;
    serviceStatus.dwServiceSpecificExitCode = 0;
    serviceStatus.dwCheckPoint = 1;
    serviceStatus.dwWaitHint = 3000;
    serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
    serviceStatus.dwCurrentState = 0;

    BOOL ret = TRUE;
    DWORD returnCode;
    switch (control) {
    case SERVICE_CONTROL_INTERROGATE:
        // サービスの状態の問い合わせ
        log(_T("SERVICE_CONTROL_INTERROGATE\n"));
        serviceStatus.dwCurrentState = SERVICE_RUNNING;
        serviceStatus.dwCheckPoint = 0;
        serviceStatus.dwWaitHint = 0;
        ret = SetServiceStatus (serviceStatusHandle, &serviceStatus);
        returnCode = NO_ERROR;
        break;
    case SERVICE_CONTROL_STOP:
        // サービスの停止
        log(_T("SERVICE_CONTROL_STOP\n"));

        // SERVICE_STOP_PENDINGをSCMへ通知
        serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
        ret = SetServiceStatus (serviceStatusHandle, &serviceStatus);
        if (!ret) {
            log(_T("SetServiceStatus failed. %u\n"), GetLastError());
            returnCode = ERROR_SET_SERVICE_STATUS_FAILED;
            break;
        }
        // 別スレッドで動作しているサービスメイン関数に終了することを伝え、3秒間待機
        stopRequest = TRUE;
        Sleep(3000);
        
        // SERVICE_STOPPEDをSCMへ通知
        serviceStatus.dwCurrentState = SERVICE_STOPPED;
        serviceStatus.dwCheckPoint = 0;
        serviceStatus.dwWaitHint = 0;
        ret = SetServiceStatus (serviceStatusHandle, &serviceStatus);
        if (!ret) {
            log(_T("SetServiceStatus failed. %u\n"), GetLastError());
            returnCode = ERROR_SET_SERVICE_STATUS_FAILED;
            break;
        }
        returnCode = NO_ERROR;
        break;
    default:
        returnCode = ERROR_CALL_NOT_IMPLEMENTED;
    }
    return returnCode;
}

コントロールハンドラの第一引数にどの制御リクエストかが制御コードとして渡されます。制御コードには、以下の5つの内のどれかが指定されます。

表7.1 制御コード

コード名意味
SERVICE_CONTROL_STOPサービスの停止要求です。
SERVICE_CONTROL_PAUSEサービスの一時停止要求です。
SERVICE_CONTROL_CONTINUEサービスの再開要求です。
SERVICE_CONTROL_INTERROGATEサービスの状態の更新要求です。サービスはSetServiceStatus関数を用いて、サービスの状態を伝える必要があります。
SERVICE_CONTROL_SHUTDOWNOSがシャットダウンする前に、サービスに対してクリーンアップ処理の実行を依頼する要求です。

サービスの開始は、コントロールハンドラではなく、main関数およびサービスメイン関数で処理されますので、ここには含まれません。

それ以外の停止、一時停止、再開に加えて、サービスの状態通知とシャットダウン要求があることになります。

このサンプルでは、シンプルにするために一時停止と再開をサポートしないこととします。また、シャットダウンも含めていませんので、結果として、停止(SERVICE_CONTROL_STOP)と状態通知(SERVICE_CONTROL_INTERROGATE)のみを処理すればよいこととなります。

まず、サービスの状態通知ですが、今回のサンプルは単純に実行中か停止しか状態を持たない上、停止した場合はコントロールハンドラが呼び出されることがないことから、常に実行中(SERVICE_RUNNING)をSCMへ伝えることとします。

DWORD WINAPI ServiceControlHandlerEx(DWORD control, DWORD eventType, LPVOID eventData, LPVOID context )
{
    ... 
    switch (control) {
    case SERVICE_CONTROL_INTERROGATE:
        // サービスの状態の問い合わせ
        log(_T("SERVICE_CONTROL_INTERROGATE\n"));
        serviceStatus.dwCurrentState = SERVICE_RUNNING;
        serviceStatus.dwCheckPoint = 0;
        serviceStatus.dwWaitHint = 0;
        ret = SetServiceStatus (serviceStatusHandle, &serviceStatus);
        returnCode = NO_ERROR;
        break;
    ...
}

次に、サービス停止ですが、こちらはまずSERVICE_STOP_PENDINGをSCMへ通知後、サービスメイン関数に終了を伝え、3秒間待機後、SERVICE_STOPPEDをSCMへ通知しています。

注意として、ここで用いているサービスメイン関数の停止方法は、相当、適当な方法です。本来ならば、スレッド間連携するためイベント等を用いる必要があります。

DWORD WINAPI ServiceControlHandlerEx(DWORD control, DWORD eventType, LPVOID eventData, LPVOID context )
{
   ...
    switch (control) {
        ...
    case SERVICE_CONTROL_STOP:
        // サービスの停止
        log(_T("SERVICE_CONTROL_STOP\n"));
        
        // SERVICE_STOP_PENDINGをSCMへ通知
        serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
        ret = SetServiceStatus (serviceStatusHandle, &serviceStatus);
        if (!ret) {
            log(_T("SetServiceStatus failed. %u\n"), GetLastError());
            returnCode = ERROR_SET_SERVICE_STATUS_FAILED;
            break;
        }
        
        // 別スレッドで動作しているサービスメイン関数に終了することを伝え、3秒間待機
        stopRequest = TRUE;
        Sleep(3000);
        
        // SERVICE_STOPPEDをSCMへ通知
        serviceStatus.dwCurrentState = SERVICE_STOPPED;
        serviceStatus.dwCheckPoint = 0;
        serviceStatus.dwWaitHint = 0;
        ret = SetServiceStatus (serviceStatusHandle, &serviceStatus);
        if (!ret) {
            log(_T("SetServiceStatus failed. %u\n"), GetLastError());
            returnCode = ERROR_SET_SERVICE_STATUS_FAILED;
            break;
        }
        returnCode = NO_ERROR;
        break;
    ...
}

これでWindowsサービスが完成したので、ビルドします。

第8章 scコマンドを用いたサービスの登録、削除

WindowsサービスをSCMの管理するデータベースに登録します。ここでは、scコマンドを用いた方法で行います。

まず、Windowsサービスを登録するには、以下のようにscコマンドを実行します。

> sc create test1 binPath= "(実行ファイルのパス)" DisplayName= "test1"

ここで、binPathには実行ファイルへの完全パス(EXEファイルのパス)を指定します。また、ここではサービス名として"test1"を指定しています。

注意として、scコマンドのオプションは等号(=)の後に(なぜか)空白が必要です。つまり、binPathの場合、「binPath="(実行ファイルのパス"」とすると登録できません。

サービスの登録が完了したら、以下のようにsc startでサービスを開始します。(net start test1でもかまいません)

> sc start test1

サービスの停止は、sc stopを用います。(net stop test1でも同じです)

> sc stop test1

サービスの登録解除は、sc deleteを用います。

> sc delete test1

なお、サービスの開始、停止は、コントロールパネルのサービスからも実行できます。

実行ファイル(EXE)と同じフォルダにログファイルが作成されているはずです。以下は、私が登録、開始、停止、削除の一連の手順を実行した場合のログの内容です。

Main method started
Service Main started
Running
Running
SERVICE_CONTROL_INTERROGATE
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
Running
SERVICE_CONTROL_STOP
Service Main stopped
Main method ended

CX5 SOFTWARE, 2010