hideden.hatenablog.com

はてなぶろぐー。URLなげー。

perl-win32で再起動前に終了処理をする方法

結構つまったのでメモ。

Win32では通常、ユーザーが再起動またはログアウト処理を開始すると全てのWindowに対してWM_QUERYENDSESSIONというメッセージが送られてくる。これを受信した際に各種終了処理(データを保存したり接続を切ったり)を行い、その結果終了していい場合はtrueを返す。falseを返せば再起動要求をキャンセルできる。(強制再起動ではない場合)

Perlでシステム管理系のツールを書く場合、これと同じような動作をさせたいので調べてみた。

環境は ActivePerl 5.8.8.822 / Perl 5.8.8 / WinXP SP2

まず、PerlはコンソールプログラムなのでそもそもMessageを処理していない。この場合、再起動の通知を受け取る場合はSetConsoleCtrlHandlerにて通知を受け取るCallback関数を登録する。これにより、CTRL_SHUTDOWN_EVENTやCTRL_C_EVENTなどを受け取ることができる。

ってことで、Win32::APIモジュールを使って実装してみた。

use Win32::API;
use Win32::API::Callback;

sub handler {
    my $sig = shift;
    print "SIG: $sig\n";
    return 1;
}

my $cb = Win32::API::Callback->new(\&handler, "I", "I" );
my $setcchand = Win32::API->new('kernel32','SetConsoleCtrlHandler','KI','I' );
$setcchand ->Call($cb, 1);

Win32::Sleep(200) for (1..1000);

ここでCtrl+Cを押せばCTRL_C_EVENTが発生してprintされるはずなんだが・・・Perlがエラーで落ちる。さて困ったぞと。

しょうがないのでPerlのwin32の実装を見てみる。

win32/win32.c:5188
    SetConsoleCtrlHandler(win32_ctrlhandler,TRUE);

win32/win32.c:5038-
    case CTRL_C_EVENT:
        /*  A CTRL+c signal was received */
        if (do_raise(aTHX_ SIGINT))
            sig_terminate(aTHX_ SIGINT);
        return TRUE;

なるほど。。。SetConsoleCtrlHandlerにすでに登録済み。これには本来は複数登録することが可能なのだが、どれかがTRUEを返した時点で他のは実行されない。って事で、こいつがここでSIGINTを投げるようにエミュレートしてるっぽい。なんつー罠。ちなみにCTRL_SHUTDOWN_EVENTの時はSIGTERMを投げるようだ。

なので、こう書くのが正解。

use Win32;

$SIG{TERM} = \&before_shutdown;
$SIG{INT}  = \&on_ctrl_c_press;  # Ctrl + C
$SIG{HUP}  = \&on_close_click;   # [x] ボタンを押したとき

sub before_shutdown { exit; }
sub on_ctrl_c_press { exit; }
sub on_close_click  { exit; }

Win32::Sleep(1000) while(1);

シグナルを処理する関数内ではexitしないと、終了しなくなる。タスクマネージャからは終了できるけど。
これでいいはずなのだが、シャットダウンはなぜかキャッチできない。

詳しくコードを追っかけるのが面倒だったので適当に調べたところ、再起動時にはSIGQUITが飛んでくる。どこからだろうか。まぁいい。

なので、

$SIG{QUIT} = \&before_shutdown;

とかけばいい事になる。で、一見解決したようなんだが、このままだとDOS窓が邪魔。なのでActivePerlに入ってるwperl.exeを使う。これだとDOS窓がでない。素敵。

っと思ったら罠が。単純に表示しないようにしてるわけじゃなく、コンソールプログラムからWindowsプログラムにsubsystemを切替えてるらしい。そのおかげでこっちでは使えない。なので窓無しでシステムの終了をHOOKする為にはやっぱりWM_QUERYENDSESSIONを処理しろって事なのか。

win32/win32.c:1900   win32_async_check
     /* Perhaps some other messages could map to signals ? ... */

とか書いてあるがそこ一帯が#if 0でコメントアウト

うーん。Perlだけでやるのきついかなー、やっぱ。wperlって窓作ってるのかなー。こりゃもうちょいソースまじめに読まないとダメだ。とりあえずここまでで諦め。