1. ホーム
  2. ruby

[解決済み] Rubyで外部プロセスのSTDOUTから継続的に読み込む

2023-04-02 04:36:22

質問

blenderをコマンドラインからrubyスクリプトで実行し、blenderの出力を一行ずつ処理してGUI上のプログレスバーを更新したいのですが、blenderが外部プロセスであることはあまり重要ではありません。blenderが、私が読む必要のある標準出力である外部プロセスであることは、本当に重要ではありません。

blenderプロセスがまだ実行されているとき、blenderが通常シェルに出力するプログレスメッセージをキャッチすることができないようで、いくつかの方法を試してみました。私はいつもblenderの標準出力にアクセスするようです。 の後に blenderが終了した後、まだ実行中ではなく、blenderの標準出力にアクセスするようです。

これは失敗した試みの例です。これは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}

編集します。

もう少し明確にするために、blenderを起動するコマンドはシェルに出力のストリームを返し、進捗を示します(パート1-16が完了したなど)。それは、"get" への呼び出しは、blender が終了するまでブロックされるようです。問題は、blenderがシェルに出力をプリントするように、blenderがまだ実行されている間、この出力にアクセスする方法です。

どのように解決するのですか?

私は自分のこの問題を解決するためにいくつかの成功を収めました。同じような問題を抱えている人がこのページを見つけられるように、ここに詳細といくつかの説明を掲載します。しかし、もしあなたが詳細を気にしないのであれば。 これが簡単な答えです。 :

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

そして これが長い答えです。

そして、これが長い答えです

あまりに多くの詳細があります。

本当の問題は、プロセスが明示的に標準出力をフラッシュしない場合、標準出力に書き込まれたものは、プロセスが終了するまで、実際に送信されるのではなく、バッファリングされ、IO を最小化することです (このことは どうやら は多くの C ライブラリの実装の詳細で、 IO の頻度を減らすことでスループットを最大化するように作られています)。もし、stdout を定期的にフラッシュするようにプロセスを簡単に変更できるなら、それがあなたの解決策になるでしょう。私の場合、それはblenderだったので、私のような全くの素人がソースを修正するのは少し気が引けました。

しかし、これらのプロセスをシェルから実行すると、それらはリアルタイムでシェルに標準出力を表示し、標準出力はバッファリングされていないようです。他のプロセスから呼び出されたときのみバッファリングされると思いますが、シェルが処理されている場合、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はバッファリングされているのです。これは STDOUT.flush ステートメントを追加することで解決できます。しかし、ソースを変更できない場合は、これを回避する必要があります。プロセスの外からフラッシュすることはできません。

サブプロセスがリアルタイムにシェルにプリントできるのであれば、これをRubyでもリアルタイムにキャプチャする方法があるはずです。そして、それはあります。Rubyのコアに含まれているPTYモジュール(1.8.6)を使わなければなりません。悲しいのは、それがドキュメント化されていないことです。しかし、私は幸いにもいくつかの使用例を見つけました。

まず、PTYが何であるかを説明します。 疑似端末 . 基本的に、これは ruby スクリプトがサブプロセスに対して、あたかもシェルにコマンドを入力した本当のユーザであるかのように振舞うことを可能にします。そのため、ユーザーがシェルを通じてプロセスを開始した場合にのみ発生する変更された動作(この場合、STDOUTがバッファリングされないなど)が発生します。別のプロセスがこのプロセスを開始したことを隠すことで、STDOUTがバッファリングされていないため、リアルタイムに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