LoginSignup
6
5

More than 1 year has passed since last update.

[C++] C++でサービスをつくる(正式版)

Last updated at Posted at 2021-04-16

もくじ

サービス関連記事

やりたいこと

以前の記事で、Microsoftのサンプルページをもとに、C++でWindowsサービスを作った。(MSページは下記参照)

C++でのサービスに関するMSページ
https://docs.microsoft.com/en-us/windows/win32/services/services
C++でのサービスのサンプルプログラム
https://docs.microsoft.com/en-us/windows/win32/services/svc-cpp

そこでは、ほぼMSのサンプルのコードのまま、ビルドがうまく通らなかった部分だけ直してサンプルコードとして載せていたが、簡単なサービスのサンプルとして勉強をする上では不要な部分がかなりあってわかりづらかったので、自分がいると思った部分だけ残して間引いた。

その時実験で作成したコードと、学んだサービスの作り方やしくみや構成、処理の流れをメモしておく。

スレッド構成と処理の流れ

スレッド構成

サービスは、MSのサンプルによると、最低2つのスレッドでできている。

  • メインスレッド
    サービスのプロセスが起動したときに動いているスレッド。
  • サービススレッド
    サービス固有の処理を行うためのスレッド。

処理の流れ

サービスの起動時、メインスレッドとサービススレッドで、下記のような処理をしている。

  • メインスレッドで行う処理

    • StartServiceCtrlDispatcher()で、サービス固有の処理を行うための関数を登録。
      (その関数は、サービススレッドの中で動作する)
    • サービススレッド側で登録した、SCMからくるサービスコントロール(SC)の処理を行うための関数がStartServiceCtrlDispatcher()の中でサービス終了通知がSCMからくるまで動き続ける。
  • サービススレッドで行う処理

    • スレッド起動後すぐにRegisterServiceCtrlHandlerEx()を呼び、SCMからくるサービスコントロール(SC)の処理を行うための関数を登録する。
      (その関数は、メインスレッドの中で動作する)
    • その後、そのサービス固有の処理を、サービス終了まで行う。
  • スレッドによらない処理

    • サービスは自分の状態をSetServiceStatus()を使ってServiceControlManagerに報告する必要がある。
      (報告するタイミングは下の図とサンプルコード参照)
      • 初期状態(SetServiceStatus()を呼ぶ前):状態なし(0)
      • 開始中:SERVICE_START_PENDING(2)
      • 実行中:SERVICE_RUNNING(4)
      • 終了待機中:SERVICE_STOP_PENDING(3)
      • 停止中:SERVICE_STOPPED(1)
      • ほかにもいくつか状態がある(一時停止中等)

スレッド構成と処理の流れのイメージ図

文字で書くとさっぱりわからないが、だいたい下図のようなイメージ。
image.png

※サービススレッドは「一般的にはそうするだろう」という意味で、初期化処理、固有処理、終了処理の3つに分けているが、別にそうする義務はない。初期化や終了処理が必要なければ固有処理だけでもいいし、究極サービススレッドではRegisterServiceCtrlHandlerEx()でSCを受ける関数だけ登録すれば、別に何もしなくてもいい。
サービスコントロールを受ける関数のほうでやりたいことをやってしまうというのもナシではない。
(ただし、サービスコントロールを処理する関数は30秒以内に処理を終えないといけないので、時間のかかる処理を行うことは不可。)

サンプルコード

上の図中に出てくる関数名と下のサンプルコードに出てくる関数名は一致させているので、コードを実際にビルドして動かしながら上の図を見ていると、何となく動きがわかってくると思う。

置き場所
https://github.com/tera1707/Cpp/tree/master/serviceJikken1

serviceJiken1.cpp
#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include <string>
#include "serviceJikken1.h"

#pragma comment(lib, "advapi32.lib")

#define SVCNAME TEXT("SvcName")

