output live dal comando sottoprocesso


186

Sto usando uno script Python come driver per un codice idrodinamico. Quando arriva il momento di eseguire la simulazione, utilizzo subprocess.Popenil codice, raccolgo l'output da stdout e stderr in un subprocess.PIPE--- quindi posso stampare (e salvare in un file di registro) le informazioni di output e verificare la presenza di errori . Il problema è che non ho idea di come procede il codice. Se lo eseguo direttamente dalla riga di comando, mi dà l'output su quale iterazione è, a che ora, quale sarà il passo temporale successivo, ecc.

Esiste un modo sia per archiviare l'output (per la registrazione e il controllo degli errori), sia per produrre un output in streaming live?

La sezione pertinente del mio codice:

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)

Inizialmente stavo eseguendo il piping del run_commandpassaggio in teemodo che una copia andasse direttamente al file di registro e il flusso continuasse a essere inviato direttamente al terminale, ma in questo modo non riesco a memorizzare alcun errore (per quanto ne so).


Modificare:

Soluzione temporanea:

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

quindi, in un altro terminale, eseguire tail -f log.txt(st log_file = 'log.txt').


1
Forse puoi usare Popen.pollcome in una precedente domanda Stack Overflow .
Paulo Almeida,

Alcuni comandi che mostrano l'indicazione di avanzamento (ad esempio, git) lo fanno solo se il loro output è un "dispositivo tty" (testato tramite libc isatty()). In tal caso potrebbe essere necessario aprire una pseudo-tty.
Torek,

@torek cos'è una (pseudo-) tty?
DilithiumMatrix

2
Dispositivi su sistemi simili a Unix che consentono a un processo di fingere di essere un utente su una porta seriale. Ecco come funziona ssh (lato server), ad esempio. Vedi libreria python pty e anche pexpect .
Torek,

Per quanto riguarda la soluzione temporanea: non è necessario chiamare flushe non è necessario leggere dalla pipe stderr se il sottoprocesso produce molto output stderr. Non c'è abbastanza spazio in un campo di commento per spiegare questo ...
torek,

Risposte:


169

Hai due modi per farlo, creando un iteratore dalle funzioni reado readlinee facendo:

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)

o

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)

Oppure puoi creare un readere un writerfile. Passa writeral Popene leggi dalreader

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

In questo modo avrai i dati scritti test.logsia nell'output standard che in quello standard.

L'unico vantaggio dell'approccio file è che il codice non si blocca. Quindi puoi fare quello che vuoi nel frattempo e leggere ogni volta che vuoi readerin modo non bloccante. Quando si utilizzano PIPE, reade readlinefunzioni bloccherà fino a quando il carattere è scritto al tubo o una linea è scritto rispettivamente al tubo.


1
Ugh :-) scrivere su un file, leggere da esso e dormire in loop? C'è anche una possibilità che il processo finisca prima che tu abbia finito di leggere il file.
Guy Sirton,

