ATL でサービスプログラムを作ってみる (成功版)
SPECIAL
サービスプログラム
サービスプログラムとは、Windows のバックグラウンドで動作するプログラムです。管理ツールの 「サービス」 の一覧に登録されているのがそうです。
今回は、以前に EZ-NET 研究室: ATL でサービスプログラムを作ってみる 【失敗版】 にて Visual C++ 7.0 を使ってのサービスプログラムの作成に挑戦してみましたが失敗に終わっていました。
それが、このたび Noppi さまよりすばらしい情報をいただきましたので、あらためて再挑戦することになりました。もっとも、情報をいただいたのは 2003/05 なのでした。せっかく丁寧に日本語訳までしてくださったのに、これまで手をつけられなくてごめんなさいでした。
原因は Visual C++ .NET のバグらしく、PreMessageLoop の実装に不具合があるそうです。あわせて教えてくださった情報源は http://www.codeproject.com/atl/MigratingAtlServices.asp です。
また、Visual C++ 7.1 ( .NET 2003 ) だと、属性サポートをなしで ATL プロジェクトを作成してしまうと、Release ビルド時に C3861 コンパイルエラーが起きてしまうとのことです。これは、stdafx.h あたりに #include <cstdio> を加えてあげることで解決できるとのことでした。
改めて、すばらしい情報ありがとうございました。そして、今までこの情報を寝かせてしまってごめんなさいでした。
さっそく、お礼と確認をかねて作成実験です。なお、前回は Visual Studio .NET (2002) だったのですけど、今回は Visual Studio .NET 2003 を使用しています。なお、Visual C++ 6.0 でのお話は EZ-NET 研究室: ATL でサービスプログラムを作ってみる をご覧ください。
プロジェクトを作成する
Visual C++ 7.1 を起動して、「ATL プロジェクト」 を作成します。今回はプロジェクト名を TestService4 としました。
そして ATL プロジェクトウィザードの ”アプリケーションの設定” にて、サーバの種類を 「サービス (EXE)」 としてプロジェクトを作成します。
プログラムの実装
以前の文書内にも書いてありますけど、念のため改めてサービスを構成する上での主要関数を挙げておきます。
生成されたクラスとその概要
ウィザードによって生成されたクラスは、CAtlServiceModuleT クラスから派生することで、サービスプログラムに必要な機能が実装されるようです。
- CAtlServiceModuleT::Start
- CAtlServiceModuleT::ServiceMain
- CAtlServiceModuleT::Run
- CAtlServiceModuleT::Handler
これらの関数が用意されていて、役割は次のようになります。
HRESULT CAtlServiceModuleT::Start(int nShowCmd)
サービス実行時に、_tWinMain 関数が、CAtlServiceModuleT::WinMain を呼び出し、それに続いて呼び出される関数だそうです。ただし、ローカルサーバの場合は、CAtlServiceModuleT::Run が呼び出されるそうです。
なお、引数の nShowCmd は WinMain と同様のウィンドウの表示方法です。戻り値は成功ならば S_OK を返します。
void CAtlServiceModuleT::ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv)
管理ツールの 「サービス」 から開始されたときに呼び出される関数です。サービスコントロールマネージャ (SCM) によって呼び出され、サービスのコントロールが行えるように、いろいろな手続きを実行するようです。
そして、サービスの主要処理コードのある、CAtlServiceModuleT::Run 関数が呼び出されます。
HRESULT CAtlServiceModuleT::Run(nShowCmd = SW_HIDE)
まず PreMessageLoop 関数を呼び出してスレッド ID を決定し、InitializeSecurity 関数によって CoInitializeSecurity 関数を呼び出し、セキュリティ記述子を NULL に設定するそうです。
この手続きが行われると、どのユーザでもオブジェクトにアクセスできるようになるらしく、この設定が不要の場合は、PreMessageLoop をオーバーライドして InitializeSecurity 関数を呼び出さないようにする必要があるようです。
この手続きが終わると、メッセージループの処理へ移ります。
void CAtlServiceModuleT::Handler(DWORD dwOpcode)
サービスコントロールマネージャが呼び出し、サービスプログラムに 「停止」 とか 「開始」 とかいった指示を受け取るための関数です。
dwOpcode にて、サービスコントロールマネージャからの通知を受け取り、それにあった処理を実装する必要があります。
CAtlServiceModuleT では、次のような対応で、メソッドが呼び出されるようになっています。
SERVICE_CONTROL_STOP | OnStop | サービスの停止 |
---|---|---|
SERVICE_CONTROL_PAUSE | OnPause | サービスの一時中断 |
SERVICE_CONTROL_CONTINUE | OnContinue | サービスの続行 |
SERVICE_CONTROL_INTERROGATE | OnInterrogate | サービスへの問い合わせ |
SERVICE_CONTROL_SHUTDOWN | OnShutdown | サービスのシャットダウン |
N/A | OnUnknownRequest | 不明なコード |
このうち、OnStop は、唯一 AtlServiceModuleT によって実装がなされているもので、規定の動作は、サービスコントロールマネージャへもうじき終了する旨を伝えたうえで、PostThreadMessage を呼び出してサービス自身に終了命令を送信するそうです。
バグがあるか確認する
だいぶ時間があいてしまったので、まずは今回の原因となったバグが存在するかのチェックから行うことにしました。
"TestService4" プロジェクトのプロパティを開いて、「ビルドイベント」 の 「ビルド後のイベント」 に少々手を加えます。次のように /RegServer から /Service に変更して、ビルド後にサービスとして登録されるようにします。
"$(TargetPath)" /Service
こうして、Debug ビルドを行って、エラーなく完了することを確認します。
ビルドが終わったら、管理ツールからサービスマネージャを起動します。一覧の中に "TestService4" があるので、それをスタートさせてエラーが出ることを確認します。
なお、自分の環境 (Windows Server 2003) では "ローカル コンピュータ 上の TestService4 サービスは起動して停止しました。パフォーマンス ログ、警告サービスなど、一部のサービスは作業がない場合に自動的に停止します。" というエラーダイアログが表示されました。
無事 (?) エラーが発生したので、改めて改善作業を進めていくことにします。
2つのメソッドをオーバーライドする
正常に動作させるにあたって次の2つのメソッドを "CTestService4Module" クラスでオーバーライドします。そのうち、特に重要なのが PreMessageLoop() メソッドです。
HRESULT CTestService4Module::PreMessageLoop(int nShowCmd)
Run ループの最初で呼び出されるメソッドをオーバーライドします。どうやら Visual C++ 7.0 / 7.1 はここの実装が甘くてエラーとなってしまうそうなので、実装部分に少しコードを追加する必要があります。
追加するコードは次のようになります。書く場所は、生成された "testservice4.cpp" ファイルの class CTestService4Module { } の括弧の中です。
HRESULT PreMessageLoop(int nShowCmd)
{
HRESULT hr = __super::PreMessageLoop(nShowCmd);
#if _ATL_VER == 0x0710 // VC7.0 の場合は 0x0700
if (SUCCEEDED(hr) && !m_bDelayShutdown) hr = CoResumeClassObjects();
#endif
if (SUCCEEDED(hr))
{
// ここにサービスの初期化に必要な処理を書きます
}
return hr;
}
HRESULT CTestService4Module::PostMessageLoop()
このメソッドは実装上の不足等はないようですけど、重要なのでオーバーライドしておきましょう。この関数は Handler がサービスの停止 (SERVICE_CONTROL_STOP) を受け取った際に OnStop から呼び出されます。
HRESULT PostMessageLoop()
{
// ここにサービス終了時に必要な後処理を書きます。
return __super::PostMessageLoop();
}
注意事項としては、忘れずに規定クラスの PostMessageLoop() を呼び出しましょう、くらいでしょうか。
バグがなくなったかを確認する
これらの実装が終わったら、改めてコンパイルして、サービスが正常に開始するかを確認します。
上記のオーバーライドを追加しただけなので何も起こりませんけど、起動ボタンを押してサービスが起動状態になることを確認し、停止ボタンを押してサービスが終了することを確認します。
実際、やってみると無事、エラーメッセージが表示されることなくサービスの起動と停止が行えました。情報ありがとうございました^^
Beep を鳴らすサービスを作ってみる
正常にサービスとして機能する枠組みが出来上がったので、1 秒ごとに Beep 音を鳴らすというなんとも役に立たないサービスを実装してみることにします。
まず、CTestService4Module クラスに、UINT_PTR 型で m_timer というメンバ変数を追加します。これはタイマー関連の関数で使用します。
CTestService4Module
{
private:
UINT_PTR m_timer;
そして、先ほどオーバーライドした PreMessageLoop 関数にタイマーを初期化するコードを追加します。
HRESULT PreMessageLoop(int nShowCmd)
{
HRESULT hr = __super::PreMessageLoop(nShowCmd);
#if _ATL_VER == 0x0710 // VC7.0 の場合は 0x0700
if (SUCCEEDED(hr) && !m_bDelayShutdown) hr = CoResumeClassObjects();
#endif
if (SUCCEEDED(hr))
{
// ここにサービスの初期化に必要な処理を書きます
m_timer = SetTimer(NULL, NULL, 1000, NULL);
}
return hr;
}
そして、今回は WM_TIMER メッセージを受け取るために RunMessageLoop をオーバーライドします。基底クラス CAtlExeModuleT の RunMessageLoop は次のようになっていました。
void RunMessageLoop() throw()
{
MSG msg;
while (GetMessage(&msg, 0, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
ここを少し変更して、WM_TIMER メッセージであった場合には Beep 音を鳴らすようなコードにして、CTestService4Module クラスへ実装します。なお、基底クラスの RunMessageLoop は呼び出しません。
void RunMessageLoop() throw()
{
MSG msg;
while (GetMessage(&msg, 0, 0, 0) > 0)
{
if (msg.message == WM_TIMER) MessageBeep(MB_OK);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
そして、サービスの停止や再起動といったことに対処できるように、Handler 関数が呼び出すそれぞれの関数をオーバーライドしてコードを実装します。
OnStop に関しては基底クラスの OnStop にて SERVICE_STOP_PENDING が実行されますけど、いちおうここから終了手続きということで SetServiceStatus を設定してみました。なお基底クラスのほうでも SERVICE_STOPPED は明示しないようなので、ここでも終了手続き中として
void OnStop() throw()
{
SetServiceStatus(SERVICE_STOP_PENDING);
KillTimer(NULL, m_timer);
__super::OnStop();
}
void OnPause() throw()
{
SetServiceStatus(SERVICE_PAUSE_PENDING);
KillTimer(NULL, m_timer);
SetServiceStatus(SERVICE_PAUSED);
}
void OnContinue() throw()
{
SetServiceStatus(SERVICE_CONTINUE_PENDING);
m_timer = SetTimer(NULL, NULL, 1000, NULL);
SetServiceStatus(SERVICE_RUNNING);
}
void OnShutdown() throw()
{
KillTimer(NULL, m_timer);
}
そして、サービスが一時停止や再開に応答できるように、"CTestService4Module" クラスのコンストラクタを用意して、m_status.dwControlsAccepted の値を次のように調整します。基底クラスのコンストラクタは派生クラスのより先に呼び出されるはずなので、特に明記しなくてもいいはずです。
CTestService4Module() throw()
{
m_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SHUTDOWN;
}
これで実装完了です。無駄なコードもありますけど、とりあえずはこんな感じで作れるということだけでもわかればよしということで…。
さっそくコンパイルして、サービスからいろいろといじってみると…。あれ、、、。基本的にはちゃんと動くのですけど、一時停止から再開のときだけ、Beep 音が鳴らなくなってしまいました。ちゃんと停止させて開始すれば鳴るんですけどね…。ま、まあ、OnContinue が呼び出されていることはちゃんと確認したので、うまくいかないのは Timer 周りのことをよく知らないのにプログラミングしたせいでしょう^^;;;;;
以上で、半端ながら今回の調査は成功のうちに (?) 終了となりました。
補足事項
m_status 構造体
サービスの基本状態を決める m_status 構造体は次のような変数があるようです。
dwServiceType | サービスの種類です。 | SERVICE_WIN32_OWN_PROCESS |
---|---|---|
dwCurrentState | サービスの初期状態です。 | SERVICE_STOPPED |
dwControlsAccepted | 受け入れるハンドラです。 | SERVICE_ACCEPT_STOP |
dwWin32ExtCode | 起動中または停止中に起こったエラーを報告するためのエラーコードです。 | 0 |
dwServiceSpecificExitCode | これもエラーを報告するためのコードらしい…。 | 0 |
dwCheckPoint | 進行状況を伝えるための値らしい…。 | 0 |
dwWaitHint | サービスの開始や停止にかかる時間の指標をミリ秒単位で指定するそうです…。 | 0 |
m_status.dwControlsAccepted 変数
サービスが受け入れるコントロールを指定する変数です。指定できる値は次のものがあり、これらを | 演算子で組み合わせて指定する事ができるようです。
開始 | 停止 | 中断 | 続行 | ダウン | |
---|---|---|---|---|---|
SERVICE_ACCEPT_PAUSE_CONTINUE | × | × | ○ | ○ | × |
SERVICE_ACCEPT_SHUTDOWN | × | × | × | × | ○ |
SERVICE_ACCEPT_STOP | × | ○ | × | × | × |
Handler が呼び出す関数
サービスの状態が変更されるときに Handler が呼び出す関数です。これらをオーバーライドして振る舞いを調整できます。
OnStop についてですが、基底クラスでも実装がなされているので、__super::OnStop() というようにして最後に基底クラスの関数も呼び出しましょう。なお OnStop では SetServiceStatus(SERVICE_STOP_PENDING) のままで、SERVICE_STOPPED にしないまま、基底の OnStop を呼ぶ感じになるようです。
OnStop と OnUnknownRequest 以外は基底クラスでは何も実装されていないようですけど、1度基底クラスの方の実装を確認して、それに応じてコーディングを行うのがよさそうです。
SERVICE_CONTROL_STOP | void OnStop() | サービスの停止 |
---|---|---|
SERVICE_CONTROL_PAUSE | void OnPause() | サービスの一時中断 |
SERVICE_CONTROL_CONTINUE | void OnContinue() | サービスの続行 |
SERVICE_CONTROL_INTERROGATE | void OnInterrogate() | サービスへの問い合わせ |
SERVICE_CONTROL_SHUTDOWN | void OnShutdown() | サービスのシャットダウン |
N/A | void OnUnknownRequest(DWORD dwOpcode) | 不明なコード |
SetServiceStatus 関数
サービスのコントロール中に SetServiceStatus 関数を使ってサービスの状態を明記する必要があるようですけど、その中で指定できるものには次のものがあるようです。
SERVICE_CONTINUE_PENDING | サービスの再開手続き中を意味します。 |
---|---|
SERVICE_PAUSE_PENDING | サービスの停止手続き中を意味します。 |
SERVICE_PAUSED | サービスの停止中を意味します。 |
SERVICE_RUNNING | サービスの稼動中を意味します。 |
SERVICE_START_PENDING | サービスの起動中を意味します。 |
SERVICE_STOP_PENDING | サービスの停止中を意味します。 |
SERVICE_STOPPED | サービスの停止を意味します。 |
サービスの登録と削除
作成したサービスを登録したり削除するためには、次の引数をつけて EXE ファイルを実行すればいいようです。
/Service | 一般的なサービスとして登録 |
---|---|
/RegServer | ローカルサーバとして登録 |
/UnregServer | 登録から削除 |