SERVICE_STATUS          gSvcStatus;
SERVICE_STATUS_HANDLE   gSvcStatusHandle;
HANDLE                  ghSvcStopEvent = NULL;

DWORD WINAPI SvcCtrlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
VOID WINAPI SvcMain(DWORD, LPTSTR*);

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

// 本プロセスのエントリポイント(メインスレッド)
// ServiceControlのループは、このスレッドで行われる
int __cdecl _tmain(int argc, TCHAR* argv[])
{
    OutputLogToCChokka(L"----------------------------Start------------------------------");
#ifdef _DEBUG
    // デバッグ時にアタッチするための待ち
    //Sleep(10000);
    OutputLogToCChokka(L"-----------------Start(debug wait finished..)------------------");
#endif
    // サービスを登録するためのテーブルを作成(複数のサービスを1プロセスで登録できるようだが今回は1個だけ)
    SERVICE_TABLE_ENTRY DispatchTable[] =
    {
        { (LPTSTR)SVCNAME, (LPSERVICE_MAIN_FUNCTION)SvcMain },
        { NULL, NULL }
    };

    // サービスがstopしたときにこの関数が制御を返す。
    // このプロセスは、この関数から戻ったら単純に終了すればいい。
    if (!StartServiceCtrlDispatcher(DispatchTable))
    {
        OutputLogToCChokka((LPTSTR)(TEXT("StartServiceCtrlDispatcher")));
    }
    return 0;
}

// ServiceControlがSCMから送られてくるたびに呼ばれる
DWORD WINAPI SvcCtrlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    OutputLogToCChokka(SvcCtrlTbl[dwControl]);

    switch (dwControl)
    {
    case SERVICE_CONTROL_STOP:
        ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);

        // サービス停止シグナルを送る
        SetEvent(ghSvcStopEvent);
        // 停止を報告
        ReportSvcStatus(gSvcStatus.dwCurrentState, NO_ERROR, 0);

        return NO_ERROR;

    case SERVICE_CONTROL_SESSIONCHANGE:
    {
        // gSvcStatus.dwControlsAccepted に SERVICE_ACCEPT_SESSIONCHANGE をsetすると来るSC。
        OutputLogToCChokka(L" " + SessionScTbl[dwEventType]);

        switch (dwEventType)
        {
            case WTS_SESSION_LOGON:

                break;
        }

        break;
    }

    case SERVICE_CONTROL_INTERROGATE:
        return NO_ERROR;

    case SERVICE_CONTROL_POWEREVENT:
        // gSvcStatus.dwControlsAccepted に SERVICE_ACCEPT_POWEREVENT をsetすると来るSC。
        // これが来たときは、GetSystemPowerStatus()を使って情報をとれば、変化した値を取れるとのこと
        // https://docs.microsoft.com/ja-jp/windows/win32/power/pbt-apmpowerstatuschange
        // もしくは、下記のようなPBT_**でも同じような情報が取れる。

        OutputLogToCChokka(L" " + std::to_wstring(dwEventType));

        switch (dwEventType)
        {
            case PBT_POWERSETTINGCHANGE:
            {
                auto data = reinterpret_cast<POWERBROADCAST_SETTING*>(lpEventData);

                OutputLogToCChokka(L"  " + std::to_wstring(data->PowerSetting.Data1));

                if (data->PowerSetting == GUID_BATTERY_PERCENTAGE_REMAINING)
                {
                    auto bat = reinterpret_cast<DWORD*>(data->Data);
                    auto log = std::wstring(L"   * battery remain is " + std::to_wstring(*bat));
                    OutputLogToCChokka(log.c_str());
                }
                else if (data->PowerSetting == GUID_ACDC_POWER_SOURCE)
                {
                    auto src = reinterpret_cast<DWORD*>(data->Data);
                    auto log = std::wstring(L"   * power source is " + PowSrcTbl[*src]);
                    OutputLogToCChokka(log.c_str());
                }
                break;
            }
        }
        break;

    default:
        break;
    }

    return NO_ERROR;
}

