DebugScreen におけるシグナルハンドラの正書法

11/21追記: id:tokuhirom がモジュール化してくれた CGI::ExceptionManager を使いましょうってことでよろしく (参照: http://d.hatena.ne.jp/tokuhirom/20081119/1227113506)

$SIG{__DIE__} をオーバーライドしてきれいなデバッグ画面を出してあげることは WAF の重要な役割のひとつだけど、それを正しくやるのは難しい。

まず、単純に、$SIG{__DIE__} で受け取ったものをエラーとして扱ってしまうと、ユーザーが呼び出しているライブラリが die を隠蔽するような構造になっていた場合 (eval {} が使われていた場合) に、それがうまく機能しなくなってしまう (古いバージョンの Encode.pm にはこの問題があって、結局 Encode.pm 側で対処したらしい: 435505 – Encode module in perl package can generate warnings.)。

で、一般論としては、

perlfunc見てたら「evalから$SIG{__DIE__}呼び出された時に何もしたくないときは die @_ if $^S してね!」って書いてあった…

http://perl.g.hatena.ne.jp/Uchimata/20080919/1221842240

らしいと知ったんだけど、WAF の場合、要件として、

  • redirect の動作等で、非正常系での終了を (die を使って) サポートしたい
    • 必ず return redirect(...) を書けというのは難しい
    • なので redirect() の中で die したのを eval ブロックで catch したい

というものがあり、ユーザーロジック全体を eval {} で囲まざるを得ないので、そもそも $^S を判定に使えない *1

というような事情で、上記対策がない古い Encode.pm の環境下で MENTA と NanoA が、どちらも動かない問題があったのを、結局こういう風にすることで解決しました、という話。以下が多分ベストな解。

do {
    my $err_info;
    local $SIG{__DIE__} = sub {
        my ($msg) = @_;
        if (ref($msg) eq 'HASH' && $msg->{finished}) {
            undef $err_info;
        } else {
            $err_info = NanoA::DebugScreen::build($msg);
        }
        die $msg;
    };
    local $@;
    eval {
        NanoA::Dispatch->dispatch();
        undef $err_info;
    };
    NanoA::DebugScreen::output($err_info)
        if $err_info;
};

シグナルハンドラは、最後のエラー情報を記録するだけにして、実行パスに対する変更は行わない。そして、正常パス以外で抜けてきた場合のみ、エラーメッセージを表示する。

というような解に id:hide-K さんに問題を指摘されたのをきっかけに id:miyagawa さんにアドバイスもらいながらたどり着きました。ありがとうございました。

22:06 追記: 一部バグってた。ごめんなさい >

*1:というか、$^S を使えというのは、そもそも eval {} を1階層しかしちゃダメという無茶な要求だと思う