Live-Ausgabe vom Unterprozessbefehl

184

Ich verwende ein Python-Skript als Treiber für einen Hydrodynamik-Code. Wenn es Zeit ist, die Simulation auszuführen, verwende ich subprocess.Popen, um den Code auszuführen, die Ausgabe von stdout und stderr in einem subprocess.PIPE--- zu sammeln, dann kann ich die Ausgabeinformationen drucken (und in einer Protokolldatei speichern) und auf Fehler prüfen . Das Problem ist, ich habe keine Ahnung, wie der Code voranschreitet. Wenn ich es direkt über die Befehlszeile ausführe, gibt es mir eine Ausgabe darüber, zu welcher Iteration es ist, zu welcher Zeit, wie der nächste Zeitschritt ist usw.

Gibt es eine Möglichkeit, sowohl die Ausgabe zu speichern (zur Protokollierung und Fehlerprüfung) als auch eine Live-Streaming-Ausgabe zu erstellen?

Der relevante Abschnitt meines Codes:

ret_val = subprocess.Popen( run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True )
output, errors = ret_val.communicate()
log_file.write(output)
print output
if( ret_val.returncode ):
    print "RUN failed\n\n%s\n\n" % (errors)
    success = False

if( errors ): log_file.write("\n\n%s\n\n" % errors)

Ursprünglich war kochend ich das run_commanddurch , teeso dass eine Kopie ging direkt in die Log-Datei, und der Strom noch Ausgang direkt mit dem Terminal - aber auf diese Weise ich keine Fehler (zu meinem know) speichern kann.


Bearbeiten:

Vorübergehende Lösung:

ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
    log_file.flush()

Führen Sie dann in einem anderen Terminal tail -f log.txt(st log_file = 'log.txt') aus.

DilithiumMatrix
quelle
1
Vielleicht können Sie Popen.pollwie in einer vorherigen Frage zum Stapelüberlauf verwenden .
Paulo Almeida
Einige Befehle, die eine Fortschrittsanzeige anzeigen (z. B. git), tun dies nur, wenn ihre Ausgabe ein "tty-Gerät" ist (getestet über libc isatty()). In diesem Fall müssen Sie möglicherweise eine Pseudo-Tty öffnen.
Torek
@torek was ist eine (Pseudo-) Tty?
DilithiumMatrix
2
Geräte auf Unix-ähnlichen Systemen, mit denen ein Prozess vorgibt, ein Benutzer an einer seriellen Schnittstelle zu sein. So funktioniert zum Beispiel ssh (serverseitig). Siehe Python-Pty-Bibliothek und auch pexpect .
Torek
Re temporäre Lösung: Es gibt keine Notwendigkeit zu nennen flush, und es ist notwendig aus dem Stderr Rohr zu lesen , wenn der Subprozess viel stderr Ausgabe erzeugt. Es gibt nicht genug Platz in einem Kommentarfeld, um dies zu erklären ...
torek

Antworten:

168

Sie haben zwei Möglichkeiten, dies zu tun, indem Sie entweder einen Iterator aus den Funktionen readoder erstellen und Folgendes readlinetun:

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for c in iter(lambda: process.stdout.read(1), ''):  # replace '' with b'' for Python 3
        sys.stdout.write(c)
        f.write(c)

oder

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for line in iter(process.stdout.readline, ''):  # replace '' with b'' for Python 3
        sys.stdout.write(line)
        f.write(line)

Oder Sie können eine readerund eine writerDatei erstellen . Übergeben Sie das writeran Popenund lesen Sie aus demreader

import io
import time
import subprocess
import sys

filename = 'test.log'
with io.open(filename, 'wb') as writer, io.open(filename, 'rb', 1) as reader:
    process = subprocess.Popen(command, stdout=writer)
    while process.poll() is None:
        sys.stdout.write(reader.read())
        time.sleep(0.5)
    # Read the remaining
    sys.stdout.write(reader.read())

Auf diese Weise werden die Daten sowohl in test.logdie Standardausgabe als auch in die Standardausgabe geschrieben.

Der einzige Vorteil des Dateiansatzes besteht darin, dass Ihr Code nicht blockiert. So können Sie in der Zwischenzeit tun, was Sie wollen, und auf readernicht blockierende Weise lesen, wann immer Sie wollen . Wenn Sie verwenden PIPE, werden readund readlineFunktionen blockiert, bis entweder ein Zeichen in die Pipe oder eine Zeile in die Pipe geschrieben wird.

