Windows NT サービスプログラムを書く。 この前書いたのは西暦 2000 年の夏だった。 ちょうど 2 年くらい前か。 すっかり書き方を忘れている。 備忘もかねて A6 の情報カードにメモをとりながら作っていく。
ふと気がついたけど、 先日購入したプリンタはふちなし印刷が可能だ。 ということはこの情報カードいっぱいに印刷できるということか。 情報カードデータベースを作るのに便利かもしれない。
今日は朝からずっと水曜日だと勘違いしていた。 少年マガジンを立ち読みしようとして勘違いに気づく。
昨日から Windows NT サービスプログラムを作り始めたわけだが、 終了処理がうまく動かない。 昨日は雑誌の記事を参考に書いていったので、 細かい部分がよく理解できていなかったような気がする。 2 年くらい前の MSDN Library をインストールしていたのをふと思い出して、 サービス関係のドキュメントを読む。 解説は英文のみの様子。 もしかして 2 年前もこの情報をもとに作ったのだろうか。 どうも鬱になる前後の記憶があいまいである。 またサービスを書くときのために、 MSDN の解説を翻訳して要約してソースのコメントに書いておくことにする。 そうそう書く機会などないだろうな。
何とかサービスの雛型を作ることが出来た頃には、 だいぶ遅い時間になってしまった。 終了処理がうまく行かなかったのは終了状態になったことを SCM に通知していなかったせいだった。 あとはイベントログにメッセージを表示するようにしなくては。 これにはメッセージコンパイラが必要なはずだが、 Cygwin というか mingw で提供されているのだろうか。 もっとも提供されていたところで日本語に対応しているはずもないか。 (2002.11.21 追記: Microsoft が無償配布している Platform SDK にメッセージコンパイラが付属している)
帰って TV をつけたらアクターズスタジオインタビューが終わるところだった。 ローレン・バコール・インタビュー。 しまった、そうとわかっていればもっと早く帰ってきたのに。 私は強く再放送を切望するものである。 といってもこれはこれで BS の再放送なんだけどな。 まあ、ほんの 10 分足らずとはいえバコールを見られて幸せ。 真に美しい女性というのは齢を重ねても美しいものだな。
昨日作成した Windows NT サービスの作り方をまとめてみた。
Windows NT サービスは 4 つのコンポーネントからなる。 サービスコントロールマネージャ(SCM)、 サービスコントロールプログラム(SCP)、 サービスコンフィギュレーションプログラム、 そしてサービス自体、 である。
SCM は Windows NT に標準で添付されている。 Windows NT が起動したときに実行を開始し、 シャットダウンするときに停止する。 SCM はサービスに対し、起動・終了・一時停止・処理続行などを指示する。
SCP はサービスの起動・終了・一時停止・続行・その他の、 サービスを制御するためのユーザインタフェースを提供する。 SCP は SCM とやり取りしてサービスを制御する。 たとえばコントロールパネルの「サービス」は Windows NT で提供される SCP である。
サービスコンフィギュレーションプログラムは、 サービスのインストール・削除・構成の変更を行う。
サービスは SCM からの情報とコマンドを受け付けるコードを持つ。
また SCM に対して自分の状態を報告する。
サービスプログラムは、
ひとつ以上のサービスのコード(関数)を含むことが出来る。
サービスプログラムに一つのサービスのコードしかない場合、
サービスは SERVICE_WIN32_OWN_PROCESS
タイプで作成する。
複数ある場合は SERVICE_WIN32_SHARE_PROCESS
タイプで作成する。
サービスプログラムは通常の Win32 実行プログラムである。
通常コンソールアプリケーションとして作成する。
したがってエントリポイントは main
関数になる。
main
関数への引数は、
レジストリの ImagePath
の値に設定される。
SCM はサービスプログラムを起動すると、
サービスプログラムから StartServiceCtrlDispatcher
関数が呼び出されるのを待つ。
そのため SERVICE_WIN32_OWN_PROCESS
タイプのサービスは、
すぐに StartServiceCtrlDispatcher
を実行しなければならない。
StartServiceCtrlDispatcher
関数には SERVICE_TABLE_ENTRY
構造体をわたす。
この構造体にはサービス名とサービスのエントリポイント(ServiceMain
関数)を設定する。
この関数は渡されたサービス名とエントリポイントを使用して新しいスレッドを作成し、
そのスレッドでサービスの ServiceMain
関数をコールする。
コールした ServiceMain
関数からリターンした時点でそのスレッドを終了させる。
したがってサービスは自分自信のスレッドを TerminateThread
で終了してはいけない。
StartServiceCtrlDispatcher
関数は、
プロセス中のすべてのサービスが終了したときに、
呼び出したサービスプログラムにリターンする。
すなわちサービスのスレッドが残っているとサービスプログラムにはリターンしない。
したがって main
関数の最小限のコードは次のようになる。
int main () { SERVICE_TABLE_ENTRY ent[] = { { "aService", ServiceMain }, { 0, 0 }, }; StartServiceCtrlDispatcher (ent); return 0; }
SCM はサービス制御リクエストを受け取ると、
サービスに対応した Handler
関数を呼び出す。
Handler
関数は ServiceMain
関数で指定される。
ServiceMain
の作り方
ServiceMain
関数の処理は次のようになる。
Handler
関数を登録する。
SCM は ServiceMain
が起動されてから 1 秒以内に登録されることを期待している。
1 秒を過ぎるとサービスは失敗したとみなされる。
しかし SCM はサービススレッドを終了させない。
サービスはそれなりに実行される。
Handler
関数は RegisterServiceCtrlHandler
関数で登録する。
この関数の戻り値はサービス状態ハンドルで、
SCM にサービスの状態を通知するときに使う。
このハンドルは SCM が管理しているのでクローズしてはいけない。
また Handler
関数でもこの戻り値を使用するため、
グローバル変数に格納しておく必要がある。
初期化にかかる時間が 1 秒以下程度であればすぐに行ってもよい。
1 秒以上かかるのであれば、
サービス状態 SERVICE_START_PENDING
を SCM に通知する。
通知するには SERVICE_STATUS
構造体に状態を設定して
SetServiceStatus
関数を呼び出す。
さらに初期化中には SCM に進捗を通知すること。
それには SERVICE_STATUS
構造体の
dwCheckPoint
と dwWaitHint
に値を設定して
SetServiceStatus
関数を呼び出す。
dwCheckPoint
には最初は 0 を、
dwWaitHint
には初期設定が完了するまでに必要な時間をミリ秒単位で指定する。
dwCheckPoint
は便宜をはかるために用意されたもので、
サービスの進行状況をどの程度の頻度で報告するかは、
サービスで自由に決めてよい。
もし初期化の個々のステップを報告するのであれば、
dwWaitHint
に次のステップに到達するために必要だと思われる時間をセットすればよい。
初期化が終了したら SERVICE_STATUS
構造体に SERVICE_RUNNING
を設定して
SetServiceStatus
を呼び出す。
dwCheckPoint
と dwWaitHint
には 0 を指定する。
この時点で SCM はサービスが実行中とみなす。
実行中にサービスの状態が変化したら、
必ず SetServiceStatus
を呼び出して SCM に情報を通知する。
通常サービスはループ中で処理を行う。 ループの中でサービスは自分自信を一時停止させて、 たとえばネットワーク要求、 一時停止、 処理続行、 終了、 シャットダウンなどの通知を待つ。 ネットワーク要求があればその要求を処理し、 処理が終わったらまた一時停止して次の要求や通知を待つ。
サービスの初期化中やサービスの実行中にエラーが起こって
サービスの処理を終了しなければならない場合に、
後処理が長くかかるなら SERVICE_STOP_PENDING
状態を SCM に通知すること。
そして後処理が終了したときに SERVICE_STOPPED
状態を通知する。
このとき SERVICE_STATUS
構造体の dwServiceSpecificExitCode
と
dwWin32ExitCode
に、
必ずエラーコードを設定すること。
SERVICE_STATUS g_srv_status = { SERVICE_WIN32_OWN_PROCESS, SERVICE_START_PENDING, SERVICE_ACCEPT_STOP, NO_ERROR, NO_ERROR, 0, 0 }; SERVICE_STATUS_HANDLE g_srv_status_handle; void WINAPI ServiceMain (DWORD ac, char **av) { g_srv_status_handle = RegisterServiceCtrlHandler (SERVICE_NAME, Handler); if (!g_srv_status_handle) return; g_srv_status.dwCurrentState = SERVICE_START_PENDING; g_srv_status.dwCheckPoint = 0; g_srv_status.dwWaitHint = 1000; SetServiceStatus (g_srv_status_handle, &g_srv_status); /* ここで初期化をおこなう */ g_srv_status.dwCurrentState = SERVICE_RUNNING; g_srv_status.dwCheckPoint = 0; g_srv_status.dwWaitHint = 0; SetServiceStatus (g_srv_status_handle, &g_srv_status); /* 以下でサービス本来の処理を行う */ while (g_srv_status.dwCurrentState != SERVICE_STOPPED) { switch (g_srv_status.dwCurrentState) { case SERVICE_STOP_PENDING: g_srv_status.dwCurrentState = SERVICE_STOPPED; SetServiceStatus (g_srv_status_handle, &g_srv_status); break; default: Sleep (1000); break; } } }
Handler
の作り方
サービスは制御ハンドラの Handler
関数を持たなければならない。
これはサービスが SCP から制御リクエストを受信したときに、
サービス制御ディスパッチャ (StartServiceCtrlDispatcher
) が呼び出す。
したがってこの関数は制御ディスパッチャのコンテキスト (スレッド) で実行される。
Handler
関数は呼び出されたら、
必ず SetServiceStatus
関数をコールして状態を通知しなければならない。
これは状態が変化したかどうかにかかわりなく行うこと。
SCP は ControlService
関数を使って制御リクエストを送ってくる。
SERVICE_CONTROL_INTERROGATE
制御リクエストはすべてのサービスが受け取る。
その他の標準的リクエストは、受け付けるかどうかを
SetServiceStatus
で指定することができる。
標準のリクエスト以外にも、
ユーザ定義の制御リクエストを扱うことができる。
制御ハンドラは必ず 30 秒以内にリターンしなければならない。 リターンしなければ SCM はエラーになる。 もしサービスが制御ハンドラを実行するのに時間がかかるのであれば、 別スレッドを生成してそこで処理を行うようにして、 すぐにリターンすること。
void WINAPI Handler (DWORD ctrl) { switch (ctrl) { case SERVICE_CONTROL_STOP: g_srv_status.dwCurrentState = SERVICE_STOP_PENDING; g_srv_status.dwWin32ExitCode = 0; g_srv_status.dwCheckPoint = 0; g_srv_status.dwWaitHint = 0 break; case SERVICE_CONTROL_INTERROGATE; break; default: break; } SetServiceStatus (g_srv_status_handle, &g_srv_status); }
まず SCM データベースへのハンドルを OpenSCManager
を使用して取得する。
つぎにサービスオブジェクトを CreateService
関数で作成する。
DWORD install () { char path[MAX_PATH]; SC_HANDLE scm = 0; SC_HANDLE srv = 0; int rc = 0; if (!GetModuleFileName (0, path, MAX_PATH)) return GetLastError (); scm = OpenSCManager (0, 0, SC_MANAGER_ALL_ACCESS); if (!scm) return GetLastError (); srv = CreateService (scm, SERVICE_NAME, DISPLAY_NAME, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, path, 0, 0, 0, 0, 0); if (!srv) rc = GetLastError (); else CloseServiceHandle (srv); CloseServiceHandle (scm); return rc; }
サービスを削除するには DeleteService
関数を使用する。
この関数は SCM データベースからサービスを削除するためのマーキングを行うだけである。
データベースのエントリは、オープンされているすべてのハンドルがクローズされて、
サービスが停止するまで削除されない。
もしサービスを停止できなければ、
システムを再起動したときに削除される。
SCM はレジストリからサービスキーとサブキーを削除することでサービスを削除する。
DWORD remove () { SC_HANDLE scm = 0; SC_HANDLE srv = 0; int rc = 0; scm = OpenSCManager (0, 0, SC_MANAGER_ALL_ACCESS); if (!scm) return GetLastError (); srv = OpenService (scm, SERVICE_NAME, DELETE); if (!srv) rc = GetLastError (); else { if (!DeleteService (srv)) rc = GetLastError (); CloseServiceHandle (srv); } CloseServiceHandle (scm); return rc; }
サービスを開始するには StartService
関数を使用する。
StartService
関数はデータベースがロックされていると失敗する。
データベースのロック状態は QueryServiceLockStatus
関数で知ることができる。
StartService
関数は、ServiceMain
関数のスレッドが作られたら
すぐにリターンしてくる。すなわち ServiceMain
中の初期化プロ
セスの終了を待たない。そのため実際にサービスが実行を開始でき
たかどうかは、QueryServiceStatus
関数で dwCurrentState
メン
バをチェックして判断しなければならない。初期化している間は
SERVICE_START_PENDING
が設定されているはずである。また
dwWaitHint
メンバには次にサービスの状態を確認するまでにどの
くらい待てばよいかのインターバル時間がミリ秒単位で設定される
はずである。
DWORD start () { SC_HANDLE scm = 0; SC_HANDLE srv = 0; SERVICE_STATUS st; memset (&st, 0, sizeof (st)); int rc = 0; if (!(scm = OpenSCManager (0, 0, SC_MANAGER_ALL_ACCESS))) rc = GetLastError (); else if (!(srv = OpenService (scm, SERVICE_NAME, DELETE))) rc = GetLastError (); else if (!StartService (srv, 0, 0)) rc = GetLastError (); else if (!QueryServiceStatus (srv, &st)) rc = GetLastError (); else { DWORD old; while (st.dwCurrentState == SERVICE_START_PENDING) { old = st.dwCheckPoint; Sleep (st.dwWaitHint); if (!QueryServiceStatus (srv, &st)) { rc = GetLastError (); break; } if (old >= st.dwCheckPoint) break; } if (rc) ; else if (st.dwCurrentState == SERVICE_RUNNING) printf ("service was started\n"); else printf ("service status is %d\n", st.dwCurrentState); } if (srv) CloseServiceHandle (srv); if (scm) CloseServiceHandle (scm); return rc; }
サービスの状態を変更させるには、
サービスに制御リクエストを送ればよい。
これは ControlService
関数を使用する。
この関数には Handler
関数に渡す制御コードを指定する。
制御コードは以下の標準のコードでもよいし、
ユーザ定義の値でもよい。
SERVICE_CONTORL_STOP
SERVICE_CONTORL_PAUSE
SERVICE_CONTORL_CONTINUE
SERVICE_CONTORL_INTERROGATE
各サービスは受け付けて処理する制御コードを宣言することができる。
そのコードを得るには QueryServiceStatus
関数を使用するか
SERVICE_CONTROL_INTERROGATE
制御コードを指定して
ControlService
関数をコールする。
引数で渡した SERVICE_STATUS
構造体の dwControlsAccepted
に、
Handler
関数が処理可能なコードのビットマスクが設定される。
サービスオブジェクトを取得する際に、
それぞれの制御コードに対して異なるレベルのアクセス権を指定する必要がある。
たとえば SERVICE_CONTROL_STOP
コードを送るには、
サービスオブジェクトを SERVICE_STOP
アクセス権で開かなければならない。
ControlService
関数からリターンすると、
引数で渡した SERVICE_STATUS
構造体にはサービスの最新の情報が設定されている。
以下はサービスを停止するコード例である。
DWORD stop () { SC_HANDLE scm = 0; SC_HANDLE srv = 0; SERVICE_STATUS st; memset (&st, 0, sizeof (st)); int rc = 0; if (!(scm = OpenSCManager (0, 0, SC_MANAGER_ALL_ACCESS))) rc = GetLastError (); else if (!(srv = OpenService (scm, SERVICE_NAME, SERVICE_STOP))) rc = GetLastError (); else if (!StartService (srv, 0, 0)) rc = GetLastError (); else if (!ControlService (srv, SERVICE_CONTROL_STOP, &st)) rc = GetLastError (); else printf ("service was stopped\n"); if (srv) CloseServiceHandle (srv); if (scm) CloseServiceHandle (scm); return rc; }
サービスの状態は QueryServiceStatus
関数で取得することができる。
以上
朝方急に冷え込んだ様子。 風邪を引いたか咳が止まらず発熱がひどい。 という夢を見た。 実際目覚めてみると、 頭がふらふらして仕方がない。 一時は休むことも考えたが、 少し遅く出ることにする。 出かける頃にはだいぶましになった。
電車を待つ間、小腹がすいたので、 サンドイッチを買って駅のホームで食うことにした。 しかしここのホームはベンチが少ないな。 8 人しか座れないし、 そのうえ喫煙コーナーの隣だ。 仕方がないのでひとけのない階段に座り込んで貪り食う。 高校生が階段に腰掛けているのを見るとちょっと腹が立つが、 自分のことは棚に上げることにした。 頑丈な棚だ。
会社のデスクトップで、 サービスやイベントログの作り方をまとめるために、 ばしばし日本語を入力していったが、 どうも目が疲れて困る。 15 インチのモニタで 1280x1024 にしているのでフォントが小さすぎるのだ。 そこで Emacs Lisp をごそごそいじって表示用フォントをちょうどいいくらいにする。 だいたい 16 ピクセルくらい。 しかしフォントくらいピクセルでなしにポイント数で指定できればいいのに。 できるような気もするが、なんかうまく行かなかった。 ちなみに Mozilla はフォントの設定で画面の解像度を指定できるようになっている。 標準で用意されているのは 72dpi と 96dpi。 そしてその他を選択すると画面に線が現れて、 定規で測ってそれが何 mm か入力しろといってくる。 これ最初に見たときはちょっと感動した。 現在の解像度では 125 dpi だそうだ。 しかしこれって何の意味があるのだろう。 フォントサイズはポイント数ではなくピクセル数で指定するのだ。 今は 18 ピクセルを指定している。 やっぱりスタイルシートのフォントサイズのためか。
会社のデスクトップで、 サービスやイベントログの作り方をまとめるために、 ばしばし日本語を入力していったが、 どうも指が疲れて困る。 そこでキーボードドライバをドボラック配列に置き換えることにした。 やり方は Dvorak Simplified Keyboad for Japanese に詳しい。 会社のマシンだと自分以外の人が使うこともあるので、 今まで禁じ手にしていたのだが割り切ることにした。 最初はレジストリで指定しようとしたが、 キーボードが 106 日本語キーボードだと記号類がノートパソコンで使っている配列と違ってしまう。 ノートは物理的なキーボードは日本語 JIS キーボードだが、 キーボードドライバは US タイプライタ配列キーボード用のドボラック配列ドライバを使っている。 つまり shift-2 でアットマーク @ が入力できる。 レジストリではキーボードのスキャンコードを指定して割り当てを変えるのだけれど、 101us キーボードと106 日本語キーボードでは 同じスキャンコードでも入力される文字が異なるのだな。 結局ドライバがハードウェアの種類をもとにスキャンコードを文字コードに変換しているということか。 スキャンコードのマッピングをレジストリで変更できるというのは、 かなりハードウエアよりの設定ができるのでよさげに思えるが、 ハードウェアの違いを吸収できないのでこの場合役に立たない。 ちょっと詰めが甘いような気がする。
そしてもちろん IME のローマ字定義リソースをいじって「C」でカ行を入力できるようにする。
MS-IME 97 のローマ字定義リソースは
HKEY_CURRENT_USER\Software\Microsoft\Ime\msime97\Romadef
にあり。
ちなみに IME2000 では GUI で、ローマ字の割り当てを定義できるようになっている。
で、また黙々と SDK の解説などあちこちを参照しながらまとめていく。 すると今度はエディタの禁則処理が気になりだした。 長音記号 (ー) と促音拗音をぶら下がり禁則にしている。 ぶら下がるのは区切り文字や括弧類だけでよい。 というわけで Emacs Lisp をごそごそといじる。
しかしイベントログって仕組みが大げさな割にはたいしたことができないし、 たいしたことをするつもりがないのに大掛かりな仕掛けを使わなければならないような気がする。 ちょっと文字列を出力したいだけなのに、 イベントメッセージファイルだとか、 カテゴリーメッセージファイルだとか、 パラメータメッセージファイルだとか、 そんなのをつくって番号付けしてとか面倒で仕方がない。 別に作らなくてもいいことはいいが、 そうするとイベントビューアにエラーメッセージが出るのが気に入らない。 とはいえログファイルを独自に作るのも面倒といえば面倒である。 ふと思ったけど、 そういえば IIS とかもログファイルはテキストファイルに吐き出しているな。 これは手段の選択を誤ったか。
そういえば Cygwin の inetd や cygrunsrv サービスはイベントログを吐き出していたなと思い、 ちょっと調べてみた。 どうやら syslog を使っている様子。 早速ソースをのぞいてみたら、 Windows 9x 系ではファイルに、 NT 系ではイベントログに書き出しているようだ。 イベントログのソース名は、 openlog で ident を指定した場合はその名前、 指定していない場合は "Cygwin" を使う模様。 つまり ident か "Cygwin" がイベントソース名になるというわけだ。 でもメッセージファイルを登録していないので、 メッセージビューアが、 メッセージファイルを登録していないというエラーもいっしょに表示するのがうざったい。
先週作りかけていた Perl プログラムをパッケージに分割して、 テストプログラムを作ったら、 なぜかパッケージ間のサブルーチン呼び出しがエラーになる。 サブルーチンが見つからないと。 パッケージスコープを何か勘違いしているのか。 もう一度 Perl 本を読み返す。
知り合いの芝居を観にいく。 演劇というよりコントに近いかも。 でもギャグが身体や間や状況を使ったものではなく、 台詞だけというのが多かった。 いわゆる内輪うけネタをちょっと外に開いた感じ。 あまり好きなお笑いではない。 演劇という観点から見ても決して芝居がうまいわけでもないし。 設定自体はシュールでもっと面白くできるんじゃないかと思う。 わざわざセットまで組んでやるもンでもない。 台本もらって読めば充分といった内容。
しかしこの程度のギャグで笑いがドッと起こるのだから、 客のレベルも低すぎるのではないか。 でもまあ感受性が豊かなのはいいことだ。
デスクトップでネットに接続しようとしたら、 ルータにアクセスできずにエラーとなる。 ping を打っても返答なし。 ループバックは生きているようなので、 ルータがいかれたか。
ひさしぶりに演劇の稽古。 ダンスのステップを踏む。 久々だったので、終わった頃には身体にがたがきていた。 終わって早々に退散。
友人から借りた DVD 「ドラキュラ」を観る。 1997 年、コッポラの作品。 衣装やセットやメークに金かけているなという感想あるのみ。 金をかけすぎたローバジェット作品か。 ストーリーも映像もチープである。 失敗作でも地獄の黙示録レベルであれば鑑賞に堪えるが、 これならもっと金をかけずに、 いかにも低予算で作りましたって感じにした方が自分好みだ。
あまりに疲れすぎたのか、 身体が火照って寝つかれず。 どうにも眠れないのでデスクトップを立ち上げ、 ルータやネットワークの設定をあれこれいじってみる。 最後の手段でルータに刺さっているケーブルを別の口に刺しなおしてみたら簡単につながってしまった。 いったい何が起こったのやら。 まさか知らないうちにまた雷でも落ちていたのか。