Rubyで外部コマンドを実行するには様々な方法があるけど、出力とステータスを取りたい場合はOpen3.capture3を使うと簡単に得られる。
# リファレンスマニュアルのサンプルから引用
require "open3"
o, e, s = Open3.capture3("echo a; sort >&2", :stdin_data=>"foo\nbar\nbaz\n")
p o #=> "a\n"
p e #=> "bar\nbaz\nfoo\n"
p s #=> #<Process::Status: pid 32682 exit 0>
面倒なパイプの制御などをしなくても簡単に標準入出力が扱える。
ところが、capture3では次のようなタイムアウト処理はうまく動かない。
require "timeout"
require "open3"
cmd = "/tmp/heavy.sh"
o, e, s = nil
begin
Timeout.timeout(3) do
o, e, s = Open3.capture3(cmd)
end
rescue Timeout::Error
puts "execution expired"
# pidが取れないのでkillできない
# それどころか、コマンドが終了するまでここに到達しない
end
p o, e, s
上記のスクリプトを実行すると3秒でTimeout::Errorが発生しそうだけど、実際には発生しない。
何故かと言うと、capture3が内部で使用しているpopen_runという実装の中で、次のようにensureでコマンドの終了を待っているから。
def popen_run(cmd, opts, child_io, parent_io) # :nodoc:
pid = spawn(*cmd, opts)
wait_thr = Process.detach(pid)
child_io.each {|io| io.close }
result = [*parent_io, wait_thr]
if defined? yield
begin
return yield(*result)
ensure
parent_io.each{|io| io.close unless io.closed?}
wait_thr.join
end
end
result
end
というわけで、タイムアウトしたら中断したい場合は、この辺を紐解いて自前で同じような実装をすることになる。
この場合だとブロックを渡さなければIOと子プロセスのスレッドが返ってくるので、capture3の実装をブロックを使わないで書いたオレオレバージョンとかを作ればいいかもしれない。
ただしそれだとPIDが取れない(Process::StatusはPIDを持っているけど、これは終了するまで手に入らない)ので、popen_runのあたりも含めて自前で実装するはめになる。
なので、前回の外部コマンドを実行してタイムアウトしたらkillするではあえてspawnを直接使って実装したのでした。
0 件のコメント:
コメントを投稿