2013-06-21 15 views
12

Es ist am einfachsten in Code zu erklären:Timeout innerhalb eines Popen funktioniert, aber Popen innerhalb eines Timeout nicht?

require 'timeout' 

puts "this block will properly kill the sleep after a second" 

IO.popen("sleep 60") do |io| 
    begin 
    Timeout.timeout(1) do 
     while (line=io.gets) do 
     output += line 
     end 
    end 
    rescue Timeout::Error => ex 
    Process.kill 9, io.pid 
    puts "timed out: this block worked correctly" 
    end 
end 

puts "but this one blocks for >1 minute" 

begin 
    pid = 0 
    Timeout.timeout(1) do 
    IO.popen("sleep 60") do |io| 
     pid = io.pid 
     while (line=io.gets) do 
     output += line 
     end 
    end 
    end 
rescue Timeout::Error => ex 
    puts "timed out: the exception gets thrown, but much too late" 
end 

Mein mentales Modell der beiden Blöcke ist identisch:

flow chart

Also, was bin ich dabei?

bearbeiten: drmaciver schlug auf Twitter vor, dass im ersten Fall, aus irgendeinem Grund, der Rohrsockel in den nicht blockierenden Modus geht, aber in der zweiten nicht. Ich kann mir keinen Grund vorstellen, warum das passieren würde, und ich kann mir auch nicht vorstellen, wie man die Fahnen des Beschreibers bekommt, aber es ist zumindest eine plausible Antwort? An dieser Möglichkeit arbeiten.

+0

Welchen Rubin rennst du? –

+0

Dieses Verhalten tritt bei mindestens 1.8.7 und 1.9.3 auf. jruby blockt für alle 60 auf beiden Blöcken, was das Verhalten ist, das ich a priori erraten hätte. – llimllib

+0

Beachten Sie, dass Ihr 'puts (" but this one ... ")' zwischen den beiden Blöcken für mich _wartet, bis der erste 'sleep' abgeschlossen ist, da der erste IO # popen block pflichtgemäß in einem Aufruf von' waitpid() ist '. Wenn Sie das nicht möchten, muss Ihre Rettungslogik den Kindprozess beenden. – pilcrow

Antwort

13

Aha, subtil.

Es gibt eine versteckte, blockierende ensure Klausel am Ende des IO # popen Block im zweiten Fall. Der Timeout :: Fehler ist angehoben angehoben rechtzeitig, aber Sie können nicht rescue es bis zur Ausführung der impliziten ensure Klausel zurückgegeben wird.

Under the hood, tut IO.popen(cmd) { |io| ... } etwas wie folgt aus:

def my_illustrative_io_popen(cmd, &block) 
    begin 
    pio = IO.popen(cmd) 
    block.call(pio)  # This *is* interrupted... 
    ensure 
    pio.close   # ...but then control goes here, which blocks on cmd's termination 
    end 

und der IO # knappes Entkommen ist wirklich mehr oder weniger ein pclose(3), die Sie in waitpid(2), bis die schlafendes Kind Ausfahrten blockiert.

Sie können dies wie so überprüfen:

#!/usr/bin/env ruby 

require 'timeout' 

BEGIN { $BASETIME = Time.now.to_i } 

def xputs(msg) 
    puts "%4.2f: %s" % [(Time.now.to_f - $BASETIME), msg] 
end 

begin 
    Timeout.timeout(3) do 
    begin 
     xputs "popen(sleep 10)" 
     pio = IO.popen("sleep 10") 
     sleep 100      # or loop over pio.gets or whatever 
    ensure 
     xputs "Entering ensure block" 
     #Process.kill 9, pio.pid  # <--- This would solve your problem! 
     pio.close 
     xputs "Leaving ensure block" 
    end 
    end 
rescue Timeout::Error => ex 
    xputs "rescuing: #{ex}" 
end 

Also, was können Sie tun?

Sie müssen es explizit tun, da der Interpreter keine Möglichkeit bietet, die IO # popen ensure Logik zu überschreiben. Sie können den obigen Code als Startvorlage verwenden und beispielsweise die Zeile kill() auskommentieren.

+0

Ich starrte so lange auf io.c, sah nur ein paar Zeilen * oben * die sicherstellen, ohne zu sehen, die Garantie oder überhaupt darüber nachzudenken. Große Antwort, vielen Dank. – llimllib

+0

Ist es möglich, mit dieser Lösung den Exit-Status zu erhalten? – tiktak

0

Im ersten Block wird das Timeout im untergeordneten Element ausgelöst, wodurch es beendet wird und die Steuerung an das übergeordnete Element zurückgegeben wird. Im zweiten Block wird das Timeout im Parent ausgelöst. Das Kind bekommt nie das Signal.

Siehe io.chttps://github.com/ruby/ruby/blob/trunk/io.c#L6021 und timeout.rbhttps://github.com/ruby/ruby/blob/trunk/lib/timeout.rb#L51

+0

Ich weiß, es ist keine detaillierte Antwort Bill, aber so lese ich die Blöcke. –

+1

Ein an IO # popen übergebener Block wird im Kontext des übergeordneten Prozesses ausgeführt. Ich bin mir nicht sicher, was du meinst, wenn du sagst, dass der Prozess des Kindes das Signal bekommen könnte oder nicht. – pilcrow

+0

@ JonathanJulian Ich habe diese beiden Dateien in einem geteilten Bildschirm geöffnet und versucht, es herauszufinden. Soweit ich das beurteilen kann, wird das Timeout vom Hauptthread in beiden Beispielen erzeugt. Hier ist, wo popen den Block, den es passiert hat, läuft: https://github.com/ruby/ruby/blob/trunk/io.c#L6075 – llimllib