Viktor Kerkez
quelle
1
Ugh :-) In eine Datei schreiben, daraus lesen und in der Schleife schlafen? Es besteht auch die Möglichkeit, dass der Vorgang beendet wird, bevor Sie die Datei gelesen haben.
Guy Sirton
13
Mit Python 3 benötigen Sie iter(process.stdout.readline, b'')(dh der an iter übergebene Sentinel muss eine binäre Zeichenfolge sein, da b'' != ''.
John Mellor
3
Für binäre Streams tun Sie dies:for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane
6
Neben der Antwort von @JohnMellor waren in Python 3 die folgenden Änderungen erforderlich: process = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) for line in iter(process.stdout.readline, b'') sys.stdout.write(line.decode(sys.stdout.encoding))
bergercookie
4
aber die Ausgabe ist nicht live, oder? Nach meiner Erfahrung wartet es nur, bis der Prozess abgeschlossen ist, und druckt erst dann auf der Konsole. Link -> stackoverflow.com/questions/30026045/…
denis631
91

Executive Summary (oder "tl; dr" -Version): Es ist einfach, wenn es höchstens eine gibt subprocess.PIPE, sonst ist es schwierig.

Es kann an der Zeit sein, ein wenig darüber zu erklären, wie es subprocess.Popenfunktioniert.

(Vorsichtsmaßnahme: Dies ist für Python 2.x, obwohl 3.x ähnlich ist. Bei der Windows-Variante bin ich ziemlich verschwommen. Ich verstehe das POSIX-Zeug viel besser.)

Die PopenFunktion muss etwas gleichzeitig mit null bis drei E / A-Streams umgehen. Diese werden bezeichnet stdin, stdoutund stderrwie üblich.

Sie können angeben:

  • NoneDies zeigt an, dass Sie den Stream nicht umleiten möchten. Diese werden stattdessen wie gewohnt geerbt. Beachten Sie, dass dies zumindest auf POSIX-Systemen nicht bedeutet, dass Pythons verwendet wird sys.stdout, sondern nur Pythons tatsächliche Standardausgabe. siehe Demo am Ende.
  • Ein intWert. Dies ist ein "roher" Dateideskriptor (zumindest unter POSIX). (Randnotiz: PIPEund STDOUTsind eigentlich ints intern, aber "unmögliche" Deskriptoren, -1 und -2.)
  • Ein Stream - wirklich jedes Objekt mit einer filenoMethode. Popenfindet den Deskriptor für diesen Stream unter Verwendung von stream.fileno()und fährt dann wie für einen intWert fort.
  • subprocess.PIPEDies gibt an, dass Python eine Pipe erstellen soll.
  • subprocess.STDOUT( stderrnur für ): Weisen Sie Python an, denselben Deskriptor wie für zu verwenden stdout. Dies ist nur dann sinnvoll, wenn Sie einen (Nicht- None) Wert für angegeben haben stdout, und selbst dann wird er nur benötigt, wenn Sie ihn festlegen stdout=subprocess.PIPE. (Andernfalls können Sie nur das gleiche Argument angeben, für das Sie angegeben haben stdout, z Popen(..., stdout=stream, stderr=stream). B. )

Die einfachsten Fälle (keine Rohre)

Wenn Sie nichts umleiten (alle drei als Standardwert Nonebelassen oder explizit angeben None), Pipeist dies recht einfach. Es muss nur den Unterprozess abspalten und laufen lassen. Oder, wenn Sie zu einem nicht umleiten PIPE-an intoder ein Strom ist fileno()-es ist immer noch einfach, da das Betriebssystem die ganze Arbeit macht. Python muss nur den Unterprozess abspalten und stdin, stdout und / oder stderr mit den bereitgestellten Dateideskriptoren verbinden.

Der immer noch einfache Fall: ein Rohr

Wenn Sie nur einen Stream umleiten, ist das Pipeimmer noch ziemlich einfach. Lassen Sie uns jeweils einen Stream auswählen und ansehen.

Angenommen , Sie möchten einige liefern stdin, aber wir stdoutund stderrgehen un-umgeleitet, oder gehen Sie zu einem Dateideskriptor. Als übergeordneter Prozess muss Ihr Python-Programm lediglich write()Daten über die Pipe senden. Sie können dies selbst tun, z.

proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc

oder Sie können die Standarddaten an übergeben proc.communicate(), was dann das stdin.writeoben gezeigte tut . Es kommt keine Ausgabe zurück, es communicate()gibt also nur einen weiteren echten Job: Es schließt auch die Leitung für Sie. (Wenn Sie nicht anrufen proc.communicate(), müssen Sie anrufen proc.stdin.close(), um die Pipe zu schließen, damit der Unterprozess weiß, dass keine Daten mehr eingehen.)

