不断从Ruby中的外部进程的STDOUT中读取


86

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

当Blender进程仍在运行时,我似乎无法捕获Blender通常打印到Shell的进度消息,并且我尝试了几种方法。我似乎总是搅拌器退出后(而不是在它仍在运行时)访问搅拌器的标准输出。

这是失败尝试的示例。它确实获取并打印了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}

编辑:

为了更清楚一点,调用搅拌器的命令在shell中返回输出流,指示进度(第1-16部分已完成,等等)。似乎所有对“获取”输出的调用都被阻止,直到搅拌机退出为止。问题是当Blender仍在运行时如何访问此输出,因为Blender会将其输出打印到Shell。

Answers:


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似乎没有被缓冲。我相信只有从另一个进程调用时,它才会被缓冲,但是如果要处理一个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也不缓冲。这可以通过STDOUT.flush在random.rb中的块内添加一条语句来解决。但是,如果您无法更改源,则必须解决此问题。您不能从过程外部刷新它。

如果子进程可以实时打印到shell,那么还必须有一种使用Ruby实时捕获它的方法。还有。我必须使用红宝石内核中包含的PTY模块(无论如何仍为1.8.6)。可悲的是,它没有记录在案。但是我幸运地找到了一些使用的例子。

首先,解释什么是PTY,它代表伪终端。基本上,它允许ruby脚本将自身呈现给子进程,就好像它是在命令行中键入命令的真实用户一样。因此,仅当用户通过外壳启动进程时(例如,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/...
麦克科尼利亚罗

1
如何关闭pty?杀死pid?
Boris B.

很棒的答案。您帮助我改善了Heroku的rake部署脚本。它会实时显示“ git push”日志,如果发现“致命:”则中止任务gist.github.com/sseletskyy/9248357
Serge Seletskyy 2014年

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首先将第一个参数作为命令运行,然后等待它结束。在我的情况下,输出是由Blender给出的,而Blender仍在处理中。然后在之后调用该块,这对我没有帮助。
ehsanul

这是我尝试过的。混合器完成后,它返回输出:IO.popen(“ blender -b mball.blend // renders / -F JPEG -x 1 -f 1”,“ w +”)do | blender | blender.each {| line | 放线 输出+ =行;}结束
ehsanul,2009年

3
我不确定您的情况如何。我测试的代码上面yes,一个命令行应用程序,从未结束,和它的工作。代码如下:IO.popen('yes') { |p| p.each { |f| puts f } }。我怀疑这与搅拌器有关,与红宝石无关。可能Blender并不总是刷新其STDOUT。
思南大佛

好的,我只是通过外部红宝石流程对其进行了测试,您是对的。似乎是一个搅拌器问题。无论如何,谢谢您的回答。
ehsanul

事实证明,即使Blender不刷新其stdout,也毕竟有一种方法可以通过ruby获取输出。如果您有兴趣,请稍后在单独的答案中提供详细信息。
ehsanul

6

STDOUT.flush或STDOUT.sync = true


是的,这是一个me脚的答案。您的回答更好。
mveerman

不la脚!为我工作。
粘土桥

更确切地说:STDOUT.sync = true; system('<whatever-command>')
caram

4

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

问题在于IO#gets(以及其他各种IO方法)将换行符用作分隔符。他们将读取流,直到点击“ \ n”字符(搅拌器未发送)。

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

顺便说一句,对于诸如此类的问题,您应始终检查puts someobj.inspectp someobj(两者都执行相同的操作)以查看字符串中的任何隐藏字符。


1
我只是检查了给定的输出,看来Blender使用了换行符(\ n),所以这不是问题。无论如何,感谢您的提示,下次我调试此类内容时,我会记住这一点。
ehsanul

0

我不知道ehsanul当时是否回答了这个问题,但确实Open3::pipeline_rw()可用,但这确实使事情变得更简单。

我不了解ehsanul在Blender的工作,所以我用tar和举了另一个例子xztar将输入文件添加到stdout流,然后xz将其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+")  

在我的示例中,我想与之交互的实际命令就像是一个shell,是openssl

-oL -eL 告诉它仅将STDOUT和STDERR缓冲到换行符。更换L0完全无缓冲。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.