Windows アプリのサービス化

今更ながら、Windows アプリをサービス化して実行する必要に迫られたので、手順を記録してみる。

サービスはメインスレッドとサービススレッドの最低2つが必要となるため、登録/状態監視用の関数と実際のサービスを提供する関数を作成する。前者を SvcMain, 後者を SvcMainLoop と書くことにするとと、起動処理は以下のようになる。

#include <Windows.h>
#include <cstdio>

SERVICE_STATUS gSvcStatus;
SERVICE_STATUS_HANDLE gSvcStatusHandle;
HANDLE ghSvcStopEvent = NULL;
DWORD gdwTimeOut = 5;
TCHAR gsDrive[MAX_PATH], gsDir[MAX_PATH], gsFilename[MAX_PATH], gsExt[MAX_PATH];
DWORD WINAPI SvcCtrlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
VOID SvcMain(DWORD, LPTSTR*);

VOID ReportSvcStatus(DWORD, DWORD, DWORD);
VOID SvcInit(DWORD, LPTSTR*);
VOID SvcMainLoop(DWORD, LPTSTR*);
VOID SvcEnd(DWORD, LPTSTR*);

#define SVCNAME (L"MyService")

int __cdecl _tmain(int argc, char **argv) {
    // サービス登録モード
    TCHAR buf[MAX_PATH];
    if (GetModuleFileNameW(NULL, buf, _countof(buf)) != 0LL) {
        _tsplitpath(buf, gsDrive, gsDir, gsFilename, gsExt);
        _tcscat(gsDrive, gsDir);
    }

    SERVICE_TABLE_ENTRY DispatchTable[] = {
        { (LPTSTR)SVCNAME, (LPSERVICE_MAIN_FUNCTION)SvcMain},
        { NULL, NULL }
    };
    BOOL dwRet = StartServiceCtrlDispatcherW(DispatchTable);
    printf("Registration:%d\n", dwRet);
    return (dwRet == 0) ? 1 : 0;
}

// サービス状態変化時に呼び出される関数
DWORD WINAPI SvcCtrlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) {
    switch (dwControl) {
    case SERVICE_CONTROL_STOP:
        ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
        // サービス終了を通知
        SetEvent(ghSvcStopEvent);
        ReportSvcStatus(gSvcStatus.dwCurrentState, NO_ERROR, 0);

    default:;
        // Nothing to do
    }
    return NO_ERROR;
}

// サービス登録及びメインループ
VOID SvcMain(DWORD dwArgc, LPTSTR* lpszArgv) {
    gSvcStatusHandle = RegisterServiceCtrlHandlerEx(SVCNAME, SvcCtrlHandler, NULL);
    if (gSvcStatusHandle == NULL) {
        return;
    }

    // 独自プロセス
    gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    gSvcStatus.dwServiceSpecificExitCode = 0;

    ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);

    SvcInit(dwArgc, lpszArgv);

    SvcMainLoop(dwArgc, lpszArgv);
}

VOID SvcInit(DWORD dwArgc, LPTSTR* lpszArgv) {
    ghSvcStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (ghSvcStopEvent == NULL) {
        ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
        return;
    }

    ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
}

void myMain() {
    // ...
}

// メインループ
VOID SvcMainLoop(DWORD dwArgc, LPTSTR* lpszArgv) {
    while (1) {
        // 周期的に実行する関数
        myMain();
        // 第2引数を INFINITE にすると終了イベント発生まで戻らない
        DWORD r = WaitForSingleObject(ghSvcStopEvent, gdwTimeOut * 60 * 1000);
        if (r != WAIT_TIMEOUT) {
            // 終了イベント発生時はループを抜ける
            break;
        }
    }
    SvcEnd(dwArgc, lpszArgv);
}

// 終了処理: 終了状態を宣言する
VOID SvcEnd(DWORD dwArgc, LPTSTR* lpszArgv) {
    ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
}

// サービス状態を通知する関数
VOID ReportSvcStatus(DWORD dwCurrentState, DWORD dwExitStateCode, DWORD dwWaitHint) {
    gSvcStatus.dwCurrentState = dwCurrentState;
    gSvcStatus.dwWin32ExitCode = dwExitStateCode;
    gSvcStatus.dwWaitHint = dwWaitHint;

    gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP
        | SERVICE_ACCEPT_PAUSE_CONTINUE;
    
    if (dwCurrentState == SERVICE_RUNNING || dwCurrentState == SERVICE_STOPPED) {
        gSvcStatus.dwCheckPoint = 0;
    }
    else {
        gSvcStatus.dwCheckPoint++;
    }

    SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
}

サービスの登録/登録解除処理も必要になるが、sc コマンドで実行できる。

# 登録
sc create "MyService" binPath="C:\MyService\myService.exe"

# 登録解除
sc delete "MyService"

GetModuleFileNameW() は実行ファイルの絶対パスを取得するのに使用する。(実行ファイルと同じフォルダに .ini ファイル等を置きたかったため)相対パスだとサービスを実行できないようだ。

あとは「サービス」ユーティリティを上げて「開始」ボタンを押すとサービスが開始される。「停止」ボタンを押すとサービスが終了する。

ProgramFiles 以下にないと登録できないかと思ったが、任意のフォルダ上の実行ファイルを登録できるようだ。

投稿者について
みのしす

小さいときは科学者になろうとしたのに、その時にたまたま身に着けたプログラミングで未だに飯を食っているしがないおじさんです。(年齢的にはもうすぐおじいさん)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です