Angenommen , Sie möchten capture stdoutaber verlassen stdinund stderrallein. Auch hier ist es einfach: Rufen Sie einfach an proc.stdout.read()(oder gleichwertig), bis keine Ausgabe mehr erfolgt. Da proc.stdout()es sich um einen normalen Python-E / A-Stream handelt, können Sie alle normalen Konstrukte verwenden, z.

for line in proc.stdout:

oder Sie können wieder verwenden proc.communicate(), was einfach das read()für Sie erledigt .

Wenn Sie nur erfassen möchten stderr, funktioniert dies genauso wie bei stdout.

Es gibt noch einen Trick, bevor es schwierig wird. Angenommen, Sie möchten erfassen stdoutund auch erfassen, stderrjedoch auf derselben Pipe wie stdout:

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

In diesem Fall subprocess"betrügt"! Nun, es muss dies tun, damit es nicht wirklich schummelt: Es startet den Unterprozess mit seinem stdout und seinem stderr, die in den (einzelnen) Pipe-Deskriptor gerichtet sind, der auf seinen übergeordneten (Python) Prozess zurückgreift. Auf der übergeordneten Seite gibt es wieder nur einen einzigen Pipe-Deskriptor zum Lesen der Ausgabe. Alle "stderr" -Ausgaben werden in angezeigt proc.stdout, und wenn Sie aufrufen proc.communicate(), ist das stderr-Ergebnis (zweiter Wert im Tupel) Nonekeine Zeichenfolge.

Die schweren Fälle: zwei oder mehr Rohre

Die Probleme treten alle auf, wenn Sie mindestens zwei Rohre verwenden möchten. Tatsächlich hat der subprocessCode selbst dieses Bit:

def communicate(self, input=None):
    ...
    # Optimization: If we are only using one pipe, or no pipe at
    # all, using select() or threads is unnecessary.
    if [self.stdin, self.stdout, self.stderr].count(None) >= 2:

Aber leider haben wir hier mindestens zwei und vielleicht drei verschiedene Pfeifen hergestellt, sodass die count(None)Renditen entweder 1 oder 0 sind. Wir müssen die Dinge auf die harte Tour machen.

Unter Windows werden auf diese Weise threading.ThreadErgebnisse für self.stdoutund gesammelt self.stderr, und der übergeordnete Thread liefert self.stdinEingabedaten (und schließt dann die Pipe).

Unter POSIX wird dies verwendet, pollfalls verfügbar, andernfalls select, um die Ausgabe zu akkumulieren und die Standardeingabe zu liefern. All dies läuft im (einzelnen) übergeordneten Prozess / Thread.

Hier werden Threads oder Poll / Select benötigt, um Deadlocks zu vermeiden. Angenommen, wir haben alle drei Streams in drei separate Pipes umgeleitet. Angenommen, es gibt eine kleine Begrenzung, wie viele Daten in eine Pipe gestopft werden können, bevor der Schreibvorgang unterbrochen wird, und darauf zu warten, dass der Lesevorgang die Pipe vom anderen Ende "bereinigt". Lassen Sie uns diese kleine Grenze nur zur Veranschaulichung auf ein einzelnes Byte setzen. (So ​​funktionieren die Dinge tatsächlich, außer dass das Limit viel größer als ein Byte ist.)

Wenn der übergeordnete Prozess (Python) versucht, mehrere Bytes zu schreiben, z. B. 'go\n'to proc.stdin, geht das erste Byte ein, und das zweite bewirkt, dass der Python-Prozess angehalten wird und darauf wartet, dass der Unterprozess das erste Byte liest und die Pipe leert.

Angenommen, der Unterprozess beschließt, ein freundliches "Hallo! Keine Panik!" Gruß. Das Hgeht in seine Standard-Pipe, aber das ebewirkt , dass es angehalten wird und darauf wartet, dass sein Elternteil das liest, Hund leert die Standard-Pipe.

Jetzt stecken wir fest: Der Python-Prozess schläft und wartet darauf, "go" zu beenden, und der Unterprozess schläft ebenfalls und wartet darauf, "Hallo! Keine Panik!" Zu beenden.

Der subprocess.PopenCode vermeidet dieses Problem beim Threading-or-Select / Poll. Wenn Bytes über die Pipes gehen können, gehen sie. Wenn dies nicht möglich ist, muss nur ein Thread (nicht der gesamte Prozess) in den Ruhezustand versetzt werden. Bei Auswahl / Abfrage wartet der Python-Prozess gleichzeitig auf "kann schreiben" oder "Daten verfügbar" und schreibt in den Standard des Prozesses Nur wenn Platz vorhanden ist und nur dann stdout und / oder stderr gelesen wird, wenn Daten bereit sind. Der proc.communicate()Code (tatsächlich dort, _communicatewo die haarigen Fälle behandelt werden) wird zurückgegeben, sobald alle Standarddaten (falls vorhanden) gesendet und alle Standard- und / oder Standarddaten gesammelt wurden.

