Lesen Sie kontinuierlich aus STDOUT des externen Prozesses in Ruby

86

Ich möchte Blender über die Befehlszeile über ein Ruby-Skript ausführen, das dann die Ausgabe von Blender zeilenweise verarbeitet, um einen Fortschrittsbalken in einer GUI zu aktualisieren. Es ist nicht wirklich wichtig, dass Blender der externe Prozess ist, dessen Standard ich lesen muss.

Ich kann anscheinend nicht in der Lage sein, die Fortschrittsmeldungen zu erfassen, die Blender normalerweise auf der Shell druckt, wenn der Blender-Prozess noch ausgeführt wird, und ich habe einige Möglichkeiten ausprobiert. Ich scheine immer auf die Standardausgabe des Mixers zuzugreifen, nachdem der Mixer beendet wurde, nicht während er noch läuft.

Hier ist ein Beispiel für einen fehlgeschlagenen Versuch. Die ersten 25 Zeilen der Ausgabe des Mixers werden abgerufen und gedruckt, jedoch erst, nachdem der Mixerprozess beendet wurde:

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}

Bearbeiten:

Um es etwas klarer zu machen, gibt der Befehl, der Blender aufruft, einen Ausgabestream in der Shell zurück, der den Fortschritt anzeigt (Teil 1-16 abgeschlossen usw.). Es scheint, dass jeder Aufruf zum "Abrufen" der Ausgabe blockiert wird, bis der Mixer beendet wird. Das Problem ist, wie Sie Zugriff auf diese Ausgabe erhalten, während der Mixer noch ausgeführt wird, während der Mixer die Ausgabe an die Shell druckt.

ehsanul
quelle

Antworten:

174

Ich habe einige Erfolge bei der Lösung meines Problems erzielt. Hier sind die Details mit einigen Erklärungen, falls jemand mit einem ähnlichen Problem diese Seite findet. Wenn Sie sich jedoch nicht für Details interessieren, finden Sie hier die kurze Antwort :

Verwenden Sie PTY.spawn auf folgende Weise (natürlich mit Ihrem eigenen Befehl):

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

Und hier ist die lange Antwort mit viel zu vielen Details:

