Leer continuamente desde STDOUT del proceso externo en Ruby

86

Quiero ejecutar blender desde la línea de comando a través de un script ruby, que luego procesará la salida dada 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 captar los mensajes de progreso que blender normalmente imprime en el shell cuando el proceso de Blender todavía se está ejecutando, y he probado algunas formas. Parece que siempre accedo a la salida estándar de Blender después de que Blender se ha cerrado, no mientras aún está funcionando.

Este es un ejemplo de un intento 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 a blender devuelve un flujo de salida en el shell, lo que indica el progreso (parte 1-16 completada, etc.). Parece que cualquier llamada a "obtiene" la salida se bloquea hasta que se cierra Blender. El problema es cómo obtener acceso a esta salida mientras Blender aún se está ejecutando, ya que Blender imprime su salida en el shell.

ehsanul
fuente

Respuestas:

175

He tenido cierto éxito resolviendo 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 interesan los detalles, aquí está la respuesta corta :

Utilice 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 aquí está la respuesta larga , con demasiados detalles:

El problema real parece ser que si un proceso no vacía explícitamente su stdout, entonces todo lo que se escribe en stdout se almacena en búfer en lugar de enviarse realmente, hasta que se realiza el proceso, para minimizar IO (esto aparentemente es un detalle de implementación de muchos Bibliotecas C, creadas para maximizar el rendimiento mediante IO menos frecuentes). Si puede modificar fácilmente el proceso para que se descargue con regularidad, entonces esa sería su solución. En mi caso, era blender, por lo que un poco intimidante para un novato como yo, modificar la fuente.

Pero cuando ejecuta estos procesos desde el shell, muestran stdout al shell en tiempo real, y stdout no parece estar almacenado en búfer. Creo que solo se almacena en búfer cuando se llama desde otro proceso, pero si se está tratando con un shell, la salida estándar se ve en tiempo real, sin búfer.

Este comportamiento se puede observar incluso con un proceso ruby ​​como proceso hijo cuya salida debe recopilarse en tiempo real. Simplemente cree un script, random.rb, con la siguiente línea:

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

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

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

Verá que no obtiene el resultado en tiempo real como cabría esperar, sino todo a la vez después. STDOUT se almacena en búfer, aunque si ejecuta random.rb usted mismo, no se almacena en búfer. Esto se puede resolver agregando una STDOUT.flushdeclaración dentro del bloque en random.rb. Pero si no puede cambiar la fuente, debe solucionarlo. No puede vaciarlo desde fuera del proceso.

Si el subproceso puede imprimir en shell en tiempo real, entonces debe haber una forma de capturar esto con Ruby también en tiempo real. 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, afortunadamente, encontré algunos ejemplos de uso.

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

Para que esto funcione con el script random.rb como hijo, pruebe 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
ehsanul
fuente
7
Esto es genial, pero creo que los parámetros de los bloques stdin y stdout deberían intercambiarse. Ver: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro
1
¿Cómo cerrar el pty? ¿Matar al pid?
Boris B.
Respuesta impresionante. Me ayudaste a mejorar mi script de despliegue de rake para heroku. Es 'git push' pantallas de registro en tiempo real y aborta tarea si 'fatal:' encontraron gist.github.com/sseletskyy/9248357
Serge Seletskyy
1
Originalmente intenté usar este método pero 'pty' no está disponible en Windows. Resulta que STDOUT.sync = truees todo lo que se necesita (la respuesta de mveerman a continuación). Aquí hay otro hilo con un código de ejemplo .
Pakman
12

utilizar IO.popen. Este es un buen ejemplo.

Tu código se convertiría en algo 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
Sinan Taifour
fuente
He probado esto. El problema es el mismo. Tengo acceso a la salida después. Creo que IO.popen comienza ejecutando el primer argumento como un comando y espera a que termine. En mi caso, la salida la da Blender mientras Blender todavía está procesando. Y luego se invoca el bloqueo después, lo que no me ayuda.
ehsanul
Esto es lo que probé. Devuelve la salida después de que Blender está listo: IO.popen ("blender -b mball.blend // renderiza / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| línea | pone línea; salida + = línea;} fin
ehsanul
3
No estoy seguro de qué está sucediendo en su caso. Probé el código anterior con yesuna aplicación de línea de comandos que nunca termina , y funcionó. El código fue de la siguiente manera: IO.popen('yes') { |p| p.each { |f| puts f } }. Sospecho que es algo que tiene que ver con Blender y no con ruby. Probablemente la licuadora no siempre descarga su STDOUT.
Sinan Taifour
Ok, acabo de probarlo con un proceso de rubí externo para probar, y tienes razón. Parece ser un problema de licuadora. Gracias por la respuesta de todos modos.
ehsanul
Resulta que, después de todo, hay una manera de obtener la salida a través de ruby, aunque Blender no vacía su salida estándar. Detalles en breve en una respuesta separada, en caso de que esté interesado.
ehsanul
6

STDOUT.flush o STDOUT.sync = verdadero

mveerman
fuente
sí, esta fue una respuesta poco convincente. Tu respuesta fue mejor.
mveerman
¡No cojo! Trabajó para mi.
Clay Bridges
Más precisamente:STDOUT.sync = true; system('<whatever-command>')
caram
4

Blender probablemente no imprime saltos de línea hasta que finaliza el programa. En su lugar, está imprimiendo el carácter de retorno de carro (\ r). La solución más sencilla probablemente sea 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 de E / S) utilizan el salto de línea como delimitador. Leerán la transmisión hasta que lleguen al carácter "\ n" (que Blender no está enviando).

Intente configurar el separador de entrada $/ = "\r"o utilice en su blender.gets("\r")lugar.

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

hhaamu
fuente
1
Acabo de inspeccionar la salida dada, y parece que Blender usa un salto de línea (\ n), así que ese no era el problema. De todos modos, gracias por el consejo, lo tendré en cuenta la próxima vez que depure algo como esto.
ehsanul
0

No sé si en ese momento ehsanul respondió la pregunta, ya estaba Open3::pipeline_rw()disponible, pero realmente simplifica las cosas.

No entiendo el trabajo de ehsanul con Blender, así que hice otro ejemplo con tary xz. taragregará archivo (s) de entrada al flujo de salida estándar, luego lo xztomará stdouty lo comprimirá, nuevamente, en otra salida estándar . Nuestro trabajo es tomar la última salida estándar y escribirla 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
condichoso
fuente
0

Pregunta antigua, pero tenía problemas similares.

Sin realmente cambiar mi código Ruby, una cosa que ayudó fue envolver mi tubería con stdbuf , así:

cmd = "stdbuf -oL -eL -i0  openssl s_client -connect #{xAPI_ADDRESS}:#{xAPI_PORT}"

@xSess = IO.popen(cmd.split " ", mode = "w+")  

En mi ejemplo, el comando real con el que quiero interactuar como si fuera un shell es openssl .

-oL -eL dígale que almacene STDOUT y STDERR solo hasta una nueva línea. Reemplazar Lcon 0para eliminar el búfer por completo.

Sin embargo, esto no siempre funciona: a veces, el proceso de destino impone su propio tipo de búfer de flujo, como se señaló en otra respuesta.

Marcos
fuente