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

我想通过ruby脚本从命令行运行blender,ruby脚本将逐行处理由blender提供的输出,以更新GUI中的进度条。 搅拌机是我需要读取的标准输出的外部过程并不重要。

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

这是一个失败尝试的例子。 它确实获得并打印搅拌机输出的前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中返回一个输出stream,指示进度(部分1-16完成等)。 似乎任何调用“获取”输出都被阻止,直到搅拌器退出。 问题是如何在搅拌机仍在运行时访问此输出,因为搅拌机将其输出到shell。

我在解决这个问题上取得了一些成功。 这里有一些细节和一些解释,以防万一有类似问题的人发现这个页面。 但是,如果你不关心细节,下面是简短的答案

按照以下方式使用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库,通过较less的IO来实现吞吐量最大化)。 如果您可以轻松修改该过程,以便定期刷新stdout,那么这将是您的解决scheme。 在我的情况下,这是搅拌机,所以有点恐吓一个完整的noob,如我自己修改来源。

但是,当你从shell运行这些进程时,它们实时向shell显示stdout,而stdout似乎没有被缓冲。 只有在我相信从另一个进程调用时才被缓冲,但是如果正在处理一个shell,那么标准输出将被实时看到,无缓冲。

这个行为甚至可以被视为一个ruby进程作为其输出必须实时收集的subprocess。 只需使用以下行创build一个脚本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语句来解决。 但是,如果你不能改变来源,你必须解决这个问题。 你不能从stream程外部冲洗它。

如果subprocess能够实时打印到shell,那么就必须有一种方法来实时捕获Ruby。 还有。 你必须使用包含在ruby核心的PTY模块,我相信(1.8.6反正)。 可悲的是,它没有logging。 但是我幸运地find了一些使用的例子。

首先解释一下PTY是什么,代表伪terminal 。 基本上,它允许ruby脚本将自己展现给subprocess,就好像它是一个真正的用户,只是将命令input到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 

使用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

搅拌器可能不打印换行符,直到程序结束。 而是打印回车符(\ r)。 最简单的解决scheme可能是寻找魔术选项,用进度指示器打印换行符。

问题是IO#gets (和其他各种IO方法)使用换行符作为分隔符。 他们将读取stream,直到他们击中“\ n”字符(搅拌器不发送)。

尝试设置input分隔符$/ = "\r"或使用blender.gets("\r")

顺便说一句,对于这样的问题,你应该总是检查puts someobj.inspectp someobj (都做同样的事情)来查看string中的任何隐藏的字符。

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

我不明白ehsanul在Blender的工作,所以我用tarxz做了另一个例子。 tar将input文件添加到标准输出stream,然后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