// サービスの主たる処理を行うためのスレッド(メインスレッドとは別のスレッド)
VOID WINAPI SvcMain(DWORD dwArgc, LPTSTR* lpszArgv)
{
    // サービスコントロールを受けれ宇関数を登録(その関数は、メインスレッドで動く)
    gSvcStatusHandle = RegisterServiceCtrlHandlerEx(SVCNAME, SvcCtrlHandler, NULL);

    // 失敗したらサービスが成り立たないので終了させる
    if (!gSvcStatusHandle)
    {
        OutputLogToCChokka((LPTSTR)(TEXT("RegisterServiceCtrlHandler fail...")));
        return;
    }

    // 以下で、デフォルトでは受けないServiceControlを登録する
    // ここではGUID_BATTERY_PERCENTAGE_REMAINING(バッテリの残量変化)を受けられるようにする

    // バッテリーの残量の変化のServiceControlが来るようにする
    auto ret = RegisterPowerSettingNotification(gSvcStatusHandle, &GUID_BATTERY_PERCENTAGE_REMAINING, DEVICE_NOTIFY_SERVICE_HANDLE);
    if (!ret)
        return;

    // 電源の変化(AC⇒DC)
    ret = RegisterPowerSettingNotification(gSvcStatusHandle, &GUID_ACDC_POWER_SOURCE, DEVICE_NOTIFY_SERVICE_HANDLE);
    if (!ret)
        return;

    // ----------------------

    // どういうサービスにするか設定
    gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    gSvcStatus.dwServiceSpecificExitCode = 0;

    // SCMにまずは「起動中」であることを報告
    ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);

    // サービスに必要な各種初期化を行う
    SvcInit(dwArgc, lpszArgv);

    // サービスとしての主たる処理を行う
    SvcMainLoop(dwArgc, lpszArgv);
}

VOID SvcInit(DWORD dwArgc, LPTSTR* lpszArgv)
{
    // ここで、各種変数定義や初期化を行う。
    // サービスのステータスが「SERVICE_START_PENDING」である間は、
    // 定期的にReportSvcStatus()を呼ぶこと。(別に呼ばないまま放置してもエラーになったりはしないが
    // (もし初期化処理でエラーになったりしたときは、そこでSERVICE_STOPPEDを載せてReportSvcStatus()を呼ぶこと)

    // DEBUG用のPending処理()時間のかかる初期化処理を想定
    int ctr = 0;
    for (int i = 0; i < 5; i++)
    {
        OutputLogToCChokka(L"Pending.." + std::to_wstring(gSvcStatus.dwCheckPoint));
        ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 1000 * 2);
        Sleep(1000);
    }

    // サービス終了時に発火させるイベント(SvcMainLoopのループを抜けさせるためのイベント)
    // stopのServiceContorlを受けたらこういったイベント等を使ってループを抜け、処理を終了させる
    ghSvcStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

    if (ghSvcStopEvent == NULL)
    {
        ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
        return;
    }

    // 初期化が終了したので、SCMに「サービスの開始」を報告する
    ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
}

VOID SvcMainLoop(DWORD dwArgc, LPTSTR* lpszArgv)
{
    while (1)
    {
        // やりたい処理をここでサービス終了までやり続ける
        WaitForSingleObject(ghSvcStopEvent, INFINITE);
        return;
    }
}

VOID SvcEnd(DWORD dwArgc, LPTSTR* lpszArgv)
{
    // 必要な後処理をここで行う

    // サービス終了したら、SERVICE_STOP_PENDINGからSTOPPEDにする
    ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
}

