shonen.hateblo.jp

やったこと,しらべたことを書く.

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.popenoptions 引数と、IO.pipe を使う。

まず、IO.pipeで入力用ioと出力用ioを取得する。IO.popenoptions 引数を通して、出力用ioを標準エラー用パイプとして渡す。 その後、こちらでは出力用ioは使わないので、クローズする(クローズしないと子プロセスが書き込めない?)。 そして、入力用ioからgetsread を叩くことで、標準エラーを得ることができる。