Lectura continua de STDOUT del proceso externo en Ruby

Quiero ejecutar Blender desde la línea de comandos a través de un script de ruby, que luego procesará la salida proporcionada por Blender línea por línea para actualizar una barra de progreso en una GUI. No es realmente importante que Blender sea el proceso externo cuya salida estándar necesito leer.

Parece que no puedo ver los mensajes de progreso que la licuadora normalmente imprime en el intérprete de órdenes cuando el proceso de la licuadora todavía se está ejecutando, y lo he intentado de varias maneras. Siempre parece que accedo a la salida estándar de Blender después de que blender se haya apagado, no mientras todavía esté funcionando.

Aquí hay un ejemplo de un bash fallido. Obtiene e imprime las primeras 25 líneas de la salida de Blender, pero solo después de que el proceso de Blender haya finalizado:

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} 

Editar:

Para hacerlo un poco más claro, el comando que invoca blender devuelve una secuencia de salida en el shell, indicando progreso (parte 1-16 completada, etc.). Parece que cualquier llamada para “obtener” la salida está bloqueada hasta que se cierre el mezclador. El problema es cómo obtener acceso a esta salida mientras blender aún se está ejecutando, ya que Blender imprime su salida al shell.

He tenido cierto éxito al resolver este problema mío. Aquí están los detalles, con algunas explicaciones, en caso de que alguien que tenga un problema similar encuentre esta página. Pero si no le importan los detalles, aquí está la respuesta breve :

Use PTY.spawn de la siguiente manera (con su propio comando por supuesto):

 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 

Y esta es la respuesta larga , con demasiados detalles:

El problema real parece ser que si un proceso no limpia explícitamente su salida estándar, todo lo escrito en stdout se almacena en búfer en lugar de enviarse realmente, hasta que el proceso finalice, a fin de minimizar IO (esto es aparentemente un detalle de implementación de muchos Bibliotecas C, hechas para maximizar el rendimiento mediante IO menos frecuentes). Si puede modificar fácilmente el proceso para que elimine el stdout de manera regular, esa sería su solución. En mi caso, fue licuadora, por lo que un poco intimidante para un novato completo como yo para modificar la fuente.

Pero cuando ejecuta estos procesos desde el shell, muestran stdout en el shell en tiempo real, y el stdout no parece estar almacenado. Solo se almacena en el búfer cuando lo llaman desde otro proceso, creo, pero si se trata un shell, el stdout se ve en tiempo real, sin búfer.

Este comportamiento incluso se puede observar con un proceso de ruby ​​como proceso secundario cuyo resultado se debe recostackr en tiempo real. Solo crea una secuencia de comandos, random.rb, con la siguiente línea:

 5.times { |i| sleep( 3*rand ); puts "#{i}" } 

Luego, un script de ruby ​​para llamarlo y devolver su resultado:

 IO.popen( "ruby random.rb") do |random| random.each { |line| puts line } end 

Verás que no obtienes el resultado en tiempo real como es de esperar, pero todo de una vez después. STDOUT se está almacenando en búfer, aunque si ejecuta random.rb usted mismo, no se almacena en el búfer. Esto se puede resolver agregando una instrucción STDOUT.flush dentro del bloque en random.rb. Pero si no puedes cambiar la fuente, tienes que evitar esto. No puedes tirarlo fuera del proceso.

Si el subproceso se puede imprimir en shell en tiempo real, debe haber una forma de capturar esto con Ruby en tiempo real también. Y ahí está. Tienes que usar el módulo PTY, incluido en ruby ​​core, creo (1.8.6 de todos modos). Lo triste es que no está documentado. Pero encontré algunos ejemplos de uso afortunadamente.

Primero, para explicar qué es PTY, significa pseudo terminal . Básicamente, permite que el script de ruby ​​se presente al subproceso como si fuera un usuario real que acaba de escribir el comando en un shell. Por lo tanto, cualquier comportamiento alterado que ocurra solo cuando un usuario haya iniciado el proceso a través de un intérprete de comandos (como el STDOUT que no se almacena en el búfer, en este caso) ocurrirá. Ocultar el hecho de que otro proceso ha comenzado este proceso le permite recostackr el STDOUT en tiempo real, ya que no se está almacenando.

Para que esto funcione con el script random.rb como elemento secundario, intente con el siguiente código:

 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 

usa IO.popen . Este es un buen ejemplo.

Tu código sería algo así como:

 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 o STDOUT.sync = true

Blender probablemente no imprime saltos de línea hasta que finaliza el progtwig. En su lugar, está imprimiendo el carácter de retorno de carro (\ r). La solución más fácil es, probablemente, buscar la opción mágica que imprime saltos de línea con el indicador de progreso.

El problema es que IO#gets (y varios otros métodos IO) usan el salto de línea como delimitador. Leerán la transmisión hasta que toquen el carácter “\ n” (que blender no está enviando).

Intenta configurar el separador de entrada $/ = "\r" o usando blender.gets("\r") lugar.

Por cierto, para problemas como estos, siempre debe verificar puts someobj.inspect o p someobj (ambos hacen lo mismo) para ver los caracteres ocultos dentro de la cadena.

No sé si en el momento en que ehsanul respondió la pregunta, todavía estaba Open3::pipeline_rw() disponible, pero realmente hace las cosas más simples.

No entiendo el trabajo de ehsanul con Blender, así que hice otro ejemplo con tar y xz . tar agregará los archivos de entrada a la ruta estándar, luego xz tomará esa salida stdout y la comprimirá, nuevamente, a otra salida estándar. Nuestro trabajo es tomar el último estándar y escribirlo en nuestro archivo final:

 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