Rubyでstdinもstdoutもstderrもtimeoutも出来る万能exec
勢いで記事を生やした。
環境
$ ruby -v ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-linux-gnu]
何を作るか
別のプログラムを引数付きで起動したり、標準入力を与えたり、標準出力や標準エラーを取得したりしたい。
なぜ作ったか
- 毎回書いているような気がしたから。1つ型を作っておいたほうが良さそう。
完成品
def maiexec(cmd, stdin, args = [], tle = 8) pid = nil stat = nil stdout = nil stderr = nil er, ew = IO.pipe io2 = nil t1 = Thread.start do IO.popen([cmd] + args, 'w+', {:err => ew}) do |io| ew.close io2 = io pid = io.pid io.puts stdin io.flush io.close_write stdout = io.read(200000) stderr = er.read(200000) i, stat = Process.waitpid2 io.pid io.close er.close end end unless t1.join(tle) t1.kill Process.kill :TERM, pid io2.close er.close return [nil, nil, nil] else return [stat.exitstatus, stdout, stderr] end end
これ1つで、
- 引数付きでプログラムを起動
- 標準入力を与える
- 標準出力を得る
- 標準エラーを得る
- 終了コードを得る
- タイムアウト時間を指定
出来る
出来ないことは、インタラクティブに標準入出力をやりとりすること。 さすがにその場合はpopenをちゃんと書いたほうが良い。
利用例
a.rbがあるとする。
puts "args:#{$**','}" str = STDIN.gets.chomp STDOUT.puts "stdout:#{str}" STDERR.puts "stderr:#{str}"
別のrubyプログラムから、以下を実行すると、
p maiexec('ruby', 'foo', ['a.rb', 'arg1', 'arg2'])
次を得る。タイムアウト以外は正しく動作しているのが確認できる。
[0, "args:arg1,arg2\nstdout:foo\n", "stderr:foo\n"]
テクニック的なところ
タイムアウト処理
タイムアウトをキーワードにして適当にググると、Timeoutモジュールが引っかかる。
でもこのモジュールは少し問題があって、IOでブロッキングされるとTimeout出来なくなる。
なので、Threadを2つ用意して、片方はIO.popen
して、もう片方がsleep timelimit
して、先に終わったほうがスレッドを操作して…とやっていたが、最近になってThread#join
が使えることを知った。
Thread#join
は、そのスレッドが終了するまで親スレッドを待機するメソッドだが、パラメータを指定することで、「スレッドが終了あるいは一定時間経過するまで」親スレッドを待機出来る。
一定時間経過したかどうかは返り値で分かる。
これを使えば簡単にタイムアウト処理を実装できそう。できた。
stderr取得
IO.popen
で帰ってくるioでは、標準入力と標準出力しか操作できない。
標準エラーのioを取るには、IO.popen
の options
引数と、IO.pipe
を使う。
まず、IO.pipe
で入力用ioと出力用ioを取得する。IO.popen
の options
引数を通して、出力用ioを標準エラー用パイプとして渡す。
その後、こちらでは出力用ioは使わないので、クローズする(クローズしないと子プロセスが書き込めない?)。
そして、入力用ioからgets
やread
を叩くことで、標準エラーを得ることができる。