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 登録から削除