2011-08-19 6 views
5

Dado que deseo probar lecturas sin bloqueo desde un comando largo, creé el siguiente script, lo guardé como long, lo hice ejecutable con chmod 755, y lo coloqué en mi ruta (guardado como ~/bin/long donde está en mi ruta ~/bin)¿Por qué IO :: WaitReadable se plantea de forma diferente para STDOUT que STDERR?

Estoy en una variante * nix con ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin11.0.0] compilado con los valores predeterminados RVM. No uso Windows, y por lo tanto no estoy seguro si el script de prueba funcionará para usted si lo hace.

#!/usr/bin/env ruby 

3.times do 
    STDOUT.puts 'message on stdout' 
    STDERR.puts 'message on stderr' 
    sleep 1 
end 

¿Por qué long_err producen cada mensaje STDERR ya que es impreso por "largo"

def long_err(bash_cmd = 'long', maxlen = 4096) 
    stdin, stdout, stderr = Open3.popen3(bash_cmd) 
    begin 
    begin 
     puts 'err -> ' + stderr.read_nonblock(maxlen) 
    end while true 
    rescue IO::WaitReadable 
    IO.select([stderr]) 
    retry 
    rescue EOFError 
    puts 'EOF' 
    end 
end 

mientras long_out permanece bloqueado hasta que se imprimen todos los mensajes STDOUT?

def long_out(bash_cmd = 'long', maxlen = 4096) 
    stdin, stdout, stderr = Open3.popen3(bash_cmd) 
    begin 
    begin 
     puts 'out -> ' + stdout.read_nonblock(maxlen) 
    end while true 
    rescue IO::WaitReadable 
    IO.select([stdout]) 
    retry 
    rescue EOFError 
    puts 'EOF' 
    end 
end 

Asumo que se require 'open3' antes de probar cualquiera de las funciones.

¿Por qué IO::WaitReadable se plantea de forma diferente para STDOUT que STDERR?

También se agradecieron las soluciones provisionales usando other ways to start subprocesses si las tiene.

Respuesta

4

En la mayoría de los SO, STDOUT es con buffer mientras que STDERR no lo está. Lo que popen3 hace es básicamente abrir un conducto entre el ejecutable ejecutable y Ruby.

Cualquier salida que está en modo tamponada no se envía a través de este tubo hasta que:

  1. El tampón se llena (forzando de este modo una escalera).
  2. La aplicación emisora ​​finaliza (se alcanza EOF, forzando un color).
  3. La secuencia se vacía explícitamente.

El motivo por el que STDERR no se almacena temporalmente es que generalmente se considera importante que los mensajes de error aparezcan de forma instantánea, en lugar de buscar la eficacia mediante el almacenamiento en búfer.

Así que, sabiendo esto, se puede emular el comportamiento STDERR con STDOUT como esto:

#!/usr/bin/env ruby 

3.times do 
    STDOUT.puts 'message on stdout' 
    STDOUT.flush 
    STDERR.puts 'message on stderr' 
    sleep 1 
end 

y verá la diferencia.

Es posible que también desee marcar "Understanding Ruby and OS I/O buffering".

+0

Gracias Casper, se votará tan pronto como llegue a 15 rep. –

0

Aquí está lo mejor que tengo hasta ahora para iniciar subprocesos. Lancé muchos comandos de red, así que necesitaba una forma de desfasarlos si tardan demasiado en volver. Esto debería ser útil en cualquier situación en la que desee mantener el control de su ruta de ejecución.

Adapté esto desde un Gist, añadiendo código para probar el estado de salida del comando de 3 resultados:

  1. La conclusión con éxito (estado de salida 0)
  2. finalización de error (estado de salida es distinto de cero) - plantea una excepción
  3. Comando tiempo de espera y se mató - plantea una excepción

también se ha corregido una condición de carrera, los parámetros simplificados, añade un poco más com y código de depuración añadido para ayudarme a comprender qué estaba sucediendo con las salidas y las señales.

Llame a la función como esta:

output = run_with_timeout("command that might time out", 15) 

salida contendrá la salida estándar combinada y stderr del comando si se completa con éxito. Si el comando no se completa en 15 segundos, se eliminará y se generará una excepción.

Aquí es la función (2 constantes Tendrá que definirse en la parte superior):

DEBUG = false  # change to true for some debugging info 
BUFFER_SIZE = 4096 # in bytes, this should be fine for many applications 

def run_with_timeout(command, timeout) 
    output = '' 
    tick = 1 
    begin 
    # Start task in another thread, which spawns a process 
    stdin, stderrout, thread = Open3.popen2e(command) 
    # Get the pid of the spawned process 
    pid = thread[:pid] 
    start = Time.now 

    while (Time.now - start) < timeout and thread.alive? 
     # Wait up to `tick' seconds for output/error data 
     Kernel.select([stderrout], nil, nil, tick) 
     # Try to read the data 
     begin 
     output << stderrout.read_nonblock(BUFFER_SIZE) 
     puts "we read some data..." if DEBUG 
     rescue IO::WaitReadable 
     # No data was ready to be read during the `tick' which is fine 
     print "."  # give feedback each tick that we're waiting 
     rescue EOFError 
     # Command has completed, not really an error... 
     puts "got EOF." if DEBUG 
     # Wait briefly for the thread to exit... 
     # We don't want to kill the process if it's about to exit on its 
     # own. We decide success or failure based on whether the process 
     # completes successfully. 
     sleep 1 
     break 
     end 
    end 

    if thread.alive? 
     # The timeout has been reached and the process is still running so 
     # we need to kill the process, because killing the thread leaves 
     # the process alive but detached. 
     Process.kill("TERM", pid) 
    end 

    ensure 
    stdin.close if stdin 
    stderrout.close if stderrout 
    end 

    status = thread.value   # returns Process::Status when process ends 

    if DEBUG 
    puts "thread.alive?: #{thread.alive?}" 
    puts "status: #{status}" 
    puts "status.class: #{status.class}" 
    puts "status.exited?: #{status.exited?}" 
    puts "status.exitstatus: #{status.exitstatus}" 
    puts "status.signaled?: #{status.signaled?}" 
    puts "status.termsig: #{status.termsig}" 
    puts "status.stopsig: #{status.stopsig}" 
    puts "status.stopped?: #{status.stopped?}" 
    puts "status.success?: #{status.success?}" 
    end 

    # See how process ended: .success? => true, false or nil if exited? !true 
    if status.success? == true  # process exited (0) 
    return output 
    elsif status.success? == false # process exited (non-zero) 
    raise "command `#{command}' returned non-zero exit status (#{status.exitstatus}), see below output\n#{output}" 
    elsif status.signaled?   # we killed the process (timeout reached) 
    raise "shell command `#{command}' timed out and was killed (timeout = #{timeout}s): #{status}" 
    else 
    raise "process didn't exit and wasn't signaled. We shouldn't get to here." 
    end 
end 

Hope esto es útil.

Cuestiones relacionadas