sortie en direct de la commande de sous-processus

186

J'utilise un script python comme pilote pour un code hydrodynamique. Quand vient le temps d'exécuter la simulation, j'utilise subprocess.Popenpour exécuter le code, collecter 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 les erreurs. . Le problème est que je n'ai aucune idée de la progression du code. Si je l'exécute directement à partir de la ligne de commande, cela me donne une sortie sur l'itération, à quelle heure, quelle est la prochaine étape de temps, etc.

Existe-t-il un moyen à la fois de stocker la sortie (pour la journalisation et la vérification des erreurs) et de produire également une sortie 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 diffusais le run_command grâce teeafin qu'une copie est allé directement au fichier journal, et le flux de sortie encore directement à la borne - mais cette façon , je ne peux pas stocker des erreurs (à mon knowlege).


Éditer:

Solution temporaire:

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

puis, dans un autre terminal, exécutez tail -f log.txt(st log_file = 'log.txt').

DilithiumMatrix
la source
1
Peut-être que vous pouvez utiliser Popen.pollcomme dans une question précédente de Stack Overflow .
Paulo Almeida
Certaines commandes qui affichent une indication de progression (par exemple git) ne le font que si leur sortie est un "périphérique tty" (testé via libc isatty()). Dans ce cas, vous devrez peut-être ouvrir un pseudo-tty.
torek
@torek qu'est-ce qu'un (pseudo-) tty?
DilithiumMatrix
2
Périphériques sur des systèmes de type Unix qui permettent à un processus de se faire passer pour un utilisateur sur un port série. C'est ainsi que fonctionne ssh (côté serveur), par exemple. Voir la bibliothèque python pty , et aussi pexpect .
torek
Solution temporaire: il n'y a pas besoin d'appeler flush, et il est nécessaire de lire à partir du tube stderr si le sous-processus produit beaucoup de sortie stderr. Il n'y a pas assez de place dans un champ de commentaire pour expliquer cela ...
torek

Réponses:

169

Vous avez deux façons de faire cela, soit en créant un itérateur à partir des fonctions readou readlineet faites:

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)

ou

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)

Ou vous pouvez créer readerun writerfichier et un fichier. Passez le writerau Popenet lisez lereader

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 fichier est que votre code ne se bloque pas. Ainsi, vous pouvez faire ce que vous voulez en attendant et lire quand vous le souhaitez de readermanière non bloquante. Lorsque vous utilisezPIPE , readet les readlinefonctions se bloquent jusqu'à ce qu'un caractère soit écrit dans le tuyau ou qu'une ligne soit écrite dans le tuyau respectivement.

Viktor Kerkez
la source
1
Ugh :-) écrire dans un fichier, lire à partir de celui-ci et dormir dans la boucle? Il est également possible que le processus se termine avant que vous ayez fini de lire le fichier.
Guy Sirton
13
Avec Python 3, vous avez besoin iter(process.stdout.readline, b'')(c'est-à-dire que la sentinelle transmise à iter doit être une chaîne binaire, puisque b'' != ''.
John Mellor
3
Pour les flux binaires, procédez comme for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
suit
6
En ajoutant à la réponse de @JohnMellor, dans Python 3 les modifications suivantes étaient nécessaires: 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
mais la sortie n'est pas en direct, n'est-ce pas? d'après mon expérience, il attend juste que le processus se termine et imprime ensuite sur la console. Lien -> stackoverflow.com/questions/30026045/…
denis631
91

Executive Summary (ou version "tl; dr"): c'est facile quand il y en a au plus un subprocess.PIPE , sinon c'est dur.

Il est peut-être temps d'expliquer un peu comment subprocess.Popen ça marche.

(Attention: c'est pour Python 2.x, bien que 3.x soit similaire; et je suis assez flou sur la variante Windows. Je comprends beaucoup mieux les choses POSIX.)

La Popenfonction doit traiter de zéro à trois flux d'E / S, un peu simultanément. Ceux - ci sont indiqués stdin, stdoutetstderr comme d' habitude.

Vous pouvez fournir:

  • None, indiquant que vous ne souhaitez pas rediriger le flux. Il en héritera comme d'habitude à la place. Notez qu'au moins sur les systèmes POSIX, cela ne signifie pas qu'il utilisera Python sys.stdout, juste le stdout réel de Python ; voir la démo à la fin.
  • Une intvaleur. Il s'agit d'un descripteur de fichier "brut" (au moins dans POSIX). (Note latérale: PIPEet STDOUTsont en fait ints en interne, mais sont des descripteurs "impossibles", -1 et -2.)
  • Un flux - vraiment, n'importe quel objet avec une filenométhode. Popentrouvera le descripteur de ce flux en utilisant stream.fileno(), puis procédera comme pour une intvaleur.
  • subprocess.PIPE, indiquant que Python doit créer un tube.
  • subprocess.STDOUT(pour stderrseulement): dites à Python d'utiliser le même descripteur que pour stdout. Cela n'a de sens que si vous avez fourni une Nonevaleur (non- ) pour stdout, et même dans ce cas, elle n'est nécessaire que si vous définissez stdout=subprocess.PIPE. (Sinon, vous pouvez simplement fournir le même argument que vous avez fourni stdout, par exemple Popen(..., stdout=stream, stderr=stream).)

Les cas les plus simples (pas de tuyaux)

Si vous ne redirigez rien (laissez les trois comme Nonevaleur par défaut ou fournissez explicite None), Pipec'est assez facile. Il a juste besoin de faire tourner le sous-processus et de le laisser s'exécuter. Ou, si vous redirigez vers un non PIPE- intun flux ou un flux fileno()- c'est toujours facile, car le système d'exploitation fait tout le travail. Python a juste besoin de faire tourner le sous-processus, en connectant son stdin, stdout et / ou stderr aux descripteurs de fichier fournis.

Le cas toujours facile: un tuyau

Si vous ne redirigez qu'un seul flux, les Pipechoses restent assez faciles. Choisissons un flux à la fois et regardons.

Supposons que vous voulez fournir quelques - uns stdin, mais laisser stdoutet stderraller non redirigé, ou aller à un descripteur de fichier. En tant que processus parent, votre programme Python doit simplement l'utiliser write()pour envoyer des données dans le tube. 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 les opérations stdin.writeci-dessus. Il n'y a pas de sortie qui revient donc communicate()n'a qu'un seul autre vrai travail: 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 n'y a plus de données qui transitent.)

Supposons que vous vouliez capturer stdoutmais partir stdinet stderrseul. Encore une fois, c'est facile: il suffit d'appeler proc.stdout.read()(ou équivalent) jusqu'à ce qu'il n'y ait plus de sortie. Puisqu'il proc.stdout()s'agit d'un flux d'E / S Python normal, vous pouvez utiliser toutes les constructions normales dessus, comme:

for line in proc.stdout:

ou, encore une fois, vous pouvez utiliser proc.communicate(), ce qui fait simplement le read()pour vous.

Si vous souhaitez capturer uniquement stderr, cela fonctionne de la même manière qu'avec stdout.

Il y a encore une astuce avant que les choses ne deviennent difficiles. Supposons que vous souhaitiez capturer stdout, et également capturer stderrmais sur le même tube que stdout:

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

Dans ce cas, subprocess"triche"! Eh bien, il doit le faire, donc ce n'est pas vraiment de la triche: il démarre le sous-processus avec à la fois son stdout et son stderr dirigés dans le (unique) descripteur de tube qui renvoie à son processus parent (Python). Du côté parent, il n'y a là encore qu'un seul descripteur de tube pour lire la sortie. Toute la sortie "stderr" apparaît dans proc.stdout, et si vous appelez proc.communicate(), le résultat stderr (deuxième valeur dans le tuple) sera None, pas une chaîne.

Les étuis rigides: deux ou plusieurs tuyaux

Les problèmes surviennent tous lorsque vous souhaitez utiliser au moins deux tuyaux. En fait, le subprocesscode lui-même a 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 fait au moins deux, et peut-être trois, tuyaux différents, donc les count(None)retours sont 1 ou 0. Nous devons faire les choses à la dure.

Sous Windows, cela permet threading.Threadd'accumuler les résultats pour self.stdoutet self.stderr, et le thread parent fournit self.stdinles données d'entrée (puis ferme le canal).

Sur POSIX, cela utilise pollsi disponible, sinon select, pour accumuler la sortie et fournir une entrée stdin. Tout cela s'exécute dans le processus / thread parent (unique).

Les threads ou poll / select sont nécessaires ici pour éviter les blocages. 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 tube avant que le processus d'écriture ne soit suspendu, en attendant que le processus de lecture "nettoie" le tube de l'autre extrémité. Définissons cette petite limite à un seul octet, juste à titre d'illustration. (C'est en fait ainsi que les choses fonctionnent, sauf que la limite est beaucoup plus grande qu'un octet.)

Si le processus parent (Python) essaie d'écrire plusieurs octets - disons, 'go\n'vers proc.stdin, le premier octet entre, puis le second entraîne la suspension du processus Python, attendant que le sous-processus lise le premier octet, vidant le tube.

En attendant, supposons que le sous-processus décide d'imprimer un "Hello! Don't Panic!" salutation. Le Hva dans son tube stdout, mais le esuspend, en attendant que son parent lise cela H, vidant le tube stdout.

Maintenant nous sommes bloqués: le processus Python est endormi, attendant de finir de dire "allez", et le sous-processus est également endormi, attendant de finir de dire "Bonjour! Ne paniquez pas!".

Le subprocess.Popencode évite ce problème avec threading-or-select / poll. Lorsque les octets peuvent traverser les tuyaux, ils disparaissent. Quand ils ne peuvent pas, seul un thread (pas tout le processus) doit dormir - ou, dans le cas de select / poll, le processus Python attend simultanément "can write" ou "data available", écrit dans le stdin du processus seulement quand il y a de la place, et lit son stdout et / ou stderr seulement lorsque les données sont prêtes. Le proc.communicate()code (en fait _communicateoù les cas poilus sont traités) retourne 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 souhaitez lire les deux stdoutet stderrsur deux canaux différents (quelle que soit la stdinredirection), vous devrez également éviter les interblocages. Le scénario de blocage ici est différent - il se produit lorsque le sous-processus écrit quelque chose de long stderrpendant que vous extrayez des données stdout, ou vice versa - mais il est toujours là.


La démo

J'ai promis de démontrer que, non redirigé, Python subprocessécrit dans le stdout sous-jacent, non sys.stdout. Alors, voici un peu de 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()

Lors de l'exécution:

$ 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 StringIOobjet n'en a pas fileno. Le second omettra hellosi vous ajoutez stdout=sys.stdoutdepuis sys.stdouta été redirigé vers os.devnull.

(Si vous redirigez fichier descripteur-1 Python, le sous - processus va suivre cette redirection. L' open(os.devnull, 'w')appel produit un courant dont fileno()est supérieur à 2.)

Torek
la source
Hmm. Votre démo semble montrer le contraire de la réclamation à la fin. Vous redirigez le stdout de Python dans le tampon mais le sous-processus stdout va toujours vers la console. En quoi est-ce utile? Est-ce que je manque quelque chose?
Guy Sirton
@GuySirton: la démo montre que le sous-processus stdout (lorsqu'il n'est pas explicitement dirigé vers sys.stdout) va vers le stdout de Python , pas vers le stdout du programme python ( sys.). Ce que j'admets est une ... distinction étrange. Y a-t-il une meilleure façon d'exprimer cela?
torek
c'est bon à savoir mais nous voulons vraiment capturer la sortie du sous-processus ici, donc changer sys.stdout est cool mais ne nous aide pas je pense. Une bonne observation qui communique doit utiliser quelque chose comme select (), poll ou threads.
Guy Sirton
J'ai ajouté une implémentation avec select ()
sivann
20

Nous pouvons également utiliser l'itérateur de fichier par défaut pour lire stdout au lieu d'utiliser la construction iter avec readline ().

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)
Jughead
la source
La réponse la plus élégante ici!
Nir
9
Cette solution ne s'affiche pas en temps réel. Il attend que le processus soit terminé et affiche toutes les sorties en même temps. Dans la solution de Viktor Kerkez, si "your_command" s'affiche progressivement, la sortie suit progressivement, tant que "your_command" vide stdout de temps en temps (à cause du tube).
Eric H.
1
@Nir parce que ce n'est pas en direct.
melMass
Cette solution itère sur le descripteur par défaut, elle ne sera donc mise à jour que lorsqu'une ligne est mise à jour dans la sortie. Pour une mise à jour basée sur les caractères, vous devez itérer sur la méthode read () comme indiqué dans la solution de Viktor. Mais c'était exagéré pour mon cas d'utilisation.
Jughead
11

Si vous êtes en mesure d'utiliser des bibliothèques tierces, vous pourrez peut-être utiliser quelque chose comme sarge(divulgation: je suis son mainteneur). Cette bibliothèque permet un accès non bloquant aux flux de sortie des sous-processus - elle est superposée au subprocessmodule.

Vinay Sajip
la source
Beau travail sur Sarge, BTW. Cela résout en effet l'exigence du PO, mais pourrait être un peu lourd pour ce cas d'utilisation.
deepelement
Si vous suggérez un outil, montrez au moins un exemple d'utilisation pour ce cas précis.
Serhiy
4

Solution 1: Connectez stdoutET stderrsimultanément en temps réel

Une solution simple qui enregistre simultanément stdout ET stderr, ligne par ligne en temps réel dans un fichier journal.

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()

Solution 2: Une fonction read_popen_pipes()qui vous permet d'itérer sur les deux tubes (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)

# 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
la source
3

Une bonne solution, mais «lourde», consiste à utiliser Twisted - voir en bas.

Si vous êtes prêt à vivre avec seulement stdout, quelque chose de ce genre devrait fonctionner:

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" entier, ce qui n'est pas utile, ce que nous pourrions vraiment utiliser ici, c'est quelque chose qui lit toutes les données qui se trouvent dans le tube en ce moment)

On pourrait également essayer d'aborder cela 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

Maintenant, nous pourrions également ajouter stderr en ayant deux threads.

Notez cependant que la documentation de sous-processus décourage l'utilisation de ces fichiers directement et recommande d'utiliser communicate()(principalement concerné par les blocages qui, je pense, ne sont pas un problème ci-dessus) et les solutions sont un peu klunky donc il semble vraiment que le module de sous-processus n'est pas tout à fait à la hauteur le travail (voir aussi: http://www.python.org/dev/peps/pep-3145/ ) et nous devons regarder autre chose.

Une solution plus complexe consiste à utiliser Twisted comme indiqué ici: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

La façon dont vous faites cela avec Twisted consiste à créer votre processus en utilisant reactor.spawnprocess()et en fournissant un ProcessProtocolqui traite ensuite la sortie de manière asynchrone. L'exemple de code Python Twisted est ici: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py

Guy Sirton
la source
Merci! J'ai juste essayé quelque chose comme ça (basé sur le commentaire de @PauloAlmeida, mais mon appel à subprocess.Popen bloque - c'est-à-dire qu'il ne vient à la boucle while qu'une fois qu'il revient ...
DilithiumMatrix
1
Ce n'est pas ce qui se passe. Il entre tout de suite dans la boucle while, puis bloque l' read()appel jusqu'à ce que le sous-processus se termine et que le processus parent reçoive EOFsur le tube.
Alp
@Alp intéressant! donc c'est.
DilithiumMatrix
Ouais, j'ai été trop rapide pour publier ça. Cela ne fonctionne pas correctement et ne peut pas être facilement corrigé. retour à la table de dessin.
Guy Sirton
1
@zhermes: Donc le problème avec read () est qu'il essaiera de lire la sortie entière jusqu'à EOF, ce qui n'est pas utile. readline () aide et peut être tout ce dont vous avez besoin (de très longues lignes peuvent aussi être un problème). Vous devez également faire attention à la mise en mémoire tampon dans le processus que vous lancez ...
Guy Sirton
3

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())

Faites une boucle dans le flux lisible tant qu'il est lisible et s'il obtient un résultat vide, arrêtez-le.

La clé ici est que readline()renvoie une ligne (avec \nà la fin) tant qu'il y a une sortie et vide si c'est vraiment à la fin.

J'espère que cela aide quelqu'un.

Kabirbaidhya
la source
3

Sur la base de tout ce qui précède, je suggère une version légèrement modifiée (python3):

  • en appelant en boucle readline (La solution iter suggérée semblait bloquer pour toujours pour moi - Python 3, Windows 7)
  • structuré de sorte que la gestion des données lues n'a pas besoin d'être dupliquée après que le sondage ne retourne pas -None
  • stderr est acheminé vers stdout pour que les deux sorties de sortie soient lues
  • Code ajouté pour obtenir la valeur de sortie de cmd.

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
la source
La returncodepartie était cruciale dans mon cas.
stardust
2

Il semble que la sortie tamponnée en ligne fonctionnera pour vous, auquel cas quelque chose comme ce qui suit pourrait convenir. (Attention: il n'a pas été testé.) Cela ne donnera la sortie standard du sous-processus qu'en temps réel. Si vous voulez avoir à la fois 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...
Alp
la source
2

Pourquoi ne pas définir stdoutdirectement sur sys.stdout? Et si vous avez également besoin de générer une sortie dans 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)
xaav
la source
Cela ne fonctionnerait pas: le module de sous-processus fourche et définit le stdoutdescripteur de fichier sur le descripteur de fichier de l'objet fichier transmis. La méthode write ne serait jamais appelée (du moins c'est ce que fait le sous-processus pour stderr, je suppose que c'est la même chose pour stdout).
t.animal
2

Toutes les solutions ci-dessus que j'ai essayées n'ont pas réussi à séparer la sortie stderr et stdout, (plusieurs tubes) ou bloquées à jamais lorsque le tampon du tube du système d'exploitation était plein, ce qui se produit lorsque la commande que vous exécutez sort trop rapidement (il y a un avertissement à ce sujet sur python poll () manuel du sous-processus). Le seul moyen fiable que j'ai trouvé était de sélectionner, mais c'est une solution uniquement posix:

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
la source
Une autre alternative consiste à créer un filetage par tuyau. Chaque thread peut bloquer les E / S sur le tuyau, sans bloquer les autres threads. Mais cela introduit son propre ensemble de problèmes. Toutes les méthodes ont des ennuis, il vous suffit de choisir laquelle (s) vous trouvez le moins ennuyeux. :-)
torek
2

Similaire aux réponses précédentes, mais la solution suivante a fonctionné pour moi sur Windows utilisant Python3 pour fournir une méthode commune pour imprimer et se connecter 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()
scottysbasement
la source
2

Je pense que la subprocess.communicateméthode est un peu trompeuse: elle remplit en fait les stdout et stderr que vous spécifiez dans le fichiersubprocess.Popen .

Pourtant, la lecture de la subprocess.PIPEque vous pouvez fournir aux subprocess.Popens » stdout et stderr finira par remplir les paramètres des tampons de tuyaux OS et interblocage votre application (surtout si vous avez plusieurs processus / threads qui doivent utiliser subprocess).

Ma solution proposée est de fournir les fichiers stdout et stderr - et de lire le contenu des fichiers au lieu de lire depuis le blocage PIPE. Ces fichiers peuvent être tempfile.NamedTemporaryFile()- qui peuvent également être consultés pour la lecture pendant leur écriture 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 voici le code source qui est prêt à être utilisé avec autant de commentaires que je pourrais fournir pour expliquer ce qu'il fait:

Si vous utilisez python 2, s'il vous plaît assurez - vous d' abord installer la dernière version du subprocess32 paquet de 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
la source
1

Voici une classe 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 essayé d'écraser simplement la méthode d'écriture, mais cela ne fonctionne pas car le sous-processus ne l'appellera jamais (la redirection se produit au niveau du descripteur de fichier). J'utilise donc mon propre tube, de la même manière que dans le sous-processus-module. Cela présente l'avantage d'encapsuler toute la logique de journalisation / d'impression dans l'adaptateur et vous pouvez simplement transmettre des instances de l'enregistreur à 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 journalisation mais que print()vous souhaitez simplement l'utiliser, vous pouvez évidemment supprimer de grandes parties du code et réduire la classe. Vous pouvez également étendre par une __enter__et __exit__méthode et appeler finisheden __exit__sorte que vous pouvez facilement l' utiliser comme contexte.

t. animal
la source
1

Aucune des solutions pythoniques 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 teecomme 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 l'état de réussite de l'ensemble de la chaîne de commande (uniquement disponible dans Bash). Si j'omettais le && exit ${PIPESTATUS}, alors cela renverrait toujours zéro puisque teejamais échoue.

unbufferpeut être nécessaire pour imprimer chaque ligne immédiatement dans le terminal, au lieu d'attendre trop longtemps jusqu'à ce que le "tampon de tube" soit rempli. Cependant, le débuffeur avale le statut de sortie d'assert (SIG Abort) ...

2>&1 enregistre également stderror dans le fichier.

Mike76
la source