ATL でサービスプログラムを作ってみる

SPECIAL


サービスプログラム

サービスプログラムとは、Windows のバックグラウンドで動作するプログラムです。管理ツールの 「サービス」 の一覧に登録されているのがそうです。

今回は、以前に EZ-NET 研究室: ATL でサービスプログラムを作ってみる 【失敗版】 にて Visual C++ 7.0 を使ってのサービスプログラムの作成に挑戦してみましたが、なにやらうまくいかなかったので、あらためて Visual C++ 6.0 での再挑戦です。

 

プロジェクトを作成する

Visual C++ 6.0 を起動して、「ATL COM AppWizard」 を作成します。そしてサーバの種類として 「サービス (EXE)」 を選択してプロジェクトを作成します。

 

プログラムの実装

生成されたクラスとその概要

サービスプログラムの制御に関係するメソッドは次のもののようです。

  • CServiceModule::Start
  • CServiceModule::ServiceMain
  • CServiceModule::Run
  • CServiceModule::Handler
  • CServiceModule::LogEvent

これらの関数が用意されていて、役割は次のようになります。

 

HRESULT CServiceModule::Start()

サービスのスタートアップおよび登録。プログラマーが特に意識する必要はなさそうです。

 

void CServiceModule::ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv)

コントロール要求ハンドラの登録と、メインループの実行。プログラマーが特に意識する必要はなさそうです。

この中で Run() 関数が呼び出されて、サービスプログラムのメイン処理が開始されることになるようです。

 

void CServiceModule::Run()

サービスのメイン実行ルーチン。この中でサービスが初期化され、メッセージループが開始されるようです。

このループはサービスを停止するまで持続されます。

 

void CServiceModule::Handler(DWORD dwOpcode)

サービスコントロールマネージャが呼び出し、サービスプログラムに 「停止」 とか 「開始」 とかいった指示を受け取るための関数です。

dwOpcode にて、サービスコントロールマネージャからの通知を受け取り、それにあった処理を実装する必要があります。

 

SERVICE_CONTROL_STOP サービスの停止
SERVICE_CONTROL_PAUSE サービスの一時中断
SERVICE_CONTROL_CONTINUE サービスの続行
SERVICE_CONTROL_INTERROGATE サービスへの問い合わせ
SERVICE_CONTROL_SHUTDOWN サービスのシャットダウン
(N/A) 不明なコード

このうちの SERVICE_CONTROL_STOP と不明なコード以外は、新規作成直後には何の関数も呼び出さないようになっていますので、必要な機能を書き加える必要があります。

また不明なコードが検出された場合には、ディフォルトではイベントログへの書き出しになっているようです。

 

void CServiceModule::LogEvent(LPCTSTR pFormat, ...)

イベントログへメッセージを書き出すようです。

処理を追記すべき場所を探す

とりあえずデバッガを使って、サービスの機能を実装したらいいかを調べてみることにします。

Run() 関数が重要なループになるようですけど、とりあえずはそれを呼び出す ServiceMain() 関数の最初の行にブレークポイントを設定してみることにしましたが、デバッグモードからは呼び出されないのか、ブレークポイントで停止してくれませんでした。

なので改めて Run() 関数の最初の行にブレークポイントを設定して実行してみると、ちゃんとブレークポイントに到達しました。

 

特に深追いすることなくステップオーバーで一通り進んでみると、この関数内の最後のほう、次のようなプログラムの部分でループを繰り返しているようです。

MSG msg;

while (GetMessage(&msg, 0, 0, 0))

DispatchMessage(&msg);

ここで GetMessage() 関数は、メッセージキューからメッセージを取得するための API で、WM_QUIT を取得しない限り 0 以外(TRUE) を返します。メッセージを取得するまで待つようです。

また DispatchMessage() 関数は、メッセージを送るための API で、取得したメッセージをそのまま再送しているようです。

 

なのでここで、Windows のプログラムのひとつとして正常に機能するように調整されているようです。

サービスの機能を実装するならば、この while ループ内で、メッセージループに支障をきたさない程度に実装するか、またはその直前あたりで別スレッドを立てるのがよさそうです。または PeekMessage() API を使ったループに切り替えるかですけど。

 