// 現在のサービスステータスをセットして、SCMに報告する
//   dwCurrentState - The current state (see SERVICE_STATUS)
//   dwWin32ExitCode - The system error code
//   dwWaitHint - Estimated time for pending operation in milliseconds
VOID ReportSvcStatus(DWORD dwCurrentState,
    DWORD dwWin32ExitCode,
    DWORD dwWaitHint)
{
    static DWORD dwCheckPoint = 1;

    auto msg = CsTbl[dwCurrentState] + std::wstring(L": dwCheckPoint = ") + std::to_wstring(dwCheckPoint) + L" dwWaitHint = " + std::to_wstring(dwWaitHint);
    OutputLogToCChokka(msg);

    // Fill in the SERVICE_STATUS structure.
    gSvcStatus.dwCurrentState = dwCurrentState;
    gSvcStatus.dwWin32ExitCode = dwWin32ExitCode;
    gSvcStatus.dwWaitHint = dwWaitHint;

    // Pending中はサービス停止や中断できないようにし、SESSIONCHANGEやPOWER系のSCも受けないようにする
    if (dwCurrentState == SERVICE_START_PENDING)
        gSvcStatus.dwControlsAccepted = 0;
    else gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SESSIONCHANGE | SERVICE_ACCEPT_POWEREVENT;

    if ((dwCurrentState == SERVICE_RUNNING) ||
        (dwCurrentState == SERVICE_STOPPED))
        gSvcStatus.dwCheckPoint = 0;
    else gSvcStatus.dwCheckPoint = dwCheckPoint++;

    // Report the status of the service to the SCM.
    SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
}
serviceJikken1.h
#pragma once

// DEBUG
#include <iostream>
#include <time.h>
#include <thread>   // std::this_thread::get_id()を使うのに必要
#include <fstream>  // std::wofstreamを使うのに必要

const std::wstring CsTbl[] =
{
    L"INIT",
    L"SERVICE_STOPPED",
    L"SERVICE_START_PENDING",
    L"SERVICE_STOP_PENDING",
    L"SERVICE_RUNNING",
    L"SERVICE_CONTINUE_PENDING",
    L"SERVICE_PAUSE_PENDING",
    L"SERVICE_PAUSED",
};
const std::wstring PowSrcTbl[] =
{
    L"PoAc",
    L"PoDc",
    L"PoHot",
};
const std::wstring SvcCtrlTbl[] =
{
    L"UNDEFINED",
    L"SERVICE_CONTROL_STOP",
    L"SERVICE_CONTROL_PAUSE",
    L"SERVICE_CONTROL_CONTINUE",
    L"SERVICE_CONTROL_INTERROGATE",
    L"SERVICE_CONTROL_SHUTDOWN",
    L"SERVICE_CONTROL_PARAMCHANGE",
    L"SERVICE_CONTROL_NETBINDADD",
    L"SERVICE_CONTROL_NETBINDREMOVE",
    L"SERVICE_CONTROL_NETBINDENABLE",
    L"SERVICE_CONTROL_NETBINDDISABLE",
    L"SERVICE_CONTROL_DEVICEEVENT",
    L"SERVICE_CONTROL_HARDWAREPROFILECHANGE",
    L"SERVICE_CONTROL_POWEREVENT",
    L"SERVICE_CONTROL_SESSIONCHANGE",
    L"SERVICE_CONTROL_PRESHUTDOWN",
    L"SERVICE_CONTROL_TIMECHANGE",
    L"SERVICE_CONTROL_USER_LOGOFF",
    L"SERVICE_CONTROL_TRIGGEREVENT",
    L"reserved for internal use",
    L"reserved for internal use",
    L"SERVICE_CONTROL_LOWRESOURCES",
    L"SERVICE_CONTROL_SYSTEMLOWRESOURCES",
};
const std::wstring SessionScTbl[] =
{
    L"UNDEFINED",
    L"WTS_CONSOLE_CONNECT",
    L"WTS_CONSOLE_DISCONNECT",
    L"WTS_REMOTE_CONNECT",
    L"WTS_REMOTE_DISCONNECT",
    L"WTS_SESSION_LOGON",
    L"WTS_SESSION_LOGOFF",
    L"WTS_SESSION_LOCK",
    L"WTS_SESSION_UNLOCK",
    L"WTS_SESSION_REMOTE_CONTROL",
    L"WTS_SESSION_CREATE",
    L"WTS_SESSION_TERMINATE",
};