Wenn Sie beide stdoutund stderrzwei verschiedene Pipes lesen möchten (unabhängig von einer stdinUmleitung), müssen Sie auch einen Deadlock vermeiden. Das Deadlock-Szenario ist hier anders - es tritt auf, wenn der Unterprozess etwas lang schreibt, stderrwährend Sie Daten abrufen stdout, oder umgekehrt -, aber es ist immer noch da.


Die Demo

Ich habe versprochen zu demonstrieren, dass Python subprocesses unumgeleitet an das zugrunde liegende stdout schreibt, nicht sys.stdout. Also, hier ist ein Code:

from cStringIO import StringIO
import os
import subprocess
import sys

def show1():
    print 'start show1'
    save = sys.stdout
    sys.stdout = StringIO()
    print 'sys.stdout being buffered'
    proc = subprocess.Popen(['echo', 'hello'])
    proc.wait()
    in_stdout = sys.stdout.getvalue()
    sys.stdout = save
    print 'in buffer:', in_stdout

def show2():
    print 'start show2'
    save = sys.stdout
    sys.stdout = open(os.devnull, 'w')
    print 'after redirect sys.stdout'
    proc = subprocess.Popen(['echo', 'hello'])
    proc.wait()
    sys.stdout = save

show1()
show2()

Beim Ausführen:

$ python out.py
start show1
hello
in buffer: sys.stdout being buffered

start show2
hello

Beachten Sie, dass die erste Routine fehlschlägt, wenn Sie hinzufügen stdout=sys.stdout, da ein StringIOObjekt keine hat fileno. Beim zweiten wird das weggelassen, hellowenn Sie hinzufügen, stdout=sys.stdoutda sys.stdoutumgeleitet wurde os.devnull.

(Wenn Sie Python - Datei-Descriptor-1 umleiten, die subprocess werden diese Umleitung folgen. Der open(os.devnull, 'w')Aufruf erzeugt einen Strom , der fileno()größer als 2)

torek
quelle
Hmm. Ihre Demo scheint am Ende das Gegenteil der Behauptung zu zeigen. Sie leiten Pythons Standardausgabe in den Puffer um, aber die Teilprozess-Standardausgabe wird weiterhin an die Konsole gesendet. Wie ist das nützlich? Vermisse ich etwas
Guy Sirton
@GuySirton: Die Demo zeigt, dass der Unterprozess stdout (wenn nicht explizit darauf verwiesen sys.stdout) an Pythons stdout geht, nicht an das ( ) stdout des Python- Programmssys. . Was ich zugebe, ist eine ... merkwürdige Unterscheidung. Gibt es einen besseren Weg, dies auszudrücken?
Torek
Das ist gut zu wissen, aber wir möchten die Ausgabe des Teilprozesses hier wirklich erfassen, daher ist das Ändern von sys.stdout cool, hilft uns aber nicht, denke ich. Für eine gute Beobachtung bei der Kommunikation muss etwas wie select (), poll oder threads verwendet werden.
Guy Sirton
Ich habe eine Implementierung mit select ()
sivann
20

Wir können auch den Standard-Datei-Iterator zum Lesen von stdout verwenden, anstatt das iter-Konstrukt mit readline () zu verwenden.

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)
Knallkopf
quelle
Die eleganteste Antwort hier!
Nir
9
Diese Lösung wird nicht in Echtzeit angezeigt. Es wartet, bis der Vorgang abgeschlossen ist, und zeigt alle Ausgaben auf einmal an. Wenn in der Lösung von Viktor Kerkez "your_command" progressiv angezeigt wird, folgt die Ausgabe progressiv, solange "your_command" von Zeit zu Zeit (aufgrund der Pipe) stdout spült.
Eric H.
1
@Nir weil es nicht live ist.
MelMass
Diese Lösung iteriert im Standarddeskriptor, sodass sie nur aktualisiert wird, wenn eine Zeile in der Ausgabe aktualisiert wird. Für ein zeichenbasiertes Update müssen Sie die read () -Methode wie in Viktors Lösung gezeigt durchlaufen. Aber das war ein Overkill für meinen Anwendungsfall.
Jughead
12

Wenn Sie Bibliotheken von Drittanbietern verwenden können, können Sie möglicherweise Folgendes verwenden sarge(Offenlegung: Ich bin der Betreuer). Diese Bibliothek ermöglicht den nicht blockierenden Zugriff auf Ausgabestreams aus Unterprozessen - sie ist über das subprocessModul geschichtet .