13
Con Python 3, è necessario iter(process.stdout.readline, b'')(ovvero, la sentinella passata a iter deve essere una stringa binaria, poiché b'' != ''.
John Mellor,

3
Per i flussi binari, procedere come segue:for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane,

6
Aggiungendo alla risposta di @JohnMellor, in Python 3 erano necessarie le seguenti modifiche: 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
ma l'output non è live, vero? nella mia esperienza, attende solo fino a quando il processo termina l'esecuzione e solo allora stampa sulla console. Link -> stackoverflow.com/questions/30026045/...
denis631

91

Riepilogo (o versione "tl; dr"): è facile quando ce n'è al massimo uno subprocess.PIPE , altrimenti è difficile.

Potrebbe essere il momento di spiegare un po 'come subprocess.Popen funziona.

(Avvertenza: questo è per Python 2.x, sebbene 3.x sia simile; e sono piuttosto confuso sulla variante di Windows. Capisco molto meglio le cose POSIX.)

La Popenfunzione deve gestire da zero a tre flussi I / O, un po 'contemporaneamente. Questi sono indicati stdin, stdoutestderr come al solito.

Puoi fornire:

  • None, indicando che non si desidera reindirizzare lo stream. Invece erediterà questi come al solito. Notare che sui sistemi POSIX, almeno, questo non significa che utilizzerà Python's sys.stdout, solo lo stdout effettivo di Python ; vedi la demo alla fine.
  • Un intvalore. Questo è un descrittore di file "grezzo" (almeno in POSIX). (Nota a margine: PIPEe in STDOUTrealtà lo sonoint internamente, ma sono descrittori "impossibili", -1 e -2.)
  • Un flusso - davvero, qualsiasi oggetto con un filenometodo. Popentroverà il descrittore per quello stream, usando stream.fileno(), e quindi procederà come per un intvalore.
  • subprocess.PIPE, indicando che Python dovrebbe creare una pipe.
  • subprocess.STDOUT( stderrsolo per ): dire a Python di usare lo stesso descrittore di stdout. Questo ha senso solo se hai fornito un Nonevalore (non ) per stdout, e anche allora, è necessario solo se impostato stdout=subprocess.PIPE. (Altrimenti puoi semplicemente fornire lo stesso argomento che hai fornitostdout , ad es Popen(..., stdout=stream, stderr=stream).)

I casi più semplici (senza tubi)

Se non reindirizzi nulla (lasci tutti e tre come Nonevalore predefinito o fornisci esplicito None), Pipeè abbastanza facile. Deve solo svincolare il sottoprocesso e lasciarlo funzionare. Oppure, se si reindirizza a un non PIPE—un into di un flusso fileno()— è ancora facile, poiché il sistema operativo fa tutto il lavoro. Python deve semplicemente escludere il sottoprocesso, collegando lo stdin, lo stdout e / o lo stderr ai descrittori di file forniti.

Il caso ancora semplice: una pipa

Se reindirizzi solo uno stream, le Pipecose sono ancora abbastanza facili. Scegliamo uno stream alla volta e guardiamo.

Supponiamo che tu voglia fornirne alcuni stdin, ma lascialo andare stdoute stderrnon reindirizzato, oppure vai a un descrittore di file. Come processo principale, il tuo programma Python deve semplicemente utilizzare write()per inviare i dati nella pipe. Puoi farlo da solo, ad esempio:

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

oppure puoi passare i dati stdin a proc.communicate(), che quindi fa quanto stdin.writesopra indicato. Non viene restituito alcun output, quindi communicate()ha solo un altro vero lavoro: chiude anche il tubo per te. (Se non si chiama, proc.communicate()è necessario chiamare proc.stdin.close()per chiudere la pipe, in modo che il sottoprocesso sappia che non ci sono più dati in arrivo.)

Supponiamo che tu voglia catturare stdoutma partire stdine stderrsolo. Ancora una volta, è facile: basta chiamare proc.stdout.read()(o equivalente) fino a quando non c'è più output. Poiché proc.stdout()è un normale flusso di I / O Python, puoi usare tutti i normali costrutti su di esso, come:

for line in proc.stdout:

o, ancora una volta, puoi usare proc.communicate(), il che fa semplicemente read()per te.

Se vuoi catturare solo stderr, funziona come con stdout.

C'è ancora un trucco prima che le cose diventino difficili. Supponiamo che tu voglia catturare stdout, e anche catturare stderrma sulla stessa pipe di stdout:

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

In questo caso, subprocess"trucchi"! Bene, deve farlo, quindi non è davvero un imbroglio: avvia il sottoprocesso con sia il suo stdout che il suo stderr diretti nel (singolo) descrittore di pipe che ritorna al suo processo genitore (Python). Sul lato genitore, c'è di nuovo un solo descrittore di pipe per leggere l'output. Tutto l'output "stderr" appare in proc.stdout, e se chiami proc.communicate(), il risultato stderr (secondo valore nella tupla) sarà None, non una stringa.

I casi difficili: due o più tubi

Tutti i problemi sorgono quando si desidera utilizzare almeno due pipe. In effetti, il subprocesscodice stesso ha questo 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:

Ma, ahimè, qui abbiamo realizzato almeno due, e forse tre, pipe diverse, quindi i count(None)rendimenti sono 1 o 0. Dobbiamo fare le cose nel modo più duro.

Su Windows, questo utilizza threading.Threadper accumulare risultati per self.stdoute self.stderre fa in modo che il thread principale fornisca i self.stdindati di input (e quindi chiuda la pipe).

Su POSIX, questo utilizza pollse disponibile, altrimenti select, per accumulare output e fornire input stdin. Tutto ciò viene eseguito nel processo / thread (singolo) padre.

Thread o poll / select sono necessari qui per evitare deadlock. Supponiamo, ad esempio, di aver reindirizzato tutti e tre i flussi a tre tubi separati. Supponiamo inoltre che esista un piccolo limite alla quantità di dati che possono essere inseriti in una pipe prima che il processo di scrittura venga sospeso, in attesa che il processo di lettura "ripulisca" la pipe dall'altra estremità. Impostiamo quel piccolo limite su un singolo byte, solo a scopo illustrativo. (Ecco come funzionano le cose, tranne per il fatto che il limite è molto più grande di un byte.)

Se il processo parent (Python) tenta di scrivere più byte — diciamo 'go\n'a proc.stdin, il primo byte entra e quindi il secondo fa sospendere il processo Python, in attesa che il sottoprocesso legga il primo byte, svuotando la pipe.

Nel frattempo, supponiamo che il sottoprocesso decida di stampare un amichevole "Ciao! Non farti prendere dal panico!" saluto. La Hva nella sua pipe stdout, ma la efa sospendere, aspettando che il suo genitore la leggaH , svuotando la pipe stdout.

Ora siamo bloccati: il processo Python è addormentato, in attesa di finire dicendo "vai", e anche il sottoprocesso è addormentato, in attesa di finire dicendo "Ciao! Non farti prendere dal panico!".

Il subprocess.Popencodice evita questo problema con threading o select / poll. Quando i byte possono oltrepassare le pipe, vanno. Quando non possono, solo un thread (non l'intero processo) deve dormire - o, nel caso di select / poll, il processo Python attende simultaneamente "può scrivere" o "dati disponibili", scrive nello stdin del processo solo quando c'è spazio e legge il suo stdout e / o stderr solo quando i dati sono pronti. Il proc.communicate()codice (in realtà _communicatedove vengono gestiti i casi pelosi) restituisce una volta che tutti i dati stdin (se presenti) sono stati inviati e tutti i dati stdout e / o stderr sono stati accumulati.

Se si desidera leggere entrambi stdoute stderrsu due pipe diverse (indipendentemente dal stdinreindirizzamento), è necessario evitare anche il deadlock. Lo scenario di deadlock qui è diverso - si verifica quando il sottoprocesso scrive qualcosa da molto tempo stderrmentre si stanno estraendo dati stdouto viceversa - ma è ancora lì.


The Demo

Ho promesso di dimostrare che, non reindirizzato, Python subprocesses scrive allo stdout sottostante, no sys.stdout. Quindi, ecco un po 'di codice:

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

Quando eseguito:

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

start show2
hello

Nota che la prima routine fallirà se aggiungi stdout=sys.stdout, poiché un StringIOoggetto non ha fileno. Il secondo ometterà il hellose si aggiunge da stdout=sys.stdoutquando sys.stdoutè stato reindirizzato a os.devnull.

(Se si reindirizza di Python descrittori di file-1, il sottoprocesso sarà seguire quel reindirizzamento. La open(os.devnull, 'w')chiamata produce un flusso di cui fileno()è maggiore di 2)


Hmm. La tua demo sembra mostrare il contrario dell'affermazione alla fine. Stai reindirizzando lo stdout di Python nel buffer ma lo stdout del sottoprocesso sta ancora andando alla console. Quanto è utile? Mi sto perdendo qualcosa?
Guy Sirton,

@GuySirton: la demo mostra che lo stdout del sottoprocesso (quando non esplicitamente diretto a sys.stdout) va allo stdout di Python , non allo stdout del programma python ( sys.). Che ammetto è una ... strana distinzione. C'è un modo migliore per esprimerlo?
Torek,

va bene sapere, ma vogliamo davvero catturare l'output del sottoprocesso qui, quindi cambiare sys.stdout è bello ma non ci aiuta a pensare. Una buona osservazione che comunica deve usare qualcosa come select (), sondaggio o thread.
Guy Sirton,


Ho aggiunto un'implementazione con select ()
sivann

20

Possiamo anche usare l'iteratore di file predefinito per leggere stdout invece di usare il costrutto iter con readline ().

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)

La risposta più elegante qui!
Nir

9
Questa soluzione non viene visualizzata in tempo reale. Attende il completamento del processo e visualizza tutti gli output contemporaneamente. Nella soluzione di Viktor Kerkez, se "tuo_comando" viene visualizzato progressivamente, l'output segue progressivamente, purché "tuo_comando" elimini di tanto in tanto lo stdout (a causa della pipe).
Eric H.

1
@Nir perché non è in diretta.
melMass

Questa soluzione scorre sul descrittore predefinito, quindi si aggiornerà solo quando una riga si aggiorna nell'output. Per un aggiornamento basato sui caratteri è necessario ripetere il metodo read () come mostrato nella soluzione di Viktor. Ma quello era un eccesso per il mio caso d'uso.
Jughead,

11

Se sei in grado di utilizzare librerie di terze parti, potresti essere in grado di utilizzare qualcosa di simile sarge(divulgazione: sono il suo manutentore). Questa libreria consente l'accesso senza blocchi ai flussi di output da sottoprocessi - è sovrapposta asubprocess modulo.


Ottimo lavoro su sarge, BTW. Ciò risolve effettivamente i requisiti del PO, ma potrebbe essere un po 'pesante per quel caso d'uso.
deepelement

Se stai suggerendo uno strumento, mostra almeno un esempio di utilizzo per questo caso esatto.
Serhiy,

4

Soluzione 1: accedere stdoutE stderrcontemporaneamente in tempo reale

Una soluzione semplice che registra contemporaneamente stdout E stderr, riga per riga in tempo reale in un file di registro.

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

Soluzione 2: una funzione read_popen_pipes()che consente di scorrere su entrambe le pipe (stdout / stderr), contemporaneamente in tempo reale

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

3

Una buona soluzione "pesante" è usare Twisted - vedi il fondo.

Se sei disposto a vivere solo con qualcosa di simile, dovrebbe funzionare:

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

(Se usi read () prova a leggere l'intero "file" che non è utile, quello che potremmo davvero usare qui è qualcosa che legge tutti i dati che sono nella pipe in questo momento)

Si potrebbe anche provare ad affrontare questo problema con il threading, ad esempio:

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

Ora potremmo potenzialmente aggiungere anche stderr con due thread.

Si noti tuttavia che i documenti di sottoprocesso scoraggiano l'uso diretto di questi file e raccomandano di utilizzarli communicate()(principalmente relativi ai deadlock che penso non siano un problema sopra) e le soluzioni sono un po 'ingombranti, quindi sembra davvero che il modulo di sottoprocesso non sia abbastanza all'altezza il lavoro (vedi anche: http://www.python.org/dev/peps/pep-3145/ ) e dobbiamo guardare qualcos'altro.

Una soluzione più impegnativa è utilizzare Twisted come mostrato qui: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

Il modo in cui lo fai con Twisted è quello di creare il tuo processo usando reactor.spawnprocess()e fornendo un processo ProcessProtocolche quindi elabora l'output in modo asincrono. Il codice Python di esempio Twisted è qui: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py


Grazie! Ho appena provato qualcosa del genere (basato sul commento di @PauloAlmeida, ma la mia chiamata a sottoprocesso.Popen sta bloccando - cioè arriva al ciclo while una volta che ritorna ...
DilithiumMatrix

1
Non è quello che sta succedendo. Sta entrando subito nel ciclo while, quindi si blocca sulla read()chiamata fino a quando il sottoprocesso non termina e il processo principale riceve EOFsulla pipe.
Alp

@Alp interessante! così è.
DilithiumMatrix

Sì, sono stato troppo veloce per pubblicare questo. In realtà non funziona correttamente e non può essere facilmente risolto. torna al tavolo da disegno.
Guy Sirton,

1
@zhermes: Quindi il problema con read () è che proverà a leggere l'intero output fino a EOF, il che non è utile. readline () aiuta e può essere tutto ciò di cui hai bisogno (anche le righe molto lunghe possono essere un problema). Devi anche fare attenzione al buffering nel processo che stai avviando ...
Guy Sirton

3

Oltre a tutte queste risposte, un semplice approccio potrebbe anche essere il seguente:

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

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

    if not line:
        break

    print(line.strip())

Passa attraverso il flusso leggibile finché è leggibile e se ottiene un risultato vuoto, interrompilo.

La chiave qui è che readline()restituisce una linea (con \nalla fine) fintanto che c'è un output e vuota se è davvero alla fine.

Spero che questo aiuti qualcuno.


3

Sulla base di quanto sopra suggerisco una versione leggermente modificata (python3):

  • mentre il ciclo chiama readline (La soluzione iter suggerita sembrava bloccarmi per sempre - Python 3, Windows 7)
  • strutturato in modo da non dover duplicare la gestione dei dati letti dopo che il sondaggio non restituisce-None
  • stderr collegato a stdout in modo da leggere entrambe le uscite
  • Aggiunto codice per ottenere il valore di uscita di cmd.

Codice:

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

La returncodeparte è stata cruciale nel mio caso.
polvere di stelle

2

Sembra che l'output con buffer di linea funzionerà per te, nel qual caso potrebbe essere adatto qualcosa di simile al seguente. (Avvertenza: non è stata testata.) Ciò fornirà lo stdout del sottoprocesso solo in tempo reale. Se vuoi avere sia stderr che stdout in tempo reale, dovrai fare qualcosa di più complesso 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...

2

Perché non impostare stdoutdirettamente su sys.stdout? E se è necessario eseguire l'output anche su un registro, è possibile semplicemente ignorare il metodo di scrittura di 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)

Non funzionerebbe: il modulo di sottoprocesso esegue il fork e imposta il stdoutdescrittore di file sul descrittore di file dell'oggetto file passato. Il metodo write non verrebbe mai chiamato (almeno è quello che fa il sottoprocesso per stderr, credo sia lo stesso per stdout).
t

2

Tutte le soluzioni di cui sopra che ho provato non sono riuscite a separare l'output stderr e stdout (più pipe) o sono state bloccate per sempre quando il buffer della pipe del sistema operativo era pieno, cosa che accade quando il comando che stai eseguendo genera output troppo velocemente (c'è un avviso per questo su Python poll () manuale del sottoprocesso). L'unico modo affidabile che ho trovato è stato tramite select, ma questa è una soluzione solo per 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()

Un'altra alternativa è quella di staccare un filo per tubo. Ogni thread può eseguire I / O di blocco sul tubo, senza bloccare gli altri thread. Ma questo introduce una sua serie di problemi. Tutti i metodi hanno dei fastidi, devi solo scegliere quello che ritieni meno fastidioso. :-)
torek

2

Simile alle risposte precedenti, ma la seguente soluzione ha funzionato per me su Windows usando Python3 per fornire un metodo comune per stampare e accedere in tempo reale ( 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()

2

Penso che il subprocess.communicatemetodo sia un po 'fuorviante: in realtà riempie il stdout e lo stderr specificati in subprocess.Popen.

Eppure, la lettura dal subprocess.PIPEche si può fornire al subprocess.Popen's stdout e stderr parametri finirà per riempire i buffer di tubo del sistema operativo e situazione di stallo la vostra applicazione (soprattutto se hai più processi / threads che devono utilizzare subprocess).

La mia soluzione proposta è fornire allo stdout e allo stderr i file e leggere il contenuto dei file invece di leggere dal deadlockPIPE . Questi file possono essere tempfile.NamedTemporaryFile()- a cui è possibile accedere anche per la lettura mentre vengono scritti da subprocess.communicate.

Di seguito è riportato un esempio di utilizzo:

        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

E questo è il codice sorgente che è pronto per essere utilizzato con tutti i commenti che potrei fornire per spiegare cosa fa:

Se stai usando python 2, assicurati di installare prima l'ultima versione del pacchetto subprocess32 da 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



1

Ecco una classe che sto usando in uno dei miei progetti. Reindirizza l'output di un sottoprocesso al registro. All'inizio ho provato semplicemente a sovrascrivere il metodo di scrittura, ma questo non funziona in quanto il sottoprocesso non lo chiamerà mai (il reindirizzamento avviene a livello di descrittore di file). Quindi sto usando la mia pipa, simile a come viene fatto nel modulo sottoprocesso. Questo ha il vantaggio di incapsulare tutta la logica di registrazione / stampa nell'adattatore e puoi semplicemente passare le istanze del logger a 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())

Se non hai bisogno di loggarti ma vuoi semplicemente usarlo print()puoi ovviamente rimuovere grandi parti del codice e mantenere la classe più corta. Si potrebbe anche espanderlo con un __enter__e __exit__metodo e chiamare finishedin __exit__modo che si potrebbe facilmente utilizzarlo come contesto.


1

Nessuna delle soluzioni Pythonic ha funzionato per me. Si è scoperto che proc.stdout.read()o simili potrebbero bloccarsi per sempre.

Pertanto, utilizzo in teequesto modo:

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

Questa soluzione è utile se si sta già utilizzando shell=True.

${PIPESTATUS}acquisisce lo stato di successo dell'intera catena di comando (disponibile solo in Bash). Se omettessi il && exit ${PIPESTATUS}, questo restituirebbe sempre zero poiché teenon fallisce mai.

unbufferpotrebbe essere necessario per stampare immediatamente ogni riga nel terminale, invece di attendere troppo a lungo fino a quando il "buffer del tubo" si riempie. Tuttavia, il buffer non ingerisce lo stato di uscita dell'assert (SIG Abort) ...

2>&1 registra anche stderror nel file.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.