前項でIIS用FastCGIについて述べた件の続き。
モジュールマップ登録では個々の.fcgiファイルについて1エントリが必要とされるため、拡張子.fcgi全体をひとつのモジュールマップで済ませるための簡単なラッパーを書いてみた。ものすごく単純だが用法を誤らなければ相応に使える。
use strict;
use warnings;
use FCGI;
use File::Spec;
open STDERR, ">&", STDOUT;
my $request = FCGI::Request or die;
while ( $request->Accept >= 0 ) {
my $stderr = '';
eval {
local $SIG{__WARN__} = sub { $stderr .= shift };
local $SIG{__DIE__} = sub { die($stderr = shift) };
my $file = File::Spec->catfile(
$ENV{DOCUMENT_ROOT} // 'Undefined',
$ENV{SCRIPT_NAME} // 'index.fcgi'
);
return print "Status: 404\n\n" unless -f $file;
do $file;
};
$stderr = "$@" if $@;
if ($stderr) {
print "Status: 403\nContent-Type: text/plain\n\n", $stderr;
}
}
1;
__END__
このラッパーを要求パス *.fcgi に対して適用する1と、ドキュメントルート以下の FastCGIファイルについて 半常駐化状態で実行されるようになる。PATH_INFO は期待どうり動作するし、 既定のドキュメント に index.fcgi を追加した場合にも対応する。ただしあくまでも簡易的に、だ。
このラッパーがなぜ半常駐化なのかというと、常駐対象になるのが、特殊変数および our
宣言2されたパッケージ変数と use
または require
で読み込まれたモジュールに限定されているからだ。.fcgi ファイル自体は do $file
の形式で読込実行されているが、これはこの命令が実行される度にファイルアクセスが発生し、コードが再コンパイル3されることによる。このとき do $file
は package main
名前空間内で実行されているため、複数の .fcgi で同名のグローバル変数や関数宣言が共有されることになる。
.fcgi 自体は毎回再コンパイルされるが、その中で使われる use モジュールについてはコンパイル済コードが再利用されるため、その使用比率が大きいコードほどメリットがある。一方で名前空間のファイル別隔離処理を端折っているため同名関数は後続のコード実行で上書きされてしまうため注意を要する。このあたりが簡易版と称する所以だ。
エラー処理について言えば、FastCGI 起動前に STDERR を STDOUT にリダイレクトすることで、do $file
実行時の構文エラーが極力 IIS Worker Process へ渡らないように細工している。$SIG{}
の定義も同様に STDERR を漏らさないためだ。これを怠ると IIS が 503 Service Unavailable を返すようになり、かつ、サーバ自体を再起動しなければ回復できない4ことがままある。
このラッパーそのものはドキュメントルートの外に配置する。また拡張子も.fcgi以外(.pl)とするべきだ。またこのラッパー自体は完全常駐状態となるため、リロードするためには IIS Worker Process を終了させなければならない。 ↩
our $count //= 0; などと書けば、未定義状態の初回ロード時だけ初期化される。レキシカル変数については eval $EXPR と違って異なるスコープとなる。よって my 宣言は重複しても影響がない。 ↩
require は再ロードも再コンパイルもしないが、再実行もしない。再コンパイルせずに再実行だけ行う関数はない。 ↩
IISサービス(W3SVC)を再起動するだけでは回復しない。$stderr の出力時に Status: 500 としていないのも同じ理由による。これだけ念を入れても不用意な実行時エラー(よくあるのは再入不能モジュールの実行時エラー。例えば binmode Encoding関係や Win32関係など)でサービス停止に追い込まれる当たり、IIS の FastCGI 環境は結構繊細なので、エラーに無頓着なタイプの開発者には全く向いていない。 ↩