機能を実装してみる

■ 基本コードの追加

とりあえず、1秒刻みに BEEP 音でも鳴らすサービスを作成してみましょうか…。ということで先ほど調べた while ループの中へ、システムビープを鳴らすためのコードを書き加えてみることにしました。

書き込む部分は、Run() 関数内の最後のほうです。

LogEvent(_T("Service Started"));

if (m_bService)

SetServiceStatus(SERVICE_RUNNING);

 

MSG msg;

while (GetMessage(&msg, 0, 0, 0))

{

MessageBeep(MB_OK);

DispatchMessage(&msg);

}

 

_Module.RevokeClassObjects();

 

CoUninitialize();

}

コード的には単純に、while ループ内で MessageBeep() API を呼び出してあげるだけです。

さてこのようにして実行してみましたが、どうやら GetMessage() 関数の待ちのため、システムビープはなりませんでした。ループの手前ならば音が鳴るので、メッセージがそう届くものではないようです。

となるとやはり、別スレッドによる処理のほうが都合がいいのかもしれませんね。

 

とりあえず、せっかくなので WM_TIMER メッセージを発生させてみることにします。先ほどのコードの while ループの前に、SetTimer() API をつかって 1 秒ごとに WM_TIMER が発生するようにしてみます。

なお、m_timer は、UINT_PTR 型のインスタンスで、CServiceModule のインスタンスとして追加しておきました。これをつかってタイマーを特定します。

 

LogEvent(_T("Service Started"));

if (m_bService)

SetServiceStatus(SERVICE_RUNNING);

 

MSG msg;

 

m_timer = SetTimer(NULL, NULL, 1000, NULL);

 

while (GetMessage(&msg, 0, 0, 0))

{

if (msg.message == WM_TIMER) MessageBeep(MB_OK);

DispatchMessage(&msg);

}

 

_Module.RevokeClassObjects();

 

CoUninitialize();

}

こうして実行すると、1秒間隔で WM_TIMER が発生するため、ちゃんとそれにあわせてシステムビープがなるようになりました。

ただ SetTimer にて特定の関数を呼び出すように指定することができるようなので、実際にちゃんとしたプログラムを組む場合にはそのようにしたほうがよさそうですけどね。

 

■ Handler を実装してみる

これでとりあえず (?) 基本機能の実装ができたので、今度は制御機構のコーディングをしてみます。といっても Handler() 関数に SetTimer() API や KillTimer() API を実装するだけという簡単なものですけど。

 

inline void CServiceModule::Handler(DWORD dwOpcode)

{

switch (dwOpcode)

{

case SERVICE_CONTROL_STOP:

KillTimer(NULL, m_timer);

SetServiceStatus(SERVICE_STOP_PENDING);

PostThreadMessage(dwThreadID, WM_QUIT, 0, 0);

break;

case SERVICE_CONTROL_PAUSE:

KillTimer(NULL, m_timer);

break;

case SERVICE_CONTROL_CONTINUE:

m_timer = SetTimer(NULL, NULL, 1000, NULL);

break;

case SERVICE_CONTROL_INTERROGATE:

break;

case SERVICE_CONTROL_SHUTDOWN:

KillTimer(NULL, m_timer);

break;

default:

LogEvent(_T("Bad service request"));

}

}

 

■ サービスとして登録する

とりあえずコードが書けたら、コントロールパネルのサービスとして登録してみます。

「プロジェクト」 の 「設定」 ダイアログから、「カスタムビルド」 タブを選択します。するとそこの 「コマンド」 と表記された下のエディットボックスに、登録コードがあります。

ここの、

"$(TargetPath)" /RegServer

の記載を、

"$(TargetPath)" /Service

にすることで、次のビルドからはローカルサーバではなくサービスとして登録されます。

 

そうしたらコントロールパネルの 「サービス」 を開いて、登録したサービスを起動してみます。すると今回の場合、ちゃんと、一秒刻みでシステムビープがなり始めました。停止させればシステムビープもやみます。

