web-dev-qa-db-fra.com

sortie en direct de la commande de sous-processus

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').

134
DilithiumMatrix

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.

123
Viktor Kerkez

Résumé (ou "tl; dr" version): c'est facile quand il y a au plus un 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.
  • Une valeur int. C'est un descripteur de fichier "brut" (au moins dans POSIX). (Remarque: PIPE et STDOUT sont en fait ints en interne, mais sont des descripteurs "impossibles", -1 et -2.)
  • Un flux - en réalité, tout objet avec une méthode 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).)

Les cas les plus faciles (pas de tuyaux)

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.

Le cas toujours facile: un tuyau

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 cas difficiles: deux ou plusieurs tuyaux

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.


La démo

J'ai promis de démontrer que, non redirigé, Python subprocesses é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.)

79
torek

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)
13
Jughead

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.

10
Vinay Sajip

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

2
Guy Sirton

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...
1
Alp

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

1
Rotareti

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

  • boucle While en appelant readline (La solution proposée par iter semblait bloquer pour moi toujours - Python 3, Windows 7)
  • structuré, il n'est donc pas nécessaire de dupliquer le traitement des données lues après que l'interrogation renvoie la variable non -None
  • stderr passe dans stdout pour que les deux sorties soient lues
  • Ajout du code 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
0
mrtnlrsn

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)
0
xaav

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.

0
kabirbaidhya

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.

0
Mike76

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.

0
t.animal

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

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

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


0
Victor Klapholz