// ログをCドライブ直下に残すDEBUG用関数
void OutputLogToCChokka(std::wstring txt)
{
    //FILE* fp = NULL;
    auto t = time(nullptr);
    auto tmv = tm();
    auto error = localtime_s(&tmv, &t); // ローカル時間(タイムゾーンに合わせた時間)を取得

    WCHAR buf[256] = { 0 };
    wcsftime(buf, 256, L"%Y/%m/%d %H:%M:%S ", &tmv);

    // 現在のスレッドIDを出力
    auto thId = std::this_thread::get_id();

    // ログ出力
    std::wstring logtxt = buf + txt;

    // ファイルを開く(なければ作成)
    // C直下のファイルに書くにはexe実行時に管理者権限にする必要アリ
    std::wofstream ofs(L"C:\\mylog.log", std::ios::app);
    if (!ofs)
    {
        return;
    }
    // 現在時刻とスレッドIDを付けたログをファイルに書き込み
    ofs << thId << L"  " << logtxt.c_str() << std::endl;
    std::wcout << thId << L"  " << logtxt.c_str() << std::endl;
    // ファイル閉じる
    ofs.close();
}

※SvcMainの最初に書いてる登録系の処理を、SvcInit()にもっていった方がわかりやすいかも。

実験していたときのメモ

以下は実験しながらメモしていた内容をそのまま載せてるので順不同かつまとめていないので見づらいが、消すのがもったいないので残す。

・pendingにしている間は、サービスコントロール(SC)は来ないのか?
 →RegisterPowerSettingNotificationを呼ぶとくるようになる下記のSCは、Pending中でも来る!!
  ・GUID_BATTERY_PERCENTAGE_REMAINING
  ・GUID_ACDC_POWER_SOURCE

・SERVICE_CONTROL_SESSIONCHANGEはくるか?
  →status.dwCurrentState はPendingでも、
   dwControlsAcceptedにSERVICE_ACCEPT_SESSIONCHANGEをセットしていたら来る。
  
・つまり、SCがくるか来ないかは、status.dwCurrentStateではなくstatus.dwControlsAcceptedに入れてる値に左右される。
SERVICE_ACCEPT_SESSIONCHANGEをsetしてたらSERVICE_CONTROL_SESSIONCHANGEがくるし、
SERVICE_ACCEPT_POWEREVENTをsetしてたらSERVICE_CONTROL_POWEREVENTが来る。

・SCこないとして、その間のログインSCは取りこぼしてしまうのか?
 →dwControlsAcceptedをsetしてない間のSCは取りこぼす。

・最初dwCurrentState になにもsetしない、pendingしてる間もなにもsetしない、
 RUNNINGになるときにdwCurrentState にSERVICE_ACCEPT_SESSIONCHANGEにしても、
 あとからほしいもの来てくれないか??
 →きてくれない。
  dwCurrentState をセットした後から、来るようになる。

・サービス=自動にしたときに再起動したら、
 ---ControlAcceptedにSESSION_CHANGEを入れて試す---
 ・即RUNNINGでCONNECTでるか?→でた。
 ・1秒ほどPendingで同上→でた
 ・10秒ほどPendingで同上→でた
 ・70秒ほどPendingで同上→でた(Pendingの12回目で出てた。)
 ---ControlAcceptedに最初なにも入れず、RUNNINGになるときにSESSION_CHANGEを入れて試す---
 ・70秒ほどPendingで同上→でない。RUNNINGになったあとも出ない。
  =PendingにしたらSCを遅らせることができるわけではない。ControlAcceptedに何も入れないことで
   SC来ないようにしたら、その間に来たSCは取りこぼす。