ただ、「開始」 と 「停止」 以外の機能は利用できないみたいですね…。

 

■ その他の機能を有効にするには…

とりあえず初期化関連のあたりを調べてみると、CServiceModule::Init() 関数にてなにやら初期状態を決定しているようです。

そのなかでまず気になったのが m_status メンバ変数への設定です。しらべてみたところこれは SERVICE_STATUS 構造体のインスタンスで、メンバーの意味は次のようになるようです。

 

dwServiceType サービスの種類です。 SERVICE_WIN32_OWN_PROCESS
dwCurrentState サービスの初期状態です。 SERVICE_STOPPED
dwControlsAccepted 受け入れるハンドラです。 SERVICE_ACCEPT_STOP
dwWin32ExtCode 起動中または停止中に起こったエラーを報告するためのエラーコードです。 0
dwServiceSpecificExitCode これもエラーを報告するためのコードらしい…。 0
dwCheckPoint 進行状況を伝えるための値らしい…。 0
dwWaitHint サービスの開始や停止にかかる時間の指標をミリ秒単位で指定するそうです…。 0

ざっとこのような感じですけど、この中の dwControlsAccepted の値を調整すればよさそうですね。

 

これのとりうる値のうち、基本的なものは次のような感じになるようです。

  開始 停止 中断 続行 ダウン
SERVICE_ACCEPT_PAUSE_CONTINUE × × ×
SERVICE_ACCEPT_SHUTDOWN × × × ×
SERVICE_ACCEPT_STOP × × × ×

 

なので、Init() 関数の行の dwControlsAccepted を次のようにすることで、中断や続行といった機能を持たせることができそうです。

m_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SHUTDOWN;

このようにして改めて登録してみたところ、今度はちゃんと 「一時停止」 も選択肢に入るようになりました。ただ、コードの実装が甘かったようで、エラーとなってしまいましたが。

 

■ 中断コードを調整する

おそらく、SetServiceStatus() 関数をハンドラで呼び出していないのが原因のような気がします。というわけで SetServiceStatus のとりうる引数を調べてみたところ、次のようなものをとるようです。

 

SERVICE_CONTINUE_PENDING サービスの再開手続き中を意味します。
SERVICE_PAUSE_PENDING サービスの停止手続き中を意味します。
SERVICE_PAUSED サービスの停止中を意味します。
SERVICE_RUNNING サービスの稼動中を意味します。
SERVICE_START_PENDING サービスの起動中を意味します。
SERVICE_STOP_PENDING サービスの停止中を意味します。
SERVICE_STOPPED サービスの停止を意味します。

これは m_status.dwCurrentState に渡せる値と同じなのですけど、これらを SetServiceStatus へ渡すことで、現在の状態を通知することができるようです。

ということはとりあえず中断コードの場合、まずはハンドラにて SERVICE_CONTROL_PAUSE をハンドルした直後に SERVICE_PAUSE_PENDING に設定したあと、タイマーを削除してから SERVICE_PAUSED を設定すればいいという感じでしょうか。

 

ためしに Handler の SERVICE_CONTROL_PAUSE の部分を次のように変えてみます。

case SERVICE_CONTROL_PAUSE:

SetServiceStatus(SERVICE_PAUSE_PENDING);

KillTimer(NULL, m_timer);

SetServiceStatus(SERVICE_PAUSED);

break;

このようにしてみると、ちゃんと一時停止を行うことができるようになりました。

ためしに SERVICE_PAUSED の方の SetServiceStatus 行を消してみるとやはり失敗。逆に SERVICE_PAUSE_PENDING の方をけしてみると、この場合はとりあえずエラーになるということはないようです…。

 

このような感じで、再開 (SERVICE_CONTROL_CONTINUE) ならば SERVICE_CONTINUE_PENDING から SERVICE_RUNNING に移るようにコーディングしてやればよさそうです。

ただ、SERVICE_CONTROL_SHUTDOWN のタイミングが今ひとつわからなかったです。とりあえずすぐに支障をきたすことはなさそうなので、とりあえずは、開始・停止・中断・続行 あたりを実装できるのでよしとしておくことにします。