web-dev-qa-db-ja.com

Rubyで外部プロセスのSTDOUTから連続的に読み取ります

コマンドラインからRubyスクリプトを使用してBlenderを実行します。このスクリプトは、blenderによって指定された出力を行ごとに処理してGUIの進行状況バーを更新します。 stdoutを読み取る必要がある外部プロセスです。

ブレンダープロセスがまだ実行中の場合、ブレンダーが通常シェルに出力する進行メッセージをキャッチできないようで、いくつかの方法を試しました。私は常にブレンダーの標準出力にアクセスするようですafterブレンダーは、まだ実行中ではなく、終了しました。

失敗した試行の例を次に示します。 Blenderの出力の最初の25行を取得して印刷しますが、Blenderプロセスが終了した後のみです。

blender = nil
t = Thread.new do
  blender = open "| blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1"
end
puts "Blender is doing its job now..."
25.times { puts blender.gets}

編集:

少しわかりやすくするために、ブレンダーを呼び出すコマンドは、シェルに出力のストリームを返し、進行状況(パート1-16完了など)を示します。 Blenderが終了するまで、出力を「取得」する呼び出しはブロックされるようです。問題は、Blenderがシェルに出力を出力するため、Blenderの実行中にこの出力にアクセスする方法です。

84
ehsanul

私はこの問題を解決することに成功しました。同様の問題を抱えている人がこのページを見つけた場合に備えて、いくつかの説明とともに詳細を示します。しかし、詳細を気にしない場合、ここに短い答えがあります

PTY.spawnを次の方法で使用します(もちろん、独自のコマンドを使用して)。

require 'pty'
cmd = "blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1" 
begin
  PTY.spawn( cmd ) do |stdout, stdin, pid|
    begin
      # Do stuff with the output here. Just printing to show it works
      stdout.each { |line| print line }
    rescue Errno::EIO
      puts "Errno:EIO error, but this probably just means " +
            "that the process has finished giving output"
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end

そして、ここに長い答えがあり、あまりにも多くの詳細があります:

実際の問題は、プロセスがそのstdoutを明示的にフラッシュしない場合、stdoutに書き込まれたものは、プロセスが完了するまで実際に送信されるのではなく、バッファリングされるため、IO (これはapparently IOの頻度を減らしてスループットを最大化するために作成された多くのCライブラリの実装の詳細です。)定期的にstdoutをフラッシュするようにプロセスを簡単に変更できる場合は、私の場合、それはブレンダーでしたので、ソースを変更するために私自身のような完全な初心者には少し威圧的でした。

ただし、これらのプロセスをシェルから実行すると、シェルに標準出力がリアルタイムで表示され、標準出力はバッファリングされていないようです。私が信じている別のプロセスから呼び出されたときにのみバッファリングされますが、シェルが処理されている場合、stdoutはバッファリングされずにリアルタイムで表示されます。

この動作は、出力をリアルタイムで収集する必要がある子プロセスとしてRubyプロセスで確認することもできます。次の行を使用して、random.rbスクリプトを作成します。

5.times { |i| sleep( 3*Rand ); puts "#{i}" }

次に、Rubyスクリプトを呼び出して出力を返す:

IO.popen( "Ruby random.rb") do |random|
  random.each { |line| puts line }
end

期待どおりの結果がリアルタイムで得られないことがわかりますが、その後は一度にすべてが得られます。 random.rbを自分で実行した場合でも、STDOUTはバッファリングされますが、バッファリングされません。これは、random.rbのブロック内にSTDOUT.flushステートメントを追加することで解決できます。ただし、ソースを変更できない場合は、これを回避する必要があります。プロセスの外部からフラッシュすることはできません。

サブプロセスがリアルタイムでシェルに出力できる場合、これをRubyでリアルタイムにキャプチャする方法も必要です。そしてPTYモジュールを使用する必要があります。 、Ruby coreと信じています(とにかく1.8.6)。悲しいことは、文書化されていないことです。しかし、幸いにも使用例がいくつか見つかりました。

最初に、PTYとは何かを説明するために、 疑似端末 の略です。基本的に、Rubyスクリプトは、コマンドをシェルに入力したばかりの実際のユーザーであるかのように、サブプロセスに自身を提示できます。したがって、ユーザーが開始したときにのみ発生する変更された動作別のプロセスがこのプロセスを開始したという事実を隠すことで、バッファされていないため、STDOUTをリアルタイムで収集できます。

Random.rbスクリプトを子としてこれを機能させるには、次のコードを試してください。

require 'pty'
begin
  PTY.spawn( "Ruby random.rb" ) do |stdout, stdin, pid|
    begin
      stdout.each { |line| print line }
    rescue Errno::EIO
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end
173
ehsanul

使用する IO.popenこれ は良い例です。

コードは次のようになります。

blender = nil
t = Thread.new do
  IO.popen("blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1") do |blender|
    blender.each do |line|
      puts line
    end
  end
end
12
Sinan Taifour

STDOUT.flushまたはSTDOUT.sync = true

5
mveerman

Blenderはおそらくプログラムを終了するまで改行を出力しません。代わりに、キャリッジリターン文字(\ r)を印刷しています。最も簡単な解決策は、おそらく進行状況インジケーターで改行を出力するマジックオプションを検索することです。

問題は、_IO#gets_(および他のさまざまなIOメソッド)が区切り文字として改行を使用していることです。 "\ n"文字(どのブレンダー送信していません)。

入力セパレーター_$/ = "\r"_を設定するか、代わりにblender.gets("\r")を使用してみてください。

ところで、これらのような問題については、常に_puts someobj.inspect_または_p someobj_(どちらも同じことを行う)をチェックして、文字列内の非表示文字を確認する必要があります。

4
hhaamu

Ehsanulが質問に答えた時点で Open3::pipeline_rw() が利用可能であったかどうかはわかりませんが、実際に物事が簡単になります。

Blenderでのehsanulの仕事がわからないので、tarxzで別の例を作成しました。 tarは、入力ファイルをstdoutストリームに追加し、xzがそのstdoutを取得して、再び別のstdoutに圧縮します。私たちの仕事は、最後の標準出力を取得して最終ファイルに書き込むことです。

require 'open3'

if __FILE__ == $0
    cmd_tar = ['tar', '-cf', '-', '-T', '-']
    cmd_xz = ['xz', '-z', '-9e']
    list_of_files = [...]

    Open3.pipeline_rw(cmd_tar, cmd_xz) do |first_stdin, last_stdout, wait_threads|
        list_of_files.each { |f| first_stdin.puts f }
        first_stdin.close

        # Now start writing to target file
        open(target_file, 'wb') do |target_file_io|
            while (data = last_stdout.read(1024)) do
                target_file_io.write data
            end
        end # open
    end # pipeline_rw
end
0
condichoso