ATL でサービスプログラムを作ってみる 【失敗版】
SPECIAL
サービスプログラム
サービスプログラムとは、Windows のバックグラウンドで動作するプログラムです。管理ツールの 「サービス」 の一覧に登録されているのがそうです。
Visual C++ 7.0 には ATL をつかってこのサービスプログラムを作成するためのウィザードがあったので、それを使ってサービスプログラムを作成してみることにしました。
が、Visual C++ 7.0 ではどうしてもうまくいきませんでした。現在はまだ検討中です。
プロジェクトを作成する
Visual Studio .NET を起動して、Visual C++ プロジェクトの 「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 を呼び出してサービス自身に終了命令を送信するそうです。
機能を実装してみる
とりあえず、1秒刻みに BEEP 音でも鳴らすサービスを作成してみましょうか…。
と思って、何も手を加えていない状態のプログラムをとりあえずサービスとして登録し、管理ツールから実行してみたのですが、この段階だとすぐに停止してしまいました。
なのでまずはメッセージループあたりを手に入れる必要がありそうです。
RunMessageLoop
そこでまず、メッセージループの本体と思われる CAtlExtModuleT::RunMessageLoop の中身を調べてみることにします。
void RunMessageLoop() : throw ()
{
MSG msg;
while (GetMessage(&msg, 0, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
ソースを見るとこのような感じになっていました。
GetMessage API によって、エラー (-1) か WM_QUIT (0) 以外である限り、while 文の中を実行するようになっています。while 構造内では、TranslateMessage API によって仮想キーを文字列に変換し、DispatchMessage API によって GetMessage API で取得したメッセージをディスパッチ (送信?) します。
ということは、ループが終わってしまうということは、WM_QUIT がどこかで送信されているということですね…。メッセージの取得エラーという可能性もありますけど。またはそもそも、PreMessageLoop メソッドが失敗している可能性もあり…。
Run ループの構造
ループの主要部分のはずの Run 関数の流れを調べてみます。
まずは PreMessageLoop 関数が呼び出されます。この中では ThreadID を取得した上で、InitializeSecurity() 関数が呼び出されます。
それに成功した場合は、続いて (サービス起動ならばログを残して) RunMessageLoop 関数が呼び出されます。
RunMessageLoop が成功に終わると、PostMessageLoop 関数が呼び出され、後始末が行われて終了します。
この分だと、いつ WM_QUIT が発生しているのかよくわかりません…。とりあえず、どうやらイベントログが残っていそうな感じなので、イベントビュアでそのあたりを調べてみます。
すると、イベント ID が 0 となっていて、「リモート コンピュータからメッセージを表示するために必要なレジストリ情報またはメッセージ DLL ファイルがローカル コンピュータにない可能性がある」 とのことです。
イベント ID が 0 というのはなんだか問題な気がするのですが、ほかのサービスでもイベントビュアに ID 0 で残っていたりするのでいいのかも知れません。
デバッガでテストする
とりあえず、ローカルサーバとして起動させ、デバッガで行く末を眺めてみることにします。
すると、Run 関数からトレースしたのですけど、PreMessageLoop 関数が S_FAIL を返しているようです。S_FALSE では後始末こそされるものの、肝心の RunMessageLoop 関数は呼び出されないようになっているので、これでは確かに停止してしまってもおかしくないですね。
このエラー、どうやら AtlExtModuleT::PreMessageLoop 関数内の RegisterClassObjects 関数にて返されるようですけど、いったい何が原因なのでしょう…。
デバッグ可能な限りへ入っていくと、AtlComModuleRegisterClassObjects で S_FALSE が返ってくることがわかります。
呼び出し時の引数は、
pComModule | &_AtlComModule |
---|---|
dwClsContext | CLSCTX_LOCAL_SERVER = 0x4 |
dwFlags | REGCLS_MULTIPLUSER = 1 |
という感じになっていました。
Visual C++ 6.0 で実験
ウィザードで作成した直後であるにもかかわらず、ビルドしてちゃんと動かないとなると…。
とりあえず、Visual C++ 6.0 でも同等のプロジェクトウィザードがあったので、ためしにそれにて新規プロジェクトを立ち上げてみました。
そしてビルド…。/Service オプションで登録して、サービスを開始してみると…。
なんと、あっさりとサービス開始です。
もちろんなにも実装していないので何もしないサービスなのですけど、開始ボタンを押せばちゃんと開始するし、停止ボタンでちゃんと停止してくれました。
Visual C++ 7.0 ではぜんぜんうまくいかないのはなぜなのでしょうね。ただのバグとかだったらいいのですけど…。
とりあえずは、しばらく様子を見ておいたほうがよさそうです…。
サービスとして登録するには
このウィザードで生成されたサービスプログラムは、ビルドしただけでは 「ローカルサーバ」 という位置づけで登録されるそうです。
実際に管理ツールの 「サービス」 へ登録するには、コマンドラインから /Service スイッチをつけて、EXE ファイルを起動します。
たとえば、ServiceApp.exe というサービスプログラムをビルドしたならば、次のような感じでサービスを登録できます。
ServiceApp.exe /Service
さらっと調べてみたところ、この CAtlServiceModuleT で作成したものの引数は、次のものがあるようです。
/Service | 一般的なサービスとして登録 |
---|---|
/RegServer | ローカルサーバとして登録 |
/UnregServer | 登録から削除 |