Vinay Sajip
quelle
Gute Arbeit an Sarge, übrigens. Das löst zwar die Anforderungen des OP, könnte aber für diesen Anwendungsfall etwas hartnäckig sein.
deepelement
4

Lösung 1: Protokollieren stdoutUND stderrgleichzeitig in Echtzeit

Eine einfache Lösung, die sowohl stdout als auch stderr gleichzeitig zeilenweise in Echtzeit in einer Protokolldatei protokolliert.

import subprocess as sp
from concurrent.futures import ThreadPoolExecutor


def log_popen_pipe(p, stdfile):

    with open("mylog.txt", "w") as f:

        while p.poll() is None:
            f.write(stdfile.readline())
            f.flush()

        # Write the rest from the buffer
        f.write(stdfile.read())


with sp.Popen(["ls"], stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    with ThreadPoolExecutor(2) as pool:
        r1 = pool.submit(log_popen_pipe, p, p.stdout)
        r2 = pool.submit(log_popen_pipe, p, p.stderr)
        r1.result()
        r2.result()

Lösung 2: Eine Funktion read_popen_pipes(), mit der Sie beide Pipes (stdout / stderr) gleichzeitig in Echtzeit durchlaufen können

import subprocess as sp
from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

# The function in use:

with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):
        print(out_line, end='')
        print(err_line, end='')

    return p.poll()
Rotareti
quelle
3

Eine gute, aber "schwere" Lösung ist die Verwendung von Twisted - siehe unten.

Wenn Sie bereit sind, nur mit etwas in dieser Richtung zu leben, sollte dies funktionieren:

import subprocess
import sys
popenobj = subprocess.Popen(["ls", "-Rl"], stdout=subprocess.PIPE)
while not popenobj.poll():
   stdoutdata = popenobj.stdout.readline()
   if stdoutdata:
      sys.stdout.write(stdoutdata)
   else:
      break
print "Return code", popenobj.returncode

(Wenn Sie read () verwenden, wird versucht, die gesamte "Datei" zu lesen, was nicht nützlich ist. Was wir hier wirklich verwenden könnten, ist etwas, das alle Daten liest, die sich gerade in der Pipe befinden.)

Man könnte auch versuchen, dies mit Threading zu erreichen, z.

import subprocess
import sys
import threading

popenobj = subprocess.Popen("ls", stdout=subprocess.PIPE, shell=True)

def stdoutprocess(o):
   while True:
      stdoutdata = o.stdout.readline()
      if stdoutdata:
         sys.stdout.write(stdoutdata)
      else:
         break

t = threading.Thread(target=stdoutprocess, args=(popenobj,))
t.start()
popenobj.wait()
t.join()
print "Return code", popenobj.returncode

Jetzt könnten wir möglicherweise auch stderr hinzufügen, indem wir zwei Threads haben.