・gSvcStatus.dwControlsAcceptedに何を設定するかで、くるSCが変わる。
 例えばSERVICE_ACCEPT_SESSIONCHANGEをセットしとかないとSessionCahngeedはこない。

・Pendingを長時間おこなったからといって、Widowsの起動、ログインが時間かかるように
 なってしまうわけではない。また、別に後続のサービスの開始を待たせるわけではない。
 (Pendingしている間も、他のサービス開始したりしてる)

・今、RegisterPowerSettingNotificationを呼んで、PBT_POWERSETTINGCHANGEが来てくれるように
しているが、dwControlsAcceptedにSERVICE_ACCEPT_POWEREVENTをset
しておいたら来てくれるようになるSC「SERVICE_CONTROL_POWEREVENT」を受けたときに
GetSystemPowerStatus()を実行してあげたら、PBT_POWERSETTINGCHANGEで取れるのと同じ情報とれそう。
(なんでわざわざRegisterPowerSettingNotification()なんてものが存在するのか??)

image.png
ここのどのボタンが押せるかは、status.dwControlsAcceptedになにをsetしてSetSErviceStatusしたか、できまる。(SERVICE_ACCEPT_STOPをsetしてたら、停止ボタン押せる、等。)

・Status.dwCheckPointは、とりあえずSetServiceStatusをPendingで呼ぶたびに++してればよいっぽい。
 また、dwWaitHintには「あと何msでPending終わるのか」の実装的な申告値をいれておき、
 実装者はその時間内にPendingを終わらす必要がある。で、もしその時間内に終わらんかったら、
 もう一度申告値入れて、カウント++して、SetServiceStatusを呼ぶ、ということを
 Pending終わってRUNNNIGになるまで繰り返すっべきぽい。

・ただ、申告値どおりにPending終わらなかった、を何回繰り返しても、別になにも起こらないっぽい。
 (申告値を0にして、5秒おきにSetServiceStatusを繰り返し呼ぶ、ということを何回繰り返しても、
 強制的にプロセス終了、とかにはならなかった。)

・試した限り、サービス起動後1秒おきに600回(10分)、PendingでSetServiceStatusし続けても、
 サービス強制終了にはならず、その後RUNNINGでSetServiceStatusしたら普通に動いていた。

・またサービス起動後SetServiceStatusをせずに3分以上放置していても、強制的に終了したりは
 しなかった。

・SCを処理するハンドラは、30秒以内に制御を返さないといけないらしい。
 http://eternalwindows.jp/windevelop/service/service03.html

・dwControlsAcceptedにSERVICE_ACCEPT_POWEREVENTをsetしていなくても、
 RegisterPowerSettingNotification()をGUID_ACDC_POWER_SOURCEを入れて呼んでたら、
 SERVICE_CONTROL_POWEREVENTのうちのPBT_POWERSETTINGCHANGEのPOWERBROADCAST_SETTINGの
 data->PowerSetting == GUID_ACDC_POWER_SOURCEが乗ったヤツは来る。

・逆にRegisterPowerSettingNotification()を呼んでないと、dwControlsAcceptedに
 SERVICE_ACCEPT_POWEREVENTをsetしていてもSERVICE_CONTROL_POWEREVENTの
 PBT_POWERSETTINGCHANGEはこない。(SERVICE_CONTROL_POWEREVENTのほかのPBT_は来る)

参考

EternalWindow サービス
欲しい情報はすべてここに書いてある。
http://eternalwindows.jp/windevelop/service/service00.html

C++でのサービスに関するMSページ
https://docs.microsoft.com/en-us/windows/win32/services/services
C++でのサービスのサンプルプログラム
https://docs.microsoft.com/en-us/windows/win32/services/svc-cpp

※書籍
インサイドWindows(第六版)の348ページ辺り
サービス起動後のに何をしたらいいか、の流れは主にここを見て調べた。(図もそのあたりをベースに書いた)が、表現がなんか難しくて、なかなかついていけない....

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5