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


답변

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


답변

STDOUT.flush 또는 STDOUT.sync = true


답변

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

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

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

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


답변

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


답변

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

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하려면로 교체하십시오 .

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


답변