2011-08-19 12 views
5

Biorąc pod uwagę, że chcę przetestować niezablokowane odczyty z długiego polecenia, utworzyłem następujący skrypt, zapisałem go jako long, uczyniłem go wykonywalnym z chmod 755 i umieściłem go na mojej ścieżce (zapisałem jako ~/bin/long, gdzie ~/bin jest na mojej ścieżce).Dlaczego IO :: WaitReadable jest inaczej podnoszone dla STDOUT niż STDERR?

Jestem w wersji * nix z ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin11.0.0] skompilowanej z domyślnymi wartościami RVM. Nie używam systemu Windows i dlatego nie jestem pewien, czy skrypt testowy będzie działał dla ciebie, jeśli to zrobisz.

#!/usr/bin/env ruby 

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

Dlaczego long_err produkować każdą wiadomość stderr jak to jest wydrukowane przez „długi”

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 

podczas long_out pozostaje zablokowany, aż wszystkie komunikaty stdout są drukowane?

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 

Zakładam, że przed wykonaniem którejś z tych funkcji uzyskasz require 'open3'.

Dlaczego wartość IO::WaitReadable jest podnoszona inaczej dla STDOUT niż STDERR?

Obejścia z zastosowaniem other ways to start subprocesses również doceniane, jeśli je masz.

Odpowiedz

4

W większości systemów operacyjnych STDOUT to buforowany, podczas gdy STDERR nie jest. To, co robi popen3, to w zasadzie otworzyć potok pomiędzy wersją Exeutable a Ruby.

Każdy wyjściowy jest w trybie buforowanej nie jest wysyłany przez tę rurę aż do:

  1. bufor jest napełniany (wymuszając w ten sposób kolor).
  2. Aplikacja wysyłająca kończy działanie (EOF zostaje osiągnięty, wymuszenie spłukiwania).
  3. Strumień jest wyraźnie przepłukany.

Powód, dla którego STDERR nie jest buforowany, jest zwykle uważany za ważny, aby komunikaty o błędach pojawiały się natychmiast, a nie dla wydajności poprzez buforowanie.

Tak, wiedząc o tym, można naśladować zachowanie stderr z STDOUT tak:

#!/usr/bin/env ruby 

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

i widać różnicę.

Możesz również sprawdzić "Understanding Ruby and OS I/O buffering".

+0

dziękuję Casper, rozpocznie się, jak tylko dotrę do 15 przedstawicieli –

0

Oto najlepsze, jakie udało mi się uzyskać do uruchomienia podprocesów. Wprowadzam wiele poleceń sieciowych, więc potrzebowałem sposobu na ich wygaśnięcie, jeśli zbyt długo będą wracać. To powinno być przydatne w każdej sytuacji, w której chcesz zachować kontrolę nad swoją ścieżką wykonania.

Przystosowałem to z Gist dodanie kodu do badania stanu wyjścia polecenia 3 rezultaty:

  1. Ukończenie (stan wyjście 0)
  2. realizacji błędu (stan wyjścia jest niezerowy) - podnosi wyjątek
  3. poleceń limit czasu i został zabity - podnosi wyjątek

Poprawiono również warunek wyścigu, uproszczone parametry, dodał jeszcze kilka com i dodałem kod debugowania, aby pomóc mi zrozumieć, co się dzieje z wyjściami i sygnałami.

Wywołać funkcję tak:

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

wyjściowy będzie zawierać łączną STDOUT i STDERR polecenia, jeśli zakończy się pomyślnie. Jeśli polecenie nie zakończy się w ciągu 15 sekund, zostanie ono zabite, a wyjątek podniesiony.

Oto funkcja (2 stałe trzeba określić na górze):

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 

Nadzieja jest to przydatne.

Powiązane problemy