Beachten Sie jedoch, dass die Subprozessdokumente davon abraten, diese Dateien direkt zu verwenden, und empfehlen die Verwendung communicate()(hauptsächlich bei Deadlocks, von denen ich denke, dass sie oben kein Problem darstellen). Die Lösungen sind etwas klobig, sodass das Subprozessmodul anscheinend nicht ganz den Anforderungen entspricht den Job (siehe auch: http://www.python.org/dev/peps/pep-3145/ ) und wir müssen uns etwas anderes ansehen.

Eine aufwändigere Lösung ist die Verwendung von Twisted wie hier gezeigt: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

Die Art und Weise, wie Sie dies mit Twisted tun, besteht darin, Ihren Prozess mit reactor.spawnprocess()einem zu erstellen und bereitzustellen ProcessProtocol, das dann die Ausgabe asynchron verarbeitet. Der Twisted-Beispiel-Python-Code finden Sie hier: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py

Guy Sirton
quelle
Vielen Dank! Ich habe gerade so etwas versucht (basierend auf dem Kommentar von @PauloAlmeida, aber mein Aufruf von subprocess.Popen blockiert - dh es kommt erst zur while-Schleife, wenn es zurückkehrt ...
DilithiumMatrix
1
Das ist nicht was los ist. Es tritt sofort in die while-Schleife ein und blockiert dann den read()Aufruf, bis der Unterprozess beendet wird und der übergeordnete Prozess EOFauf der Pipe empfängt .
Alp
@Alp interessant! so ist es.
DilithiumMatrix
Ja, ich war zu schnell, um dies zu posten. Es funktioniert tatsächlich nicht richtig und kann nicht einfach repariert werden. zurück zum Zeichentisch.
Guy Sirton
1
@zhermes: Das Problem mit read () ist also, dass versucht wird, die gesamte Ausgabe bis EOF zu lesen, was nicht nützlich ist. readline () hilft und ist möglicherweise alles, was Sie brauchen (sehr lange Zeilen können jedoch auch ein Problem sein). Sie müssen auch auf Pufferung in dem Prozess
achten,
3

Zusätzlich zu all diesen Antworten könnte ein einfacher Ansatz auch folgender sein:

process = subprocess.Popen(your_command, stdout=subprocess.PIPE)

while process.stdout.readable():
    line = process.stdout.readline()

    if not line:
        break

    print(line.strip())

Durchlaufen Sie den lesbaren Stream, solange er lesbar ist. Wenn ein leeres Ergebnis angezeigt wird, stoppen Sie ihn.

Der Schlüssel hier ist, dass readline()eine Zeile (mit \nam Ende) zurückgegeben wird, solange eine Ausgabe vorhanden ist und leer ist, wenn sie wirklich am Ende ist.

Hoffe das hilft jemandem.

Kabirbaidhya
quelle
3

Basierend auf all dem oben genannten schlage ich eine leicht modifizierte Version (python3) vor:

  • while-Schleife, die readline aufruft (Die vorgeschlagene Iter-Lösung schien für mich für immer zu blockieren - Python 3, Windows 7)
  • Die Verarbeitung der gelesenen Daten muss nicht dupliziert werden, nachdem die Umfrage nicht zurückgegeben wurde.None
  • stderr wird in stdout geleitet, sodass beide Ausgangsausgänge gelesen werden
  • Code hinzugefügt, um den Exit-Wert von cmd zu erhalten.

Code:

import subprocess
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT, universal_newlines=True)
while True:
    rd = proc.stdout.readline()
    print(rd, end='')  # and whatever you want to do...
    if not rd:  # EOF
        returncode = proc.poll()
        if returncode is not None:
            break
        time.sleep(0.1)  # cmd closed stdout, but not exited yet

# You may want to check on ReturnCode here
mrtnlrsn
quelle
Der returncodeTeil war in meinem Fall entscheidend.
Sternenstaub
2

Es sieht so aus, als würde die zeilengepufferte Ausgabe für Sie funktionieren. In diesem Fall könnte etwas wie das Folgende passen. (Vorsichtsmaßnahme: Es ist nicht getestet.) Dadurch wird der Standardprozess des Unterprozesses nur in Echtzeit angezeigt. Wenn Sie sowohl stderr als auch stdout in Echtzeit haben möchten, müssen Sie etwas Komplexeres tun select.

proc = subprocess.Popen(run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
while proc.poll() is None:
    line = proc.stdout.readline()
    print line
    log_file.write(line + '\n')
# Might still be data on stdout at this point.  Grab any
# remainder.
for line in proc.stdout.read().split('\n'):
    print line
    log_file.write(line + '\n')
# Do whatever you want with proc.stderr here...
Alp
quelle
2

Warum nicht stdoutdirekt einstellen sys.stdout? Und wenn Sie auch in ein Protokoll ausgeben müssen, können Sie einfach die Schreibmethode von f überschreiben.

import sys
import subprocess

class SuperFile(open.__class__):

    def write(self, data):
        sys.stdout.write(data)
        super(SuperFile, self).write(data)

f = SuperFile("log.txt","w+")       
process = subprocess.Popen(command, stdout=f, stderr=f)
xaav
quelle
Das würde nicht funktionieren: Das Unterprozessmodul gabelt und setzt den stdoutDateideskriptor auf den Dateideskriptor des übergebenen Dateiobjekts. Die Schreibmethode würde niemals aufgerufen werden (zumindest macht das der Unterprozess für stderr, ich denke, es ist dasselbe für stdout).
Tier
2

Alle oben genannten Lösungen, die ich ausprobiert habe, konnten entweder die Ausgabe von stderr und stdout (mehrere Pipes) nicht trennen oder wurden für immer blockiert, wenn der Pipe-Puffer des Betriebssystems voll war. Dies geschieht, wenn der Befehl, den Sie ausführen, zu schnell ausgegeben wird (es gibt eine Warnung für Python) poll () Handbuch des Unterprozesses). Der einzige zuverlässige Weg, den ich gefunden habe, war durch Auswahl, aber dies ist eine reine Posix-Lösung:

import subprocess
import sys
import os
import select
# returns command exit status, stdout text, stderr text
# rtoutput: show realtime output while running
def run_script(cmd,rtoutput=0):
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    poller = select.poll()
    poller.register(p.stdout, select.POLLIN)
    poller.register(p.stderr, select.POLLIN)

    coutput=''
    cerror=''
    fdhup={}
    fdhup[p.stdout.fileno()]=0
    fdhup[p.stderr.fileno()]=0
    while sum(fdhup.values()) < len(fdhup):
        try:
            r = poller.poll(1)
        except select.error, err:
            if err.args[0] != EINTR:
                raise
            r=[]
        for fd, flags in r:
            if flags & (select.POLLIN | select.POLLPRI):
                c = os.read(fd, 1024)
                if rtoutput:
                    sys.stdout.write(c)
                    sys.stdout.flush()
                if fd == p.stderr.fileno():
                    cerror+=c
                else:
                    coutput+=c
            else:
                fdhup[fd]=1
    return p.poll(), coutput.strip(), cerror.strip()
Sivann
quelle
Eine andere Alternative besteht darin, ein Gewinde pro Rohr abzuspinnen. Jeder Thread kann E / A in der Pipe blockieren, ohne die anderen Threads zu blockieren. Dies führt jedoch zu einer Reihe von Problemen. Alle Methoden haben Ärger, Sie wählen nur die aus, die Sie am wenigsten nerven. :-)
Torek
2