Das eigentliche Problem scheint zu sein, dass, wenn ein Prozess seine Standardausgabe nicht explizit löscht, alles, was in Standardausgabe geschrieben wird, gepuffert und nicht tatsächlich gesendet wird, bis der Prozess abgeschlossen ist, um E / A zu minimieren (dies ist anscheinend ein Implementierungsdetail von vielen C-Bibliotheken, die so erstellt wurden, dass der Durchsatz durch weniger häufige E / A maximiert wird. Wenn Sie den Prozess einfach so ändern können, dass er regelmäßig gelöscht wird, ist dies Ihre Lösung. In meinem Fall war es ein Mixer, also ein bisschen einschüchternd für einen kompletten Noob wie mich, die Quelle zu ändern.

Wenn Sie diese Prozesse jedoch über die Shell ausführen, wird stdout in Echtzeit für die Shell angezeigt, und stdout scheint nicht gepuffert zu sein. Es wird nur gepuffert, wenn es von einem anderen Prozess aufgerufen wird, aber wenn eine Shell behandelt wird, wird das Standardout in Echtzeit ungepuffert angezeigt.

Dieses Verhalten kann sogar bei einem Ruby-Prozess als untergeordneter Prozess beobachtet werden, dessen Ausgabe in Echtzeit erfasst werden muss. Erstellen Sie einfach ein Skript, random.rb, mit der folgenden Zeile:

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

Dann ein Ruby-Skript, um es aufzurufen und seine Ausgabe zurückzugeben:

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

Sie werden sehen, dass Sie das Ergebnis nicht wie erwartet in Echtzeit erhalten, sondern auf einmal danach. STDOUT wird gepuffert, obwohl es nicht gepuffert wird, wenn Sie random.rb selbst ausführen. Dies kann gelöst werden, indem eine STDOUT.flushAnweisung innerhalb des Blocks in random.rb hinzugefügt wird. Wenn Sie die Quelle jedoch nicht ändern können, müssen Sie dies umgehen. Sie können es nicht von außerhalb des Prozesses spülen.

Wenn der Unterprozess in Echtzeit in eine Shell gedruckt werden kann, muss es eine Möglichkeit geben, dies auch mit Ruby in Echtzeit zu erfassen. Und da ist. Sie müssen das PTY-Modul verwenden, das meiner Meinung nach im Ruby Core enthalten ist (1.8.6 sowieso). Traurige Sache ist, dass es nicht dokumentiert ist. Aber ich habe zum Glück einige Anwendungsbeispiele gefunden.

Um zu erklären, was PTY ist, steht es zunächst für Pseudo-Terminal . Grundsätzlich kann sich das Ruby-Skript dem Unterprozess so präsentieren, als wäre es ein echter Benutzer, der den Befehl gerade in eine Shell eingegeben hat. Daher tritt jedes geänderte Verhalten auf, das nur auftritt, wenn ein Benutzer den Prozess über eine Shell gestartet hat (z. B. wenn STDOUT in diesem Fall nicht gepuffert wird). Wenn Sie die Tatsache verbergen, dass ein anderer Prozess diesen Prozess gestartet hat, können Sie STDOUT in Echtzeit erfassen, da es nicht gepuffert wird.

Versuchen Sie den folgenden Code, damit dies mit dem Skript random.rb als Kind funktioniert:

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
quelle
7
Das ist großartig, aber ich glaube, die Blockparameter stdin und stdout sollten vertauscht werden. Siehe: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro
1
Wie schließe ich die Pty? Pid töten?
Boris B.
Tolle Antwort. Sie haben mir geholfen, mein Rake-Bereitstellungsskript für Heroku zu verbessern. Es zeigt das 'Git Push'-Protokoll in Echtzeit an und bricht die Aufgabe ab, wenn' fatal: 'gefunden wurde. Gist.github.com/sseletskyy/9248357
Serge Seletskyy
1
Ich habe ursprünglich versucht, diese Methode zu verwenden, aber 'pty' ist in Windows nicht verfügbar. Wie sich herausstellt, STDOUT.sync = trueist alles, was benötigt wird (Mveermans Antwort unten). Hier ist ein weiterer Thread mit einem Beispielcode .
Pakman
12

verwenden IO.popen. Dies ist ein gutes Beispiel.

Ihr Code würde so etwas wie:

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
quelle
Ich habe es versucht. Das Problem ist das gleiche. Ich bekomme danach Zugriff auf die Ausgabe. Ich glaube, IO.popen beginnt damit, dass das erste Argument als Befehl ausgeführt wird, und wartet darauf, dass es endet. In meinem Fall wird die Ausgabe vom Mixer ausgegeben, während der Mixer noch verarbeitet wird. Und dann wird der Block danach aufgerufen, was mir nicht hilft.
ehsanul
Folgendes habe ich versucht. Es gibt die Ausgabe zurück, nachdem der Mixer fertig ist: IO.popen ("Mixer -b mball.blend // rendert / -F JPEG -x 1 -f 1", "w +") do | Blender | blender.each {| line | setzt Linie; Ausgabe + = Zeile;} Ende
ehsanul
3
Ich bin nicht sicher, was in Ihrem Fall passiert. Ich habe den obigen Code mit yeseiner Befehlszeilenanwendung getestet, die niemals endet , und es hat funktioniert. Der Code war wie folgt : IO.popen('yes') { |p| p.each { |f| puts f } }. Ich vermute, es hat etwas mit Mixer zu tun und nicht mit Rubin. Wahrscheinlich spült der Mixer seinen STDOUT nicht immer.
Sinan Taifour
Ok, ich habe es gerade mit einem externen Ruby-Prozess zum Testen versucht, und Sie haben Recht. Scheint ein Mixerproblem zu sein. Trotzdem danke für die Antwort.
ehsanul
Es stellt sich heraus, dass es eine Möglichkeit gibt, die Ausgabe durch Ruby zu erhalten, obwohl der Mixer seine Standardausgabe nicht spült. Details in Kürze in einer separaten Antwort, falls Sie interessiert sind.
ehsanul
6

STDOUT.flush oder STDOUT.sync = true

Mveerman
quelle
Ja, das war eine lahme Antwort. Ihre Antwort war besser.
Mveerman
Nicht lahm! Hat für mich gearbeitet.
Clay Bridges
Genauer gesagt:STDOUT.sync = true; system('<whatever-command>')
Karam
4

Blender druckt wahrscheinlich keine Zeilenumbrüche, bis das Programm beendet ist. Stattdessen wird das Wagenrücklaufzeichen (\ r) gedruckt. Die einfachste Lösung ist wahrscheinlich die Suche nach der magischen Option, mit der Zeilenumbrüche mit der Fortschrittsanzeige gedruckt werden.

Das Problem ist, dass IO#gets(und verschiedene andere E / A-Methoden) den Zeilenumbruch als Trennzeichen verwenden. Sie lesen den Stream, bis sie das Zeichen "\ n" treffen (der Mixer sendet nicht).

Versuchen Sie, das Eingabetrennzeichen einzustellen $/ = "\r"oder blender.gets("\r")stattdessen zu verwenden.

Übrigens sollten Sie bei solchen Problemen immer überprüfen puts someobj.inspectoder p someobj(beide tun dasselbe), um versteckte Zeichen in der Zeichenfolge zu sehen.

hhaamu
quelle
1
Ich habe gerade die angegebene Ausgabe überprüft, und es scheint, dass Blender einen Zeilenumbruch (\ n) verwendet, sodass dies nicht das Problem war. Trotzdem danke für den Tipp, ich werde das beim nächsten Debuggen so etwas berücksichtigen.
ehsanul
0

Ich weiß nicht, ob ehsanul zu der Zeit die Frage beantwortet hat, es war noch Open3::pipeline_rw()verfügbar, aber es macht die Dinge wirklich einfacher.

Ich verstehe ehsanuls Job bei Blender nicht, also habe ich mit tarund ein weiteres Beispiel gemacht xz. tarfügt Eingabedateien zum stdout-Stream hinzu, xznimmt diese stdoutund komprimiert sie erneut auf einen anderen stdout. Unsere Aufgabe ist es, den letzten Standard zu nehmen und in unsere endgültige Datei zu schreiben:

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
quelle
0

Alte Frage, hatte aber ähnliche Probleme.

Ohne meinen Ruby-Code wirklich zu ändern, war eine Sache, die half, meine Pipe mit stdbuf zu verpacken , wie folgt :

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

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

In meinem Beispiel lautet der eigentliche Befehl, mit dem ich wie mit einer Shell interagieren möchte, openssl .

-oL -eL Sagen Sie ihm, er soll STDOUT und STDERR nur bis zu einer neuen Zeile puffern. Ersetzen Sie Ldurch, 0um vollständig zu entpuffern.

Dies funktioniert jedoch nicht immer: Manchmal erzwingt der Zielprozess seinen eigenen Stream-Puffertyp, wie in einer anderen Antwort angegeben.

Marcos
quelle