在Ruby中连续读取外部进程的STDOUT

时间:2009-07-20 17:33:05

标签: ruby shell process stdout stdin

我想通过ruby脚本从命令行运行blender,然后逐行处理blender给出的输出以更新GUI中的进度条。搅拌机是我需要阅读的外部过程并不是很重要。

当blender进程仍在运行时,我似乎无法捕获blender正常打印到shell的进度消息,我尝试了几种方法。我似乎总是在 blender退出后访问blender 的标准输出,而不是在它仍在运行时。

以下是尝试失败的示例。它确实得到并打印了搅拌机输出的前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}

修改

为了使它更清晰一点,调用blender的命令会在shell中返回一个输出流,指示进度(第1-16部分已完成等)。似乎任何“获取”输出的调用都会被阻止,直到混合器退出。问题是如何在blender仍在运行时访问此输出,因为blender将其输出打印到shell。

6 个答案:

答案 0 :(得分:170)

我在解决我的这个问题上取得了一些成功。以下是详细信息和一些解释,以防有任何类似问题的人找到此页面。但如果您不关心细节,这里是简短的答案

以下列方式使用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,那么这将是您的解决方案。在我的情况下,这是一个搅拌器,所以有点像我自己修改源的完整菜鸟一样令人生畏。

但是当你从shell运行这些进程时,它们会实时向shell显示stdout,而stdout似乎没有被缓冲。从我相信的另一个进程调用它时它才被缓冲,但如果正在处理shell,则会实时看到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

你会发现你没有像你预期的那样实时得到结果,但之后就会立刻得到结果。 STDOUT正在被缓冲,即使你自己运行random.rb,它也不是缓冲的。这可以通过在random.rb中的块内添加STDOUT.flush语句来解决。但是,如果你不能改变来源,你必须解决这个问题。您无法从流程外部清除它。

如果子进程可以实时打印到shell,那么必须有一种方法可以使用Ruby实时捕获它。而且有。您必须使用包含在ruby核心中的PTY模块我相信(1.8.6反正)。可悲的是,它没有记录。但我幸运地找到了一些使用的例子。

首先,为了解释什么是PTY,它代表pseudo terminal。基本上,它允许ruby脚本将自己呈现给子进程,就好像它是一个刚刚将命令输入shell的真实用户。因此,只有当用户通过shell启动进程(例如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

答案 1 :(得分:12)

使用IO.popenThis就是一个很好的例子。

您的代码将变为:

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

答案 2 :(得分:5)

STDOUT.flush或 STDOUT.sync = true

答案 3 :(得分:4)

Blender可能不会在结束程序之前打印换行符。而是打印回车符(\ r)。最简单的解决方案可能是搜索使用进度指示器打印换行符的魔术选项。

问题是IO#gets(和其他各种IO方法)使用换行符作为分隔符。他们将读取流,直到他们点击“\ n”字符(混合器没有发送)。

尝试设置输入分隔符$/ = "\r"或使用blender.gets("\r")代替。

顺便说一句,对于这些问题,你应该经常检查puts someobj.inspectp someobj(两者都做同样的事情),看看字符串中是否有隐藏的字符。

答案 4 :(得分:0)

我不知道当时ehsanul回答了这个问题,还有Open3::pipeline_rw()可用,但这确实让事情更简单。

我不理解ehsanul在Blender的工作,所以我用tarxz做了另一个例子。 tar会将输入文件添加到stdout流,然后xzstdout再次压缩到另一个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

答案 5 :(得分: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,以完全取消缓冲。

但这并不总是有效的:有时目标进程会强制执行自己的流缓冲区类型,就像指出的另一个答案一样。