Ähnlich wie bei früheren Antworten, aber die folgende Lösung funktionierte für mich unter Windows mit Python3, um eine gängige Methode zum Drucken und Anmelden in Echtzeit bereitzustellen ( Echtzeitausgabe mit Python ):

def print_and_log(command, logFile):
    with open(logFile, 'wb') as f:
        command = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)

        while True:
            output = command.stdout.readline()
            if not output and command.poll() is not None:
                f.close()
                break
            if output:
                f.write(output)
                print(str(output.strip(), 'utf-8'), flush=True)
        return command.poll()
Scottysbasement
quelle
2

Ich denke, dass die subprocess.communicateMethode etwas irreführend ist: Sie füllt tatsächlich die stdout und stderr , die Sie in der angeben subprocess.Popen.

Doch aus der Lektüre , subprocess.PIPEdass Sie die zur Verfügung stellen kann subprocess.Popen‚s stdout und stderr Parameter werden schließlich OS Rohrpuffer füllen und Deadlock Ihre Anwendung (vor allem , wenn Sie mehrere Prozesse / Threads haben , die verwendet werden müssen subprocess).

Meine vorgeschlagene Lösung besteht darin, stdout und stderr mit Dateien zu versorgen - und den Inhalt der Dateien zu lesen, anstatt aus dem Deadlocking zu lesen PIPE. Diese Dateien können tempfile.NamedTemporaryFile()- auf die auch zum Lesen zugegriffen werden kann, während sie von geschrieben werden subprocess.communicate.

