たごもりすメモ

コードとかその他の話とか。

PerlでSTDIN/STDOUTを任意のファイルハンドルに置き換える

いま書いてるコードで、forkしてexecするんだけど、execする前にSTDIN/STDOUTを任意のファイルハンドルに置き換えたいなー、もっというとexecするプログラムのSTDINにソケットのREADから流れてくるデータを流し込んで、STDOUTの出力をソケットのWRITEに流し込んでやりたいなー、というようなことを考えていた。

で、これが例えば今のプロセスのSTDOUTの出力をファイルに置き換えるには、以下のようにすればいい。

open(STDOUT, '>', '/path/to/file');

シェルスクリプトでも簡単。*1

exec >> /path/to/file

さて、STDIN/STDOUTとconnect済みのソケットを結合したい。connect済みのソケットはファイルディスクリプタは持っているがファイルパスを持っていない、ので、普通にopenし直すだけではうまくいかない。perlでどうすんだっけ、と思っていたら親切な人が教えてくれた。

上記tweetだ2引数openだけど、以下のように3引数openで良いですね。ありがとうありがとう!

# $socket is already connected
open(STDIN, '<&=', fileno($socket));
open(STDOUT, '>&=', fileno($socket));

'&=' ってなんだ、とperlのopenのリファレンスを熟読すると、おお、ちゃんと書いてある。& はopen済みのファイルディスクリプタと接続することを示し、さらにそのときに '=' を指定すると対象のファイルディスクリプタdupしないで同一のものを指すようになる*2、とある。へー、知らなんだ。
で、ファイルハンドル($socket)からファイルディスクリプタを取り出すために fileno() を使っている。

ちなみに一度reopenしたファイルディスクリプタ(STDIN/STDOUT)は exec() で他のプログラムをロードしてもそのまま引き継がれるので、このように準備を整えてから外部プログラムを exec() すれば、呼ばれるほうのプログラムでは STDIN/STDOUT を読み書きするだけで内容がネットワーク越しに転送される。これはだいぶ便利なので、覚えておくと困ったちゃんな構造のツールが綺麗に作れたることがあったりします。

*1:これの出力先を /dev/null にするパターンが、プロセスを端末からデタッチする方法としてよく見られる。そのときはSTDINやSTDERRも対処が必要。

*2:つまり = をつけないとdupする