外部プログラム(tcpdump)に標準入力から有限なデータを渡して,その標準出力を受け取りたい場面に出くわしたのだけれども,ライブラリ一発でうまくいかなかったので試行錯誤の末,それができる関数を作ってみた.
dump = File.read('tcpdump.dump')
filterd_dump = exec_stdio("/usr/sbin/tcpdump -r - -w - port 80 2>/dev/null", dump)
こんな感じで呼び出してデータを加工したい.
exec_stdio.rb
def exec_stdio cmd, input
output = ''
buf_size = 4096
IO.popen(cmd, "r+b") do |io|
offset = 0
writable = true
while offset < input.bytesize
begin
if writable
chunk_size = [buf_size, input.bytesize-offset].min
chunk = input.byteslice(offset, chunk_size)
offset += io.write_nonblock(chunk)
else
output += io.read_nonblock(buf_size)
end
rescue IO::WaitReadable
writable = true
retry
rescue IO::WaitWritable
writable = false
retry
end
end
io.close_write
while !io.eof
begin
output += io.read_nonblock(buf_size)
rescue IO::WaitReadable
retry
end
end
end
output
end
最初IO.writeで普通に書いていたらwiteでブロックされてプログラムがIO待ちで停止していた.調べたところパイプのバッファのサイズは65536(環境によっては4096)byteしか無く,どうもこのバッファを読み書き両方に使ってるみたい?なのでwrite_nonblockで適当な数だけ書いて,書き込めなくなったら例外をキャッチしてread_nonblockで読み出すということをやって,読み込めなくなったら今度は書き込むということをしている.書き込みが終わったらあとは読むだけなので,close_writeした後,read_nonblockで読み出している.本当にこんなコードで良いのだろうか?
バッククォートと一時ファイルで出来なくはないんだけど,わざわざディスクIOを発生させるのも嫌なので,直接データをやり取りしたかった.他にスマートな方法があったら教えて欲しい.