Unten finden Sie ein Beispiel für die Verwendung:

        try:
            with ProcessRunner(('python', 'task.py'), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

Und dies ist der Quellcode, ist bereit , verwendet werden , mit so vielen Kommentare , wie ich liefern könnte , zu erklären , was es tut:

Wenn Sie mit Python 2, vergewissern Sie sich zuerst die neueste Version des Installations subprocess32 Paket von pypi.


import os
import sys
import threading
import time
import tempfile
import logging

if os.name == 'posix' and sys.version_info[0] < 3:
    # Support python 2
    import subprocess32 as subprocess
else:
    # Get latest and greatest from python 3
    import subprocess

logger = logging.getLogger(__name__)


class ProcessError(Exception):
    """Base exception for errors related to running the process"""


class ProcessTimeout(ProcessError):
    """Error that will be raised when the process execution will exceed a timeout"""


class ProcessRunner(object):
    def __init__(self, args, env=None, timeout=None, bufsize=-1, seconds_to_wait=0.25, **kwargs):
        """
        Constructor facade to subprocess.Popen that receives parameters which are more specifically required for the
        Process Runner. This is a class that should be used as a context manager - and that provides an iterator
        for reading captured output from subprocess.communicate in near realtime.

        Example usage:


        try:
            with ProcessRunner(('python', task_file_path), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

        :param args: same as subprocess.Popen
        :param env: same as subprocess.Popen
        :param timeout: same as subprocess.communicate
        :param bufsize: same as subprocess.Popen
        :param seconds_to_wait: time to wait between each readline from the temporary file
        :param kwargs: same as subprocess.Popen
        """
        self._seconds_to_wait = seconds_to_wait
        self._process_has_timed_out = False
        self._timeout = timeout
        self._process_done = False
        self._std_file_handle = tempfile.NamedTemporaryFile()
        self._process = subprocess.Popen(args, env=env, bufsize=bufsize,
                                         stdout=self._std_file_handle, stderr=self._std_file_handle, **kwargs)
        self._thread = threading.Thread(target=self._run_process)
        self._thread.daemon = True

    def __enter__(self):
        self._thread.start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._thread.join()
        self._std_file_handle.close()

    def __iter__(self):
        # read all output from stdout file that subprocess.communicate fills
        with open(self._std_file_handle.name, 'r') as stdout:
            # while process is alive, keep reading data
            while not self._process_done:
                out = stdout.readline()
                out_without_trailing_whitespaces = out.rstrip()
                if out_without_trailing_whitespaces:
                    # yield stdout data without trailing \n
                    yield out_without_trailing_whitespaces
                else:
                    # if there is nothing to read, then please wait a tiny little bit
                    time.sleep(self._seconds_to_wait)

            # this is a hack: terraform seems to write to buffer after process has finished
            out = stdout.read()
            if out:
                yield out

        if self._process_has_timed_out:
            raise ProcessTimeout('Process has timed out')

        if self._process.returncode != 0:
            raise ProcessError('Process has failed')

    def _run_process(self):
        try:
            # Start gathering information (stdout and stderr) from the opened process
            self._process.communicate(timeout=self._timeout)
            # Graceful termination of the opened process
            self._process.terminate()
        except subprocess.TimeoutExpired:
            self._process_has_timed_out = True
            # Force termination of the opened process
            self._process.kill()

        self._process_done = True

    @property
    def return_code(self):
        return self._process.returncode


Victor Klapholz
quelle
1

Hier ist eine Klasse, die ich in einem meiner Projekte verwende. Es leitet die Ausgabe eines Unterprozesses in das Protokoll um. Zuerst habe ich versucht, die Schreibmethode einfach zu überschreiben, aber das funktioniert nicht, da der Unterprozess sie niemals aufruft (die Umleitung erfolgt auf der Ebene des Dateideskriptors). Ich verwende also meine eigene Pipe, ähnlich wie im Subprozessmodul. Dies hat den Vorteil, dass die gesamte Protokollierungs- / Drucklogik im Adapter gekapselt ist und Sie einfach Instanzen des Protokollierers an Folgendes übergeben können Popen:subprocess.Popen("/path/to/binary", stderr = LogAdapter("foo"))

class LogAdapter(threading.Thread):

    def __init__(self, logname, level = logging.INFO):
        super().__init__()
        self.log = logging.getLogger(logname)
        self.readpipe, self.writepipe = os.pipe()

        logFunctions = {
            logging.DEBUG: self.log.debug,
            logging.INFO: self.log.info,
            logging.WARN: self.log.warn,
            logging.ERROR: self.log.warn,
        }

        try:
            self.logFunction = logFunctions[level]
        except KeyError:
            self.logFunction = self.log.info

    def fileno(self):
        #when fileno is called this indicates the subprocess is about to fork => start thread
        self.start()
        return self.writepipe

    def finished(self):
       """If the write-filedescriptor is not closed this thread will
       prevent the whole program from exiting. You can use this method
       to clean up after the subprocess has terminated."""
       os.close(self.writepipe)

    def run(self):
        inputFile = os.fdopen(self.readpipe)

        while True:
            line = inputFile.readline()

            if len(line) == 0:
                #no new data was added
                break

            self.logFunction(line.strip())

Wenn Sie keine Protokollierung benötigen, sondern nur verwenden möchten, können print()Sie natürlich große Teile des Codes entfernen und die Klasse kürzer halten. Man könnte es auch durch ein erweitern __enter__und __exit__Verfahren und rufen Sie finishedin __exit__so dass Sie sie leicht als Kontext verwenden könnte.

t.animal
quelle
1

Keine der Pythonic-Lösungen hat bei mir funktioniert. Es stellte sich heraus, dass proc.stdout.read()oder ähnliches für immer blockieren kann.

Deshalb benutze ich teeso:

subprocess.run('./my_long_running_binary 2>&1 | tee -a my_log_file.txt && exit ${PIPESTATUS}', shell=True, check=True, executable='/bin/bash')

Diese Lösung ist praktisch, wenn Sie sie bereits verwenden shell=True.

${PIPESTATUS}Erfasst den Erfolgsstatus der gesamten Befehlskette (nur in Bash verfügbar). Wenn ich das weglassen && exit ${PIPESTATUS}würde, würde dies immer Null zurückgeben, da es teenie fehlschlägt.

unbufferDies kann erforderlich sein, um jede Zeile sofort in das Terminal zu drucken, anstatt viel zu lange zu warten, bis der "Pipe-Puffer" gefüllt ist. Unbuffer verschluckt jedoch den Exit-Status von Assert (SIG Abort) ...

2>&1 protokolliert auch stderror in der Datei.

Mike76
quelle