Ich benutze ein Python-Skript als Treiber für einen Hydrodynamik-Code. Wenn es an der Zeit ist, die Simulation auszuführen, verwende ich subprocess.Popen
, um den Code auszuführen, sammle die Ausgabe von stdout und stderr in einen subprocess.PIPE
---. Dann kann ich die Ausgabeinformationen drucken (und in einer Protokolldatei speichern) und überprüfen, ob Irgendwelche Fehler. Das Problem ist, ich habe keine Ahnung, wie sich der Code entwickelt. Wenn ich es direkt von der Kommandozeile aus laufe, gibt es eine Ausgabe darüber, um welche Iteration es sich handelt, um wie viel Uhr, was der nächste Zeitschritt ist usw.
Gibt es eine Möglichkeit, die Ausgabe zu speichern (für die Protokollierung und Fehlerprüfung) und auch eine Live-Streaming-Ausgabe zu erzeugen?
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 habe ich den run_command
durch tee
weitergeleitet, so dass eine Kopie direkt in die Protokolldatei ging und der Stream immer noch direkt an das Terminal ausgegeben wird. Auf diese Weise kann ich jedoch keine Fehler speichern (meines Wissens nach).
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
(s.t. log_file = 'log.txt'
) aus.
Sie haben zwei Möglichkeiten, dies zu tun, indem Sie entweder einen Iterator aus den Funktionen read
oder readline
erstellen und tun:
import subprocess
import sys
with open('test.log', 'w') as f:
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:
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 reader
- und eine writer
-Datei erstellen. Übergeben Sie die writer
an die Popen
und lesen Sie sie aus der reader
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 den test.log
als auch in die Standardausgabe geschrieben.
Der einzige Vorteil des Dateiverfahrens besteht darin, dass Ihr Code nicht blockiert. So können Sie in der Zwischenzeit tun, was Sie möchten, und aus der Variablen reader
lesen, wann immer Sie möchten. Wenn Sie PIPE
verwenden, werden read
- und readline
-Funktionen blockiert, bis entweder ein Zeichen in die Pipe oder eine Zeile in die Pipe geschrieben wird.
subprocess.PIPE
gibt, sonst ist es schwierig.Es könnte Zeit sein, ein wenig zu erklären, wie subprocess.Popen
seine Sache tut.
(Vorbehalt: Dies ist für Python 2.x, obwohl 3.x ähnlich ist, und ich bin bei der Windows-Variante ziemlich unscharf. Ich verstehe das POSIX-Material viel besser.)
Die Popen
- Funktion muss etwas gleichzeitig mit null bis drei E/A-Strömen arbeiten. Diese werden wie üblich mit stdin
, stdout
und stderr
bezeichnet.
Sie können Folgendes anbieten:
None
zeigt an, dass Sie den Stream nicht umleiten möchten. Es wird diese stattdessen wie üblich erben. Beachten Sie, dass dies zumindest auf POSIX-Systemen nicht bedeutet, dass sys.stdout
von Python verwendet wird, nur Pythons actual stdout; siehe Demo am Ende.int
Wert. Dies ist ein "roher" Dateideskriptor (mindestens in POSIX). (Randbemerkung: PIPE
und STDOUT
sind tatsächlich int
s, sind aber "unmögliche" Deskriptoren -1 und -2.)fileno
. Popen
findet den Deskriptor für diesen Stream mithilfe von stream.fileno()
und fährt dann mit einem int
-Wert fort.subprocess.PIPE
gibt an, dass Python eine Pipe erstellen soll.subprocess.STDOUT
(nur für stderr
): Weisen Sie Python an, denselben Deskriptor zu verwenden wie für stdout
. Dies ist nur sinnvoll, wenn Sie einen Wert (nicht -None
) für stdout
angegeben haben, und selbst dann ist es nur required , wenn Sie stdout=subprocess.PIPE
setzen. (Andernfalls können Sie einfach dasselbe Argument angeben, das Sie für stdout
angegeben haben, z. B. Popen(..., stdout=stream, stderr=stream)
.)Wenn Sie nichts umleiten (belassen Sie alle drei als Standardwert None
oder geben Sie den expliziten Wert None
an, ist es für Pipe
ganz einfach. Es muss nur den Subprozess ablaufen lassen und laufen lassen. Oder, wenn Sie zu einer nicht-PIPE
- einer int
- oder einer __Funktion eines Streams __-- umleiten, ist es immer noch einfach, da das Betriebssystem die ganze Arbeit erledigt. Python muss lediglich den Unterprozess abschalten und stdin, stdout und/oder stderr mit den bereitgestellten Dateideskriptoren verbinden.
Wenn Sie nur einen Stream umleiten, ist es für Pipe
immer noch ziemlich einfach. Lass uns einen Stream auswählen und schauen.
Angenommen, Sie möchten stdin
angeben, aber stdout
und stderr
werden nicht weitergeleitet oder gehen zu einem Dateideskriptor über. Als übergeordneter Prozess muss Ihr Python-Programm lediglich fileno()
verwenden, um Daten über die Pipe zu 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 stdin-Daten an write()
übergeben, die dann den oben angegebenen stdin.write
ausführt. Da keine Ausgabe zurückkommt, hat proc.communicate()
nur noch eine weitere Aufgabe: Es schließt auch die Pipe für Sie. (Wenn Sie nicht communicate()
aufrufen, müssen Sie proc.communicate()
aufrufen, um die Pipe zu schließen, damit der Unterprozess weiß, dass keine Daten mehr durchlaufen.)
Angenommen, Sie möchten stdout
erfassen, stdin
und stderr
jedoch allein lassen. Wieder ist es ganz einfach: Rufen Sie einfach proc.stdin.close()
(oder gleichwertig) auf, bis keine Ausgabe mehr erfolgt. Da proc.stdout.read()
ein normaler Python-E/A-Stream ist, können Sie alle normalen Konstrukte darauf verwenden, z.
for line in proc.stdout:
oder wiederum können Sie proc.stdout()
verwenden, was einfach die proc.communicate()
für Sie erledigt.
Wenn Sie nur stderr
erfassen möchten, funktioniert es genauso wie mit stdout
.
Es gibt noch einen Trick, bevor die Dinge hart werden. Angenommen, Sie möchten stdout
und stderr
aber in derselben Pipe wie stdout erfassen:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
In diesem Fall subprocess
"Cheats"! Nun, es muss dies tun, also nicht wirklich betrügen: Es startet den Unterprozess, wobei sowohl stdout als auch stderr in den (einzigen) Pipe-Deskriptor geleitet werden, der auf den übergeordneten Prozess (Python) zurückgreift. Auf der übergeordneten Seite gibt es nur einen einzigen Pipe-Deskriptor zum Lesen der Ausgabe. Alle "stderr" -Ausgaben werden in proc.stdout
angezeigt. Wenn Sie read()
aufrufen, ist das stderr-Ergebnis (zweiter Wert im Tuple) None
, keine Zeichenfolge.
Die Probleme treten alle auf, wenn Sie mindestens zwei Rohre verwenden möchten. Tatsächlich hat der subprocess
-Code 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 Pipes gemacht, also gibt die Funktion entweder 1 oder 0 zurück. Wir müssen es auf die harte Tour machen.
Unter Windows werden threading.Thread
zum Sammeln der Ergebnisse für self.stdout
und self.stderr
verwendet, und der übergeordnete Thread liefert self.stdin
-Eingabedaten (und schließt dann die Pipe).
Bei POSIX wird poll
verwendet, falls verfügbar, andernfalls select
, um die Ausgabe zu sammeln und die Standardeingabe zu liefern. All dies läuft im (einzigen) übergeordneten Prozess/Thread ab.Hier sind Threads oder Poll/Select erforderlich, um Deadlock zu vermeiden. Angenommen, wir haben alle drei Streams auf drei separate Pipes umgeleitet. Angenommen, es gibt eine kleine Grenze dafür, wie viele Daten in eine Pipe gestopft werden können, bevor der Schreibvorgang angehalten wird, und warten, bis der Lesevorgang die Pipe vom anderen Ende "reinigt". Lassen Sie uns dieses kleine Limit auf ein Byte setzen, nur zur Veranschaulichung. (Dies ist in der Tat so, wie die Dinge funktionieren, außer dass das Limit viel größer als ein Byte ist.).
Wenn der übergeordnete (Python) -Prozess versucht, mehrere Bytes zu schreiben, beispielsweise 'go\n'
zu proc.stdin
, geht das erste Byte ein und das zweite Byte bewirkt, dass der Python-Prozess angehalten wird. Er wartet darauf, dass der Unterprozess das erste Byte liest und die Pipe leert.
Nehmen Sie an, der Unterprozess beschließt, ein freundliches "Hallo! Keine Panik!" Zu drucken. Gruß. Die H
geht in ihre stdout-Pipe, aber die e
bewirkt, dass sie angehalten wird und wartet, bis ihr Elternteil H
liest.
Jetzt stecken wir fest: Der Python-Prozess schläft und wartet, bis er "go" beendet hat, und der Unterprozess schläft ebenfalls und wartet, bis er "Hallo! Keine Panik!".
Der Code subprocess.Popen
vermeidet dieses Problem beim Einfädeln oder Auswählen/Abfragen. 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 - oder im Fall von select/poll wartet der Python-Prozess gleichzeitig auf "Kann schreiben" oder "Daten verfügbar" und schreibt in die Standardeingabedatei des Prozesses Nur wenn Platz vorhanden ist, und nur wenn Daten bereit sind, werden Stdout und/oder Stderr gelesen. Der proc.communicate()
-Code (eigentlich _communicate
, in dem die haarigen Fälle behandelt werden) wird zurückgegeben, sobald alle stdin-Daten (falls vorhanden) gesendet wurden und alle stdout- und/oder stderr-Daten gesammelt wurden.
Wenn Sie sowohl stdout
als auch stderr
an zwei verschiedenen Pipes lesen möchten (unabhängig von einer stdin
-Weiterleitung), müssen Sie auch Deadlock vermeiden. Das Deadlock-Szenario unterscheidet sich hier - es tritt ein, wenn der Unterprozess etwas in stderr
schreibt, während Sie Daten aus stdout
oder umgekehrt abrufen -, aber es ist immer noch da.
.
subprocess
es nicht umgeleitet in das zugrunde liegende stdout schreibt, nicht sys.stdout
. Hier ist also 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()
$ python out.py
start show1
hello
in buffer: sys.stdout being buffered
start show2
hello
(Wenn Sie den Datei-Deskriptor-1 von Python umleiten, folgt der Unterprozess dieser Umleitung. /. Der Aufruf count(None)
erzeugt einen Stream, dessen proc.communicate()
größer als 2 ist.).
(If you redirect Python's file-descriptor-1, the subprocess /- will follow that redirection. The open(os.devnull, 'w')
call produces a stream whose fileno()
is greater than 2.)
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)
Wenn Sie Bibliotheken von Drittanbietern verwenden können, können Sie möglicherweise etwas wie sarge
(Offenlegung: Ich bin der Betreuer) verwenden. Diese Bibliothek ermöglicht den nicht blockierenden Zugriff auf Ausgabeströme aus Unterprozessen - sie liegt über dem Modul subprocess
.
Eine gute, aber "schwergewichtige" Lösung ist die Verwendung von Twisted - siehe unten.
Wenn Sie bereit sind, nur mit etwas in dieser Richtung zu leben, sollten Sie Folgendes tun:
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 gerade in der Pipe sind.)
Man könnte auch versuchen, dies durch Einfädeln 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 Unterprozessdokumente davon abraten, diese Dateien direkt zu verwenden, und empfehlen, communicate()
zu verwenden (hauptsächlich bei Deadlocks, von denen ich glaube, dass sie oben kein Problem darstellen), und die Lösungen sind ein wenig klunkig, sodass es wirklich so aussieht ) das Subprozessmodul ist nicht ganz der Aufgabe gewachsen (siehe auch: http://www.python.org/dev/peps/pep-3145/ ) und wir brauchen etwas anderes anschauen.
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()
zu erstellen und ProcessProtocol
bereitzustellen, der dann die Ausgabe asynchron verarbeitet. Der Twisted-Beispielcode Python ist hier: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py
Es sieht so aus, als ob die Ausgabe mit Zeilenpufferung für Sie funktionieren würde. (Vorsichtsmaßnahme: Es wurde nicht getestet.) Dadurch wird nur der Standardwert des Unterprozesses in Echtzeit angezeigt. Wenn Sie in Echtzeit sowohl stderr als auch stdout haben möchten, müssen Sie etwas komplexeres mit select
tun.
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...
Lösung 1: Protokollieren Sie stdout
UND stderr
gleichzeitig in Echtzeit
Eine einfache Lösung, die sowohl stdout
als auch stderr
gleichzeitig in Echtzeit protokolliert.
import subprocess as sp
from concurrent.futures import ThreadPoolExecutor
def log_popen_pipe(p, pipe_name):
while p.poll() is None:
line = getattr(p, pipe_name).readline()
log_file.write(line)
with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:
with ThreadPoolExecutor(2) as pool:
r1 = pool.submit(log_popen_pipe, p, 'stdout')
r2 = pool.submit(log_popen_pipe, p, 'stderr')
r1.result()
r2.result()
Lösung 2: Erstellen Sie einen Iterator, der zeilenweise und gleichzeitig in Echtzeit stdout
und stderr
zurückgibt
Hier erstellen wir eine Funktion read_popen_pipes()
, mit der Sie in Echtzeit über beide Pipes (stdout
/stderr
) iterieren 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)
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()
Ausgehend von all dem schlage ich eine leicht modifizierte Version (python3) vor:
None
zurückgegeben hatCode:
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
Warum nicht stdout
direkt auf sys.stdout
setzen? 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)
Neben all diesen Antworten könnte ein einfacher Ansatz wie folgt aussehen:
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 so lange, bis er lesbar ist. Wenn er ein leeres Ergebnis erhält, stoppen Sie ihn.
Der Schlüssel hier ist, dass readline()
eine Zeile (mit \n
am Ende) zurückgibt, solange es eine Ausgabe gibt und leer ist, wenn sie wirklich am Ende ist.
Hoffe das hilft jemandem.
Keine der Pythonic-Lösungen hat für mich funktioniert. Es stellte sich heraus, dass proc.stdout.read()
oder ähnliches für immer blockieren kann.
Deshalb benutze ich tee
so:
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 bereits Shell=True
verwenden.
${PIPESTATUS}
erfasst den Erfolgsstatus der gesamten Befehlskette (nur in Bash verfügbar). Wenn ich den && exit ${PIPESTATUS}
weglasse, würde dies immer Null zurückgeben, da tee
niemals fehlschlägt.
unbuffer
kann erforderlich sein, um jede Zeile sofort in das Terminal zu drucken, anstatt zu lange zu warten, bis der "Pipe Buffer" gefüllt ist. Unbuffer schluckt jedoch den Exit-Status von assert (SIG Abort) ...
2>&1
protokolliert auch den Stderror in der Datei.
Hier ist eine Klasse, die ich in einem meiner Projekte verwende. Er leitet die Ausgabe eines Unterprozesses an das Protokoll weiter. Zuerst habe ich versucht, die write-Methode einfach zu überschreiben, aber das funktioniert nicht, da der Unterprozess sie nie nennen wird (die Umleitung erfolgt auf Ebene des Fidesescriptors). Ich verwende also meine eigene Pipe, ähnlich wie im Subprozess-Modul. Dies hat den Vorteil, dass die gesamte Protokollierungs-/Drucklogik im Adapter gekapselt wird, und Sie können Instanzen des Loggers einfach an Popen
: subprocess.Popen("/path/to/binary", stderr = LogAdapter("foo"))
übergeben.
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 einfach print()
verwenden möchten, können Sie offensichtlich große Teile des Codes entfernen und die Klasse kürzer halten. Sie können es auch um eine __enter__
- und __exit__
-Methode erweitern und finished
in __exit__
aufrufen, damit Sie es problemlos als Kontext verwenden können.
Ähnlich wie die vorherigen Antworten, aber die folgende Lösung funktionierte für mich unter Windows mit Python3, um eine übliche Methode zum Drucken und Anmelden in Echtzeit bereitzustellen ( Getting-Realtime-Ausgabe-using-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()
Alle oben genannten Lösungen, die ich ausprobiert habe, scheiterten entweder daran, die Ausgabe von stderr und stdout (mehrere Pipes) zu trennen, oder sie blockierten für immer, wenn der OS-Pufferspeicher voll war. Dies ist der Fall, wenn der Befehl, den Sie ausführen, zu schnelle Ausgaben ausführt (es gibt eine Warnung für Python) Umfrage () Handbuch des Unterprozesses). Der einzige zuverlässige Weg, den ich gefunden habe, war durch select, 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()
Ich denke, dass die subprocess.communicate
-Methode ein wenig irreführend ist: Sie füllt tatsächlich die stdout und stderr, die Sie in subprocess.Popen
angeben.
Wenn Sie jedoch aus dem subprocess.PIPE
, den Sie den stdout - und stderr -Parametern von subprocess.Popen
zur Verfügung stellen können, lesen Sie die OS-Pufferspeicher und blockieren Ihre App (insbesondere wenn Sie mehrere Prozesse/Threads verwenden, die subprocess
verwenden müssen ).
Meine vorgeschlagene Lösung besteht darin, die Dateien stdout und stderr mit Dateien zu versehen - und den Inhalt der Dateien zu lesen, anstatt aus dem Deadlocking PIPE
zu lesen. Diese Dateien können tempfile.NamedTemporaryFile()
sein, auf die auch während des Schreibens von subprocess.communicate
zum Lesen zugegriffen werden kann.
Nachfolgend finden Sie eine Beispielnutzung:
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, der bereit ist, verwendet zu werden mit so vielen Kommentaren, wie ich liefern könnte, um zu erklären, was er tut:
Wenn Sie Python 2 verwenden, installieren Sie zunächst die neueste Version des Pakets subprocess32 von pypi.
import os
import sys
import threading
import time
import tempfile
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
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:
out = stdout.readline()
# If we've reached the sentinel, then it means that we're done with yielding data
while not self._process_done:
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)
# continue reading as long as there is still data coming from stdout
out = stdout.readline()
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