J'utilise un script Python en tant que pilote pour un code hydrodynamique. Lorsque vient le temps d’exécuter la simulation, j’utilise subprocess.Popen
pour exécuter le code, rassemble la sortie de stdout et stderr dans un subprocess.PIPE
--- puis je peux imprimer (et enregistrer dans un fichier journal) les informations de sortie et vérifier des erreurs. Le problème est que je n'ai aucune idée de la progression du code. Si je le lance directement à partir de la ligne de commande, il me restitue le résultat de l'itération, l'heure, le prochain pas, etc.
Existe-t-il un moyen de stocker la sortie (pour la journalisation et la vérification des erreurs), ainsi que de produire une sortie en streaming en direct?
La section pertinente de mon code:
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)
À l'origine, je pipais les run_command
à tee
de sorte qu'une copie soit directement insérée dans le fichier journal et que le flux soit toujours envoyé directement au terminal - mais de cette manière, je ne peux pas stocker d'erreur (à ma connaissance).
Modifier:
Solution temporaire:
ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, Shell=True )
while not ret_val.poll():
log_file.flush()
ensuite, dans un autre terminal, exécutez tail -f log.txt
(à la place de log_file = 'log.txt'
).
Vous avez deux façons de le faire, soit en créant un itérateur à partir des fonctions read
ou readline
et en effectuant:
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)
ou
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)
Ou vous pouvez créer un fichier reader
et un fichier writer
. Passez la writer
à la Popen
et lisez-la à partir de la 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())
De cette façon, vous aurez les données écrites dans le test.log
ainsi que sur la sortie standard.
Le seul avantage de l'approche par fichier est que votre code ne bloque pas. Ainsi, vous pouvez faire ce que vous voulez entre-temps et lire quand vous le voulez dans la variable reader
de manière non bloquante. Lorsque vous utilisez les fonctions PIPE
, read
et readline
, elles sont bloquées jusqu'à ce qu'un caractère soit écrit dans le canal ou qu'une ligne soit écrite dans le canal, respectivement.
subprocess.PIPE
, sinon c'est difficile.Il est peut-être temps d'expliquer un peu comment subprocess.Popen
fait son travail.
(Mise en garde: ceci concerne Python 2.x, bien que 3.x soit similaire; et je suis assez flou sur la variante Windows. Je comprends beaucoup mieux la substance POSIX.)
La fonction Popen
doit traiter simultanément trois flux d'E/S de zéro à trois. Ceux-ci sont notés stdin
, stdout
et stderr
comme d'habitude.
Vous pouvez fournir:
None
, indiquant que vous ne voulez pas rediriger le flux. Il en héritera comme d'habitude à la place. Notez que sur les systèmes POSIX, au moins, cela ne signifie pas qu’il utilisera le sys.stdout
de Python, mais uniquement le réel stdout de Python; voir la démo à la fin.int
. C'est un descripteur de fichier "brut" (au moins dans POSIX). (Remarque: PIPE
et STDOUT
sont en fait int
s en interne, mais sont des descripteurs "impossibles", -1 et -2.)fileno
. Popen
trouvera le descripteur de ce flux à l'aide de stream.fileno()
, puis procédera comme pour une valeur int
.subprocess.PIPE
, indiquant que Python doit créer un canal.subprocess.STDOUT
(pour stderr
uniquement): indiquez à Python d'utiliser le même descripteur que pour stdout
. Cela n’a de sens que si vous avez fourni une valeur (non -None
) pour stdout
, et même dans ce cas, elle n’est que nécessaire si vous définissez stdout=subprocess.PIPE
. (Sinon, vous pouvez simplement fournir le même argument que vous avez fourni pour stdout
, par exemple, Popen(..., stdout=stream, stderr=stream)
.)Si vous ne redirigez rien (laissez les trois comme valeur par défaut None
ou fournissez _ explicite None
), Pipe
l'a tout à fait facile. Il suffit de désactiver le sous-processus et de le laisser s'exécuter. Ou, si vous redirigez vers un non-PIPE
— un int
ou un fileno()
d'un flux __—, cela reste simple, car le système d'exploitation effectue tout le travail. Python doit simplement désactiver le sous-processus, en connectant ses stdin, stdout et/ou stderr aux descripteurs de fichier fournis.
Si vous redirigez un seul flux, Pipe
a encore des choses assez faciles. Choisissons un flux à la fois et regardons.
Supposons que vous souhaitiez fournir des éléments stdin
, mais que stdout
et stderr
ne soient pas redirigés ou soient dirigés vers un descripteur de fichier. En tant que processus parent, votre programme Python doit simplement utiliser write()
pour envoyer des données en aval. Vous pouvez le faire vous-même, par exemple:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc
ou vous pouvez transmettre les données stdin à proc.communicate()
, qui effectue ensuite le stdin.write
présenté ci-dessus. Aucune sortie ne revenant, donc communicate()
n'a qu'un seul autre travail réel: il ferme également le tuyau pour vous. (Si vous n'appelez pas proc.communicate()
, vous devez appeler proc.stdin.close()
pour fermer le canal, afin que le sous-processus sache qu'il ne reste plus de données.)
Supposons que vous souhaitiez capturer stdout
tout en laissant stdin
et stderr
seuls. Encore une fois, c’est simple: appelez simplement proc.stdout.read()
(ou l’équivalent) jusqu’à ce qu’il n’y ait plus de sortie. Puisque proc.stdout()
est un flux d'E/S normal Python, vous pouvez utiliser toutes les constructions normales, comme:
for line in proc.stdout:
ou encore, vous pouvez utiliser proc.communicate()
, qui fait simplement la read()
pour vous.
Si vous voulez capturer uniquement stderr
, cela fonctionne de la même façon qu'avec stdout
.
Il y a encore un tour avant que les choses ne deviennent difficiles. Supposons que vous vouliez capturer stdout
, mais aussi stderr
mais sur le même canal que stdout:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
Dans ce cas, subprocess
"triche"! Eh bien, il doit le faire, afin de ne pas tricher vraiment: il démarre le sous-processus avec à la fois son stdout et son stderr dirigés vers le descripteur de canal (unique) qui renvoie son processus parent (Python). Du côté des parents, il n’existe à nouveau qu’un seul descripteur de canal pour la lecture de la sortie. Toute la sortie "stderr" apparaît dans proc.stdout
, et si vous appelez proc.communicate()
, le résultat de stderr (la deuxième valeur du tuple) sera None
et non une chaîne.
Les problèmes surviennent tous lorsque vous voulez utiliser au moins deux tuyaux. En fait, le code subprocess
a lui-même ce 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:
Mais hélas, ici nous avons créé au moins deux, et peut-être trois, tuyaux différents, donc la count(None)
renvoie 1 ou 0. Nous devons faire les choses à la dure.
Sous Windows, cela utilise threading.Thread
pour accumuler les résultats pour self.stdout
et self.stderr
, et le thread parent fournit les données d'entrée self.stdin
(puis ferme le canal).
Sur POSIX, ceci utilise poll
si disponible, sinon select
, pour accumuler la sortie et fournir une entrée stdin. Tout cela s'exécute dans le processus/thread parent (unique).
Des fils de discussion ou des sélections/sélections sont nécessaires ici pour éviter un blocage. Supposons, par exemple, que nous ayons redirigé les trois flux vers trois canaux distincts. Supposons en outre qu'il y ait une petite limite sur la quantité de données pouvant être insérées dans un tuyau avant que le processus d'écriture ne soit suspendu, en attendant que le processus de lecture "nettoie" le tuyau de l'autre extrémité. Fixons cette petite limite à un seul octet, juste pour illustration. (C'est en fait comme ça que ça marche, sauf que la limite est beaucoup plus grande qu'un octet.)
Si le processus parent (Python) essaie d'écrire plusieurs octets, par exemple, 'go\n'
to proc.stdin
, le premier octet entre et le deuxième octet provoque la suspension du processus Python, en attente du sous-processus. lire le premier octet, vider le tuyau.
Pendant ce temps, supposons que le sous-processus décide d'imprimer un message convivial "Bonjour! Ne paniquez pas!" salutation. La H
entre dans son tuyau stdout, mais le e
le suspend, attendant que son parent lise ce H
, vidant le tuyau stdout.
Nous sommes maintenant bloqués: le processus Python est endormi et attend de pouvoir terminer "Go". Le sous-processus est également assoupi et attend de pouvoir terminer par "Bonjour! Ne paniquez pas!".
Le code subprocess.Popen
évite ce problème avec threading-or-select/poll. Quand les octets peuvent passer par dessus les tuyaux, ils vont. Quand ils ne peuvent pas, seul un thread (pas tout le processus) doit se mettre en veille - ou, dans le cas de select/poll, le processus Python attend simultanément "peut écrire" ou "données disponibles", écrit au stdin du processus uniquement lorsqu'il y a de la place et lit son stdout et/ou son stderr uniquement lorsque les données sont prêtes. Le code proc.communicate()
(en réalité _communicate
où les cas compliqués sont traités) est renvoyé une fois que toutes les données stdin (le cas échéant) ont été envoyées et que toutes les données stdout et/ou stderr ont été accumulées.
Si vous voulez lire à la fois stdout
et stderr
sur deux canaux différents (quelle que soit la redirection stdin
, vous devez également éviter les interblocages. Le scénario d'interblocage est différent ici: il se produit lorsque le sous-processus écrit quelque chose de long sur stderr
pendant que vous extrayez des données de stdout
, ou inversement, mais il existe toujours.
J'ai promis de démontrer que, non redirigé, Python subprocess
es écrit sur la sortie standard sous-jacente, et non sys.stdout
. Donc, voici un 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()
Quand exécuté:
$ python out.py
start show1
hello
in buffer: sys.stdout being buffered
start show2
hello
Notez que la première routine échouera si vous ajoutez stdout=sys.stdout
, car un objet StringIO
n'a pas de fileno
. Le second omettra la hello
si vous ajoutez stdout=sys.stdout
puisque sys.stdout
a été redirigé vers os.devnull
.
(Si vous redirigez le descripteur de fichier-1 de Python, le sous-processus will suivra cette redirection. L'appel open(os.devnull, 'w')
produit un flux dont la fonction fileno()
est supérieure à 2.)
Nous pouvons également utiliser l'itérateur de fichier par défaut pour lire stdout au lieu d'utiliser iter construct avec readline ().
import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
sys.stdout.write(line)
Si vous pouvez utiliser des bibliothèques tierces, vous pourrez peut-être utiliser quelque chose comme sarge
(divulgation: je suis son responsable). Cette bibliothèque permet un accès non bloquant aux flux de sortie des sous-processus - elle est superposée au module subprocess
.
Une bonne solution "lourde" consiste à utiliser Twisted - voir ci-dessous.
Si vous êtes prêt à vivre avec seulement stdout, quelque chose dans ce sens devrait marcher:
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
(Si vous utilisez read (), il essaie de lire le "fichier" complet, ce qui n'est pas utile. Ce que nous pourrions vraiment utiliser ici est quelque chose qui lit toutes les données contenues dans le tuyau actuellement.)
On pourrait aussi essayer d’aborder ceci avec des threads, par exemple:
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
Nous pouvons maintenant potentiellement ajouter stderr en ayant deux threads.
Notez cependant que les documents de sous-processus découragent l’utilisation directe de ces fichiers et vous conseillent d’utiliser communicate()
(principalement concernés par les blocages, ce qui, à mon avis, n’est pas un problème ci-dessus) et les solutions sont un peu klunky, il semble donc que le module de sous-processus n’est pas à la hauteur du travail(voir aussi: http://www.python.org/dev/peps/pep-3145/ ) et nous besoin de regarder quelque chose d'autre.
Une solution plus complexe consiste à utiliser Twisted comme indiqué ci-après: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html
Pour ce faire, utilisez Twisted est de créer votre processus en utilisant reactor.spawnprocess()
et en fournissant un ProcessProtocol
qui traite ensuite la sortie de manière asynchrone. Le code exemple Twisted Python est ici: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py
Il semble que la sortie avec tampon de ligne fonctionnera pour vous. Dans ce cas, les éléments suivants pourraient vous convenir. (Avertissement: ce n'est pas testé.) Cela ne donnera que la sortie standard du sous-processus en temps réel. Si vous voulez avoir stderr et stdout en temps réel, vous devrez faire quelque chose de plus complexe avec 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...
Solution 1: Journal stdout
ET stderr
simultanément en temps réel
Une solution simple qui enregistre simultanément stdout
ET stderr
en temps réel .
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()
Solution 2: Créer un itérateur qui retourne stdout
et stderr
ligne par ligne, simultanément et en temps réel
Nous créons ici une fonction read_popen_pipes()
qui vous permet d'itérer sur les deux tuyaux (stdout
/stderr
) simultanément, en temps réel:
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()
Sur la base de tout ce qui précède, je suggère une version légèrement modifiée (python3):
None
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
Pourquoi ne pas définir stdout
directement à sys.stdout
? Et si vous devez également générer un journal, vous pouvez simplement remplacer la méthode d'écriture de f.
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)
En plus de toutes ces réponses, une approche simple pourrait également être la suivante:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
while process.stdout.readable():
line = process.stdout.readline()
if not line:
break
print(line.strip())
Parcourez le flux lisible tant qu'il est lisible et si le résultat est vide, arrêtez-le.
La clé ici est que readline()
renvoie une ligne (avec \n
à la fin) tant qu’il existe une sortie et vide si elle est vraiment à la fin.
J'espère que ça aide quelqu'un.
Aucune des solutions Pythonic n'a fonctionné pour moi. Il s'est avéré que proc.stdout.read()
ou similaire peut bloquer pour toujours.
Par conséquent, j'utilise tee
comme ceci:
subprocess.run('./my_long_running_binary 2>&1 | tee -a my_log_file.txt && exit ${PIPESTATUS}', Shell=True, check=True, executable='/bin/bash')
Cette solution est pratique si vous utilisez déjà Shell=True
.
${PIPESTATUS}
capture le statut de réussite de toute la chaîne de commande (disponible uniquement dans Bash). Si j'omettais le && exit ${PIPESTATUS}
, cela renverrait toujours zéro puisque tee
n'échoue jamais.
unbuffer
peut être nécessaire pour imprimer chaque ligne immédiatement dans le terminal, au lieu d'attendre beaucoup trop longtemps jusqu'à ce que le "tampon tampon" soit rempli. Cependant, unbuffer avale le statut de sortie d'assert (SIG Abort) ...
2>&1
enregistre également stderror dans le fichier.
Voici un cours que j'utilise dans l'un de mes projets. Il redirige la sortie d'un sous-processus vers le journal. Au début, j'ai simplement essayé d'écraser la méthode write, mais cela ne fonctionne pas, car le sous-processus ne l'appellera jamais (la redirection se produit au niveau de la commande filescript). J'utilise donc mon propre canal, similaire à celui utilisé dans le module de sous-processus. Cela présente l'avantage d'encapsuler toute la logique de journalisation/d'impression dans l'adaptateur et vous pouvez simplement passer des instances du consignateur à 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())
Si vous n'avez pas besoin de vous connecter mais souhaitez simplement utiliser print()
, vous pouvez évidemment supprimer de grandes parties du code et raccourcir la classe. Vous pouvez également le développer avec une méthode __enter__
et __exit__
et appeler finished
dans __exit__
afin de pouvoir facilement l'utiliser comme contexte.
Semblable aux réponses précédentes, mais la solution suivante a fonctionné pour moi sous Windows utilisant Python3 pour fournir une méthode commune d'impression et de connexion en temps réel ( getting-realtime-output-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()
Toutes les solutions ci-dessus que j'ai essayées n'ont pas réussi à séparer les sorties stderr et stdout (plusieurs canaux), ou ont été bloquées indéfiniment lorsque le tampon de tubes OS était saturé, ce qui se produit lorsque la commande que vous exécutez les sorties trop rapidement (il existe un avertissement à cet effet sur python poll () manuel du sous-processus). Le seul moyen fiable que j'ai trouvé était de sélectionner, mais il s'agit d'une solution posix uniquement:
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()
Je pense que la méthode subprocess.communicate
est un peu trompeuse: elle remplit en fait les stdout et stderr que vous spécifiez dans le subprocess.Popen
.
Cependant, la lecture à partir du subprocess.PIPE
que vous pouvez fournir aux paramètres stdout et stderr de subprocess.Popen
finira par remplir les tampons de tubes OS et bloquer votre application (surtout si vous avez plusieurs processus/threads qui doivent utiliser subprocess
).
La solution que je propose consiste à fournir des fichiers à stdout et stderr - et à lire le contenu des fichiers au lieu de les lire dans la colonne morte PIPE
. Ces fichiers peuvent être tempfile.NamedTemporaryFile()
- auxquels on peut également accéder en lecture pendant qu'ils sont écrits par subprocess.communicate
.
Voici un exemple d'utilisation:
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
Et ceci est le code source qui est prêt à être utilisé avec autant de commentaires que je pourrais en donner pour expliquer son rôle:
Si vous utilisez python 2, assurez-vous d’abord d’installer la dernière version du paquet subprocess32 de 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