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


86

명령 줄에서 루비 스크립트를 통해 블렌더를 실행하고 싶습니다. 그러면 블렌더가 제공하는 출력을 한 줄씩 처리하여 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"하는 모든 호출이 차단 된 것 같습니다. 문제는 블렌더가 셸에 출력을 출력하므로 블렌더가 여전히 실행 중일 때이 출력에 액세스하는 방법입니다.

답변:


175

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

다음과 같은 방식으로 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이 버퍼링되지 않는 것 같습니다. 내가 믿는 다른 프로세스에서 호출 될 때만 버퍼링되지만 쉘이 처리되는 경우 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

예상 한대로 실시간으로 결과를 얻지 못했지만 나중에 모두 한꺼번에 얻을 수 있습니다. random.rb를 직접 실행하더라도 STDOUT은 버퍼링되고 있지만 버퍼링되지는 않습니다. 이것은 STDOUT.flushrandom.rb의 블록 안에 문 을 추가하여 해결할 수 있습니다 . 그러나 소스를 변경할 수 없으면이 문제를 해결해야합니다. 프로세스 외부에서 플러시 할 수 없습니다.

하위 프로세스가 실시간으로 쉘에 인쇄 할 수 있다면 Ruby로 실시간으로이를 캡처 할 수있는 방법이 있어야합니다. 그리고 있습니다. 내가 믿는 루비 코어 (1.8.6 어쨌든)에 포함 된 PTY 모듈을 사용해야합니다. 슬픈 것은 문서화되지 않았다는 것입니다. 하지만 다행히도 몇 가지 사용 사례를 찾았습니다.

먼저 PTY가 무엇인지 설명하기 위해 의사 터미널을 의미합니다 . 기본적으로 루비 스크립트는 마치 쉘에 명령을 입력 한 실제 사용자 인 것처럼 하위 프로세스에 자신을 표시 할 수 있습니다. 따라서 사용자가 셸을 통해 프로세스를 시작한 경우에만 발생하는 변경된 동작 (예 :이 경우에는 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

7
이것은 훌륭하지만 stdin과 stdout 블록 매개 변수를 바꿔야한다고 생각합니다. 참조 : ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/...
마이크 Conigliaro

1
pty를 닫는 방법? PID를 죽여?
Boris B.

멋진 대답입니다. Heroku의 레이크 배포 스크립트를 개선하는 데 도움을 주셨습니다. 실시간으로 'git push'로그를 표시하고 'fatal :'이 발견되면 작업을 중단합니다. gist.github.com/sseletskyy/9248357
Serge Seletskyy

1
원래이 방법을 사용하려고했지만 Windows에서는 'pty'를 사용할 수 없습니다. 결과적으로 STDOUT.sync = true필요한 것은 모두입니다 (아래 mveerman의 답변). 다음은 몇 가지 예제 코드가있는 또 다른 스레드입니다 .
Pakman

12

를 사용하십시오 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

나는 이것을 시도했다. 문제는 동일합니다. 나중에 출력에 액세스 할 수 있습니다. IO.popen은 첫 번째 인수를 명령으로 실행하여 시작하고 끝날 때까지 기다립니다. 제 경우에는 블렌더가 처리하는 동안 블렌더가 출력을 제공합니다. 그런 다음 블록이 호출되어 도움이되지 않습니다.
ehsanul

내가 시도한 것이 있습니다. 블렌더가 완료된 후 출력을 반환합니다. IO.popen ( "blender -b mball.blend // renders / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| line | 라인을 넣습니다. output + = line;} end
ehsanul

3
귀하의 사건에서 무슨 일이 일어나고 있는지 잘 모르겠습니다. yes절대 끝나지 않는 명령 줄 응용 프로그램 인을 사용하여 위의 코드를 테스트 했으며 제대로 작동했습니다. 코드는 다음과 같습니다 IO.popen('yes') { |p| p.each { |f| puts f } }.. 나는 그것이 루비가 아니라 블렌더와 관련이 있다고 생각합니다. 아마도 블렌더가 항상 STDOUT을 플러시하지는 않습니다.
Sinan Taifour

좋아, 방금 테스트하기 위해 외부 루비 프로세스로 시도했는데 맞습니다. 믹서기 문제인 것 같습니다. 어쨌든 답변 주셔서 감사합니다.
ehsanul

블렌더가 표준 출력을 플러시하지 않더라도 결국 루비를 통해 출력을 얻는 방법이 있음이 밝혀졌습니다. 관심이 있으시면 별도의 답변에 곧 자세히 설명하십시오.
ehsanul

6

STDOUT.flush 또는 STDOUT.sync = true


예, 이것은 절름발이 대답이었습니다. 당신의 대답은 더 좋았습니다.
mveerman

절름발이가 아닙니다! 나를 위해 일했습니다.
Clay Bridges

더 정확하게 :STDOUT.sync = true; system('<whatever-command>')
caram

4

Blender는 프로그램이 끝날 때까지 줄 바꿈을 인쇄하지 않을 것입니다. 대신 캐리지 리턴 문자 (\ r)를 인쇄합니다. 가장 쉬운 해결책은 진행률 표시기로 줄 바꿈을 인쇄하는 마법 옵션을 검색하는 것입니다.

문제는 IO#gets(및 기타 다양한 IO 방법) 줄 바꿈을 구분 기호로 사용한다는 것입니다. 그들은 "\ n"문자 (블렌더가 보내지 않는)를 칠 때까지 스트림을 읽습니다.

입력 구분자를 설정 $/ = "\r"하거나 blender.gets("\r")대신 사용하십시오.

BTW, 이와 같은 문제의 경우 항상 puts someobj.inspect또는 p someobj(둘 다 동일한 작업을 수행함) 문자열 내 숨겨진 문자를 확인해야합니다.


1
방금 주어진 출력을 검사했는데 블렌더가 줄 바꿈 (\ n)을 사용하는 것 같으므로 문제가되지 않았습니다. 어쨌든 팁을 주셔서 감사합니다. 다음에 이와 같은 것을 디버깅 할 때 명심할 것입니다.
ehsanul

0

ehsanul이 질문에 대답했을 때 Open3::pipeline_rw()아직 사용 가능한지 여부는 모르겠지만 실제로 일을 더 간단하게 만듭니다.

블렌더에서 ehsanul의 역할을 이해하지 못해서 tarxz. tar입력 파일을 stdout 스트림에 추가 한 다음 xz이 를 가져 와서 stdout다른 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

오래된 질문이지만 비슷한 문제가있었습니다.

Ruby 코드를 실제로 변경하지 않고 도움이 된 것은 파이프를 stdbuf 로 래핑하는 것입니다 .

cmd = "stdbuf -oL -eL -i0  openssl s_client -connect #{xAPI_ADDRESS}:#{xAPI_PORT}"

@xSess = IO.popen(cmd.split " ", mode = "w+")  

내 예에서 쉘처럼 상호 작용하려는 실제 명령은 openssl 입니다.

-oL -eL STDOUT 및 STDERR을 개행까지만 버퍼링하도록 지시하십시오. 완전히 버퍼링 L을 해제 0하려면로 교체하십시오 .

그래도 항상 작동하는 것은 아닙니다. 때로는 대상 프로세스가 다른 답변이 지적한 것처럼 자체 스트림 버퍼 유형을 적용합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.