はじめに
Windowsには見えないですがたくさんのプログラムがバックグラウンドで動作しています。そのバックグラウンドで動作しているプログラムの代表が Windowsサービスプログラム です。
ですが、意外とサービスプログラミングについての資料は少なくて、いざ作ってみようとすると結構苦戦します。
「Win32は死なズ。」今回はWindowsサービスの作り方を書いてみようかと思います。
今回はWindows UNICODE プログラム(つまりUTF-16)となります。
ですので使用するAPIもすべてW系を明示的に使ってます。
Windowsのアプリとサービスの違い
まず、普段作っているWindowsアプリとWindowsサービスでは、動き方がかなり違います。
普通のWindowsアプリは、ユーザーがログオンした後に起動して、そのユーザーのデスクトップ上で動作します。
たとえば、
ログオン
↓
Explorer.exe
↓
アプリ起動
↓
ウィンドウを表示
と言う感じです。
一方、Windowsサービスは Service Control Manager(SCM) というWindows側の仕組みによって管理されます。
ざっくり書くと、
Windows起動
↓
Service Control Manager
↓
サービス起動
↓
バックグラウンドで処理
となります。
つまり、普通のアプリのように、
WinMain()
{
CreateWindow(...);
MessageLoop();
}
みたいな世界ではありません。
サービスにはサービスの入口があります。
そしてWindows側から、
開始
停止
一時停止
再開
シャットダウン
を受けとります。
この点が、Windowsサービスと普通のWindowsアプリとの最大の違いです。
Windowsサービスは、ユーザーがログオンしているセッションではなく、基本的に Windows Session 0 で動作します。
ユーザーがログインして操作しているデスクトップは通常、Session 1以降の別セッション になるため、サービスから直接ウィンドウやメッセージボックスを表示しても、通常はユーザーから見えません。
つまりWindowsのサービスプログラムは、「バックグラウンドで動く普通のGUIアプリ」ではないということです。
サービスからユーザーへ情報を伝える方法についても、イベントログや名前付きパイプなどを題材に、この後の記事で書けたらいいなと思っています。
Windowsサービスは誰が起動するのか
Windowsサービスは、ユーザーが直接起動するというより、SCMによって起動されます。
Windowsの「サービス」管理画面を開くと、たくさんのサービスが並んでいると思います。
サービスの管理画面は、
services.msc
で開くことができます。
サービスを開始すると、SCM は登録されている実行ファイルを起動します。
ただし、普通のEXEのように、
int main()
や、
int WinMain()
から好き勝手処理を開始するわけではありません。
サービスとして動く場合は、
StartServiceCtrlDispatcherW()
を呼び出して、
「このプロセスはWindowsサービスですよ」
と SCM へ登録します。
サービスの基本構造
では、実際にコードを書いてみます。
Windowsサービスの最小構造をかなり簡略化すると、こんな構造になります。
#include <windows.h>
void WINAPI ServiceMain(
DWORD argc,
LPWSTR* argv);
DWORD WINAPI ServiceCtrlHandler(
DWORD control,
DWORD eventType,
LPVOID eventData,
LPVOID context);
int wmain()
{
SERVICE_TABLE_ENTRYW table[] =
{
{
const_cast<LPWSTR>(L"MyService"),
ServiceMain
},
{
nullptr,
nullptr
}
};
StartServiceCtrlDispatcherW(table);
return 0;
}
そして、SCMからサービスが起動されると、
ServiceMain()
が呼ばれます。
普通のアプリでいうところの、
WinMain()
に近いような存在です。
ただし完全に同じではありません。
ServiceMain() の中では、
RegisterServiceCtrlHandlerExW()
を使って、Windowsから送られてくる停止要求などを受け取るためのハンドラを登録します。
流れとしては、
StartServiceCtrlDispatcherW()
↓
ServiceMain()
↓
RegisterServiceCtrlHandlerExW()
↓
SERVICE_RUNNING
↓
実際のサービス処理
となります。
最初に見ると、正直かなり分かりにくいです。
私も最初に見たとき、
「なんでEXE起動するだけなのにこんなに儀式が多いんだ?」
と思いました。
しかし、これがサービスの基本の流れになるので 「Windowsサービスはこーゆうものなのだ」 と割り切ってしまうのがいいと思います。
サービスの状態をWindowsへ通知する
Windowsサービスでは、サービスが今どういう状態なのかをWindowsへ通知する必要があります。
主な状態は、
SERVICE_START_PENDING
SERVICE_RUNNING
SERVICE_STOP_PENDING
SERVICE_STOPPED
などです。
たとえばサービス起動時は、
SERVICE_START_PENDING
↓
初期化処理
↓
SERVICE_RUNNING
停止時は、
SERVICE_STOP_PENDING
↓
終了処理
↓
SERVICE_STOPPED
という流れになります。
この状態通知には、
SetServiceStatus()
を使います。
ここを適当に実装すると、
「サービスを起動しています…」
のまま止まったり、
「サービスを停止しています…」
のままになったりします。
サービスプログラムでは、実際の処理だけではなく、
SCMに対して正しい状態を通知すること
も重要になります。
サービス本体と実処理を分離するしくみ
サービスプログラムを書くたびに、
ServiceMain()
ServiceCtrlHandler()
SetServiceStatus()
を毎回書くのは面倒です。
そこで今回は、サービスプログラムの基底となるクラスを作り、それを派生させることでサービス共通の処理をまとめてしまおうと思います。
このクラスの考え方としては、
基底クラス:Windowsサービス共通処理
↓
派生クラス:実際のサービス処理(ワーカー)
という形です。
サービス固有の処理だけを派生クラス側に書けるようにします。
イメージとしては、
class CMyService : public CWinServ
{
protected:
bool OnStart() override;
void OnStop() override;
void ServiceWorker() override;
};
みたいな感じです。
こうしておけば、
SCMとの接続
サービス状態管理
停止要求受付
終了イベント
SC_HANDLE管理
などを共通クラス側にまとめられます。
これでサービスを増やしても、毎回同じコードを書かずに済みます。
実際のサービス固有処理は「ワーカー(Worker)」として派生クラスで実装しようと思います。
まずはサービスを登録する
サービスEXEを作っただけでは、Windowsサービスとして動きませんので、 SCM へ登録する必要があります。
登録には、
OpenSCManagerW()
と、
CreateServiceW()
を使います。
サービス登録の大まかな間枯れは、
SC_HANDLE hSCM = OpenSCManagerW(
nullptr,
nullptr,
SC_MANAGER_CREATE_SERVICE
);
でSCMへ接続し、
SC_HANDLE hService = CreateServiceW(
hSCM,
serviceName.c_str(),
displayName.c_str(),
SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
exePath.c_str(),
nullptr,
nullptr,
nullptr,
nullptr,
nullptr
);
という感じで登録します。
ここで少し厄介なのが、
exePath
です。
exepathは実際の実行ファイルのパスをしていしますが、パス文字列に空白が含まれている場合があります。
たとえば、
C:\Program Files\MyService\MyService.exe
このように Program Files にexeを配置する場合などです。
これをそのまま登録すると、SCMが指定されたexeを実行する際、パス解釈の問題を起こす可能性があります。
そのため、
L"\"" + exePath + L"\""
のように引用符で囲んで登録する方が安全です。
この処理も基底クラスに盛り込みます。
サービス停止にはイベントを使う
サービス本体では、ワーカースレッドを動かして処理を行う実装にします。
そして停止要求を受けたら、
SetEvent()
で終了イベントを通知します。
ワーカースレッド側では、
WaitForSingleObject()
などで終了イベントを待ちます。
実際の停止要求では、
SERVICE_CONTROL_STOP
↓
SetEvent()
↓
ワーカー終了待ち
↓
SERVICE_STOPPED
とします。
この構造にすると、
Windowsサービス側の状態管理と、実際の処理を分離しやすい
というメリットがあります。
SC_HANDLEもちゃんと閉じる
Windowsサービスを扱うと、
SC_HANDLE
がたくさん出てきます。
OpenSCManagerW()
OpenServiceW()
CreateServiceW()
などなどです。
当然ですが、使い終わったら、
CloseServiceHandle()
する必要があります。
ただ、分岐が増えると、
ここ閉じたっけ?
こっちのreturnで漏れてない?
となりがちです。
そこで、
RAIIで自動解放する形
にしておくとかなり楽になります。
概念的には、
class ScHandle
{
public:
explicit ScHandle(SC_HANDLE h = nullptr)
: m_handle(h)
{
}
~ScHandle()
{
if (m_handle)
CloseServiceHandle(m_handle);
}
private:
SC_HANDLE m_handle = nullptr;
};
こんな感じです。
これだけでもサービス操作コードがかなり安全になります。
「RAII」とはC++で、「リソースの確保と解放をオブジェクトの寿命に結びつける」考え方です。
「RAII」という名前は難しそうに聞こえるかもしれませんが、要は「後片付けをデストラクタに任せる」としているだけの事です。
Windowsサービスプログラミングは難しいのか
ここまで見ると、
SCM
ServiceMain
ControlHandler
ServiceStatus
SC_HANDLE
終了イベント
ワーカースレッド
と、覚えるものが多いです。
正直、普通のWin32アプリより取っ付きにくいと思います。
ただ、一度共通部分をクラス化してしまえば、
class CMyService : public CWinServ
{
// 実際の処理だけを書く
};
くらいまで単純化できます。
Windowsサービス自体が難しいというより、
最初にWindowsとのお約束が多い
という感じです。
そしてその「お約束」を一度クラスへ閉じ込めてしまえば、かなり扱いやすくなります。
次回
今回は、
Windowsサービスとは何か
SCMとは何か
サービス状態管理
停止処理
登録処理
あたりを整理しました。
次は実際に、
UTF-16対応した Windowsサービスクラスを作って、サービスを起動・停止できるところまでやってみようと思います。
Windowsサービスはまだ死なズ。
というか、普通に現役です。
では、また。