SendMessage とゾンビ

dispoff.exe は、モニタの電源を切ると、すぐさま終了するアプリ・・・のつもりだったが、Process Explorer で見ると、プロセスが生き残っているのが確認できた。なぜだろう。ExitProcess() まで呼んでるのに終了しないとは。

ただし、いつでもゾンビになるわけではなく、何らかの条件が整ったときに、こうなるようだ。

いつでも再現するんなら、いろいろなコードを試して、ああでもないこうでもないと試行錯誤ができるのになあ。と。

ためしに Explorer.exe を落とした状態で dispoff.exe を起動してみたら、再現できたので、デバッグ開始。

バグ再現ソース

再現するソースは以下。

#define UNICODE
#pragma comment(lib, "user32.lib")
#include <windows.h>
void __start__() {
  // program will start from here if `gcc -nostartfiles`
  ExitProcess(WinMain(GetModuleHandle(NULL), 0, "", 0));
}
int WINAPI WinMain(HINSTANCE hi, HINSTANCE hp, LPSTR cl, int cs) {
  SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2);
  return 0;
}

これを gcc -nostartfiles 略 でビルドして、Explorer.exe を落としてから実行すると、ゾンビになる。

あやしいところといえば、-nostartfiles である。そこで、ふつうにビルドしたのだが、それでも再現した。

ということは、SendMessage() が原因なはず・・・だが、引数をいろいろ変えて実行してもだめだった。

API を変えたらバグが消えた

そういえば、メッセージ送信 API には、PostMessage() もあるな、と思い出し、それに替えてみたら、ゾンビにならず、終了するようになった。

それぞれの API の特徴として、大雑把に引用すると、以下である。

"メッセージを処理し終わった後で、制御を返します。"

"メッセージを処理するのを待たずに制御を返します。"

SendMessage の弱点

SendMessage は、送信先のウィンドウが、メッセージを処理し終わるまで待つのである。HWND_BROADCAST で、すべてのトップレベルウィンドウに送信しているので、おそらく、それらすべてウィンドウがメッセージを処理し尽くさないと、dispoff.exe に制御が戻らないということになる。

ゾンビになっているということは、すなわち、誰かがメッセージを処理していないということだ。HWND_BROADCAST だと、それが誰なのか特定するのに手間がかかる。というか、どうやって特定すればいいのか分からん。そういうツールもないことはないんだろうけど。

つぶやき

そもそも、メッセージを処理してないってどういうことなんだろう。ウィンドウプロシージャは存在するけど、GetMessage() や DispatchMessage() などを用いて、メッセージキューを消化しない・・・みたいなタイプのプロセスでもいるのだろうか。あるいは、WM_SYSCOMMAND 受信時に、ウィンドウプロシージャが return 0 をしないとかかな? たぶんそれだと、DefWindowProc() 呼び忘れっぽいよな。・・・はてさて。メッセージについての仕様とか、あまりよくわかってないので、いらん推測を書くのはやめておこう。(書いてから言うな)

PostMessage による解決

でまあ、PostMessage() のほうは、送信先ウィンドウがメッセージを処理するかどうかなんて放置して、さっさと自身を終了するようになっている。これによって、理論上は WinMain 関数もさっさと終了するし、実際、終了してるので、PostMessage のそうした挙動が効いているのだろう。

SendNotifyMessage による解決

さらに、メッセージ送信 API には SendNotifyMessage というものもある。

"メッセージの処理を終えるのを待ちません。"

これが素晴らしいものである。名前は SendMessage に似ているが、PostMessage と同様に、送信先ウィンドウプロシージャにメッセージを送信すると、すぐに終了するようになっている。

SendNotifyMessage は、PostMessage よりも素晴らしい。なぜならば、PostMessage は、メッセージキューの最後にメッセージを格納するのに対して、SendNotifyMessage は、SendMessage と同じく、ウィンドウプロシージャを直接呼び出すからである。

まー、どちらの実装にしても、体感できるほどの差はなかったが、理論上は、SendNotifyMessage のほうが相手に素早くメッセージを食わせられるということだ。

デバッグ済みコード

というわけで、以下のコードをもって、デバッグ完了とした。

#define UNICODE
#pragma comment(lib, "user32.lib")
#include <windows.h>
void __start__() {
  // program will start from here if `gcc -nostartfiles`
  ExitProcess(WinMain(0, 0, NULL, 0));
}
int WINAPI WinMain(HINSTANCE hi, HINSTANCE hp, LPSTR cl, int cs) {
  SendNotifyMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2);
  return 0;
}

__start__ の中身もちょっと変えているが、デバッグとは関係ない。