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階層しかしちゃダメという無茶な要求だと思う