programing

Ruby의 외부 프로세스 STDOUT에서 지속적으로 읽기

nasanasas 2020. 9. 18. 08:17
반응형

Ruby의 외부 프로세스 STDOUT에서 지속적으로 읽기


명령 줄에서 루비 스크립트를 통해 블렌더를 실행하고 싶습니다. 그러면 블렌더가 제공하는 출력을 한 줄씩 처리하여 GUI에서 진행률 표시 줄을 업데이트합니다. 블렌더가 stdout을 읽어야하는 외부 프로세스라는 것은별로 중요하지 않습니다.

블렌더 프로세스가 여전히 실행 중일 때 블렌더가 일반적으로 쉘에 인쇄하는 진행 메시지를 잡을 수없는 것 같습니다. 몇 가지 방법을 시도했습니다. 나는 항상 블렌더가 실행되는 동안이 아니라 블렌더가 종료 된 후에 블렌더 의 표준 출력에 액세스하는 것 같습니다 .

다음은 실패한 시도의 예입니다. 블렌더 출력의 처음 25 줄을 가져 와서 인쇄하지만 블렌더 프로세스가 종료 된 후에 만 ​​가능합니다.

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 부 완료 등). 블렌더가 종료 될 때까지 출력을 "gets"하는 모든 호출이 차단 된 것 같습니다. 문제는 블렌더가 셸에 출력을 인쇄하므로 블렌더가 여전히 실행 중일 때이 출력에 액세스하는 방법입니다.


이 문제를 성공적으로 해결했습니다. 다음은 유사한 문제가있는 사람이이 페이지를 찾을 수 있도록 몇 가지 설명과 함께 세부 정보입니다. 그러나 세부 사항을 신경 쓰지 않는다면 여기에 짧은 대답이 있습니다 .

다음과 같은 방식으로 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를 최소화한다는 것입니다 (이것은 분명히 많은 구현 세부 사항입니다. C 라이브러리, 적은 빈도의 IO를 통해 처리량이 최대화되도록 만들어졌습니다. 정기적으로 stdout을 플러시하도록 프로세스를 쉽게 수정할 수 있다면 이것이 해결책이 될 것입니다. 제 경우에는 블렌더 였기 때문에 저와 같은 완전한 멍청이가 소스를 수정하는 것이 약간 위협적이었습니다.

그러나 셸에서 이러한 프로세스를 실행하면 실시간으로 셸에 stdout을 표시하고 stdout이 버퍼링되지 않는 것 같습니다. 내가 믿는 다른 프로세스에서 호출 될 때만 버퍼링되지만 셸이 처리되는 경우 표준 출력은 버퍼링되지 않은 실시간으로 표시됩니다.

이 동작은 출력을 실시간으로 수집해야하는 하위 프로세스 인 Ruby 프로세스에서도 관찰 할 수 있습니다. 다음 행을 사용하여 random.rb 스크립트를 작성하십시오.

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

그런 다음 루비 스크립트를 호출하여 출력을 반환합니다.

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

You'll see that you don't get the result in real-time as you might expect, but all at once afterwards. STDOUT is being buffered, even though if you run random.rb yourself, it isn't buffered. This can be solved by adding a STDOUT.flush statement inside the block in random.rb. But if you can't change the source, you have to work around this. You can't flush it from outside the process.

If the subprocess can print to shell in real-time, then there must be a way to capture this with Ruby in real-time as well. And there is. You have to use the PTY module, included in ruby core I believe (1.8.6 anyways). Sad thing is that it's not documented. But I found some examples of use fortunately.

First, to explain what PTY is, it stands for pseudo terminal. Basically, it allows the ruby script to present itself to the subprocess as if it's a real user who has just typed the command into a shell. So any altered behavior that occurs only when a user has started the process through a shell (such as the STDOUT not being buffered, in this case) will occur. Concealing the fact that another process has started this process allows you to collect the STDOUT in real-time, as it isn't being buffered.

To make this work with the random.rb script as the child, try the following code:

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

use IO.popen. This is a good example.

Your code would become something like:

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

STDOUT.flush or STDOUT.sync = true


Blender probably doesn't print line-breaks until it is ending the program. Instead, it is printing the carriage return character (\r). The easiest solution is probably searching for the magic option which prints line-breaks with the progress indicator.

The problem is that IO#gets (and various other IO methods) use the line break as a delimiter. They will read the stream until they hit the "\n" character (which blender isn't sending).

Try setting the input separator $/ = "\r" or using blender.gets("\r") instead.

BTW, for problems such as these, you should always check puts someobj.inspect or p someobj (both of which do the same thing) to see any hidden characters within the string.


I don't know whether at the time ehsanul answered the question, there was Open3::pipeline_rw() available yet, but it really makes things simpler.

I don't understand ehsanul's job with Blender, so I made another example with tar and xz. tar will add input file(s) to stdout stream, then xz take that stdout and compress it, again, to another stdout. Our job is to take the last stdout and write it to our final file:

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

참고URL : https://stackoverflow.com/questions/1154846/continuously-read-from-stdout-of-external-process-in-ruby

반응형