2013-02-13 12 views
5

Ho un programma Python che avvia i sottoprocessi usando Popen e consuma il loro output quasi in tempo reale mentre viene prodotto. Il codice del loop relativo è:Rilevamento della fine del flusso su popen.stdout.readline

def run(self, output_consumer): 
    self.prepare_to_run() 
    popen_args = self.get_popen_args() 
    logging.debug("Calling popen with arguments %s" % popen_args) 
    self.popen = subprocess.Popen(**popen_args) 
    while True: 
     outdata = self.popen.stdout.readline() 
     if not outdata and self.popen.returncode is not None: 
      # Terminate when we've read all the output and the returncode is set 
      break 
     output_consumer.process_output(outdata) 
     self.popen.poll() # updates returncode so we can exit the loop 
    output_consumer.finish(self.popen.returncode) 
    self.post_run() 

def get_popen_args(self): 
    return { 
     'args': self.command, 
     'shell': False, # Just being explicit for security's sake 
     'bufsize': 0, # More likely to see what's being printed as it happens 
         # Not guarantted since the process itself might buffer its output 
         # run `python -u` to unbuffer output of a python processes 
     'cwd': self.get_cwd(), 
     'env': self.get_environment(), 
     'stdout': subprocess.PIPE, 
     'stderr': subprocess.STDOUT, 
     'close_fds': True, # Doesn't seem to matter 
    } 

Questa grande opera sulle macchine di produzione, ma sulla mia macchina dev, la chiamata a .readline() si blocca quando alcuni sottoprocessi completa. Cioè, elaborerà con successo tutto l'output, compresa la riga di output finale che dice "processo completato", ma poi eseguirà nuovamente il sondaggio readline e non tornerà mai più. Questo metodo si chiude correttamente sulla macchina di sviluppo per la maggior parte dei sottoprocessi che chiamo, ma non riesce costantemente ad uscire per uno script bash complesso che a sua volta chiama molti sottoprocessi.

Vale la pena notare che popen.returncode viene impostato su un valore non None (in genere 0) molte righe prima della fine dell'output. Quindi non posso interrompere il ciclo quando è impostato, altrimenti perdo tutto ciò che viene sputato alla fine del processo e viene ancora memorizzato nel buffer in attesa di lettura. Il problema è che quando sto scaricando il buffer a quel punto, non posso dire quando sono alla fine perché l'ultima chiamata a readline() si blocca. Chiama anche read(). Chiamando lo read(1) mi viene estratto l'ultimo carattere, ma si blocca anche dopo la linea finale. popen.stdout.closed è sempre False. Come posso dire quando sono alla fine?

Tutti i sistemi eseguono python 2.7.3 su Ubuntu 12.04LTS. FWIW, stderr viene unito a stdout utilizzando stderr=subprocess.STDOUT.

Perché la differenza? Non riesce a chiudere stdout per qualche motivo? I sottoprocessi potrebbero fare qualcosa per tenerlo aperto in qualche modo? Potrebbe essere perché sto avviando il processo da un terminale sulla mia scatola di sviluppo, ma in produzione è lanciato come demone tramite supervisord? Questo cambierebbe il modo in cui i tubi vengono lavorati e, in caso affermativo, come li normalizzo?

+0

non è il problema che stai leggendo una linea da un processo che non esiste più? –

+0

Non credo. Se l'errore fosse così semplice, fallirebbe ovunque, sempre. – Leopd

+0

Perché non puoi rompere semplicemente con '' not outdata'' – sotapme

risposta

1

Il ciclo del codice principale sembra giusto. Potrebbe essere che il tubo non si sta chiudendo perché un altro processo lo tiene aperto. Ad esempio, se lo script avvia un processo in background che scrive su stdout, la pipe non si chiude. Sei sicuro che nessun altro processo figlio sia ancora in esecuzione?

Un'idea è di cambiare modalità quando viene impostato il numero .returncode. Una volta che sai che il processo principale è terminato, leggi tutto il suo output dal buffer, ma non rimanere bloccato in attesa. È possibile utilizzare select per leggere dalla pipe con un timeout. Imposta un intervallo di alcuni secondi e puoi cancellare il buffer senza rimanere bloccato in attesa del processo figlio.

1

Senza conoscere il contenuto dell '"unico script di bash" che causa il problema, ci sono troppe possibilità per determinare la causa esatta.

Tuttavia, concentrandosi sul fatto che si afferma che funziona se si esegue il tuo script Python sotto supervisord, potrebbe bloccarsi se un sottoprocesso sta cercando di leggere da stdin, o semplicemente si comporta diversamente se stdin è un tty, che (presumo) supervisord reindirizzerà da /dev/null.

Questo esempio minimo sembra gestire meglio i casi in cui il mio esempio test.sh esegue sottoprocessi che provano a leggere da stdin ...

import os 
import subprocess 

f = subprocess.Popen(args='./test.sh', 
        shell=False, 
        bufsize=0, 
        stdin=open(os.devnull, 'rb'), 
        stdout=subprocess.PIPE, 
        stderr=subprocess.STDOUT, 
        close_fds=True) 

while 1: 
    s = f.stdout.readline() 
    if not s and f.returncode is not None: 
     break 
    print s.strip() 
    f.poll() 
print "done %d" % f.returncode 

In caso contrario, si può sempre ripiegare ad usare un non-blocking read, e tirare fuori dai guai quando si ottiene la linea di output finale dicendo "processo completo", anche se è un po 'di hack.

2

Se si utilizza readline() o read(), non dovrebbe bloccarsi. Non è necessario controllare il codice di ritorno o il sondaggio(). Se è sospeso quando sai che il processo è finito, è molto probabilmente un sottoprocesso che tiene aperta la tua pipa, come altri hanno detto prima.

Ci sono due cose che potreste fare per eseguire il debug di questo: * provare a riprodurre con uno script minima invece degli attuali complessa, o * eseguire lo script complesso con strace -f -e clone,execve,exit_group e vedere che cosa è che lo script di partenza, e se qualsiasi processo è sopravvissuto allo script principale (controlla quando lo script principale chiama exit_group, se strace è ancora in attesa, hai un figlio ancora vivo).

0

Perché stai impostando il sdterr su STDOUT?

Il vero vantaggio di effettuare una chiamata a una sottoprocessione in una sottoproces consiste nel fatto che è possibile recuperare una tupla che contiene la risposta di stdout e il me stage di stderr.

Questi potrebbero essere utili se la logica dipende dal loro successo o errore.

Inoltre, ti salverà dal dolore di dover scorrere le linee. Comunicare() ti dà tutto e non ci sarebbero questioni irrisolte circa se o non il messaggio completo è stato ricevuto

+0

'communicate' attende il termine del processo. La mia applicazione richiede l'elaborazione dell'output così come viene prodotto. – Leopd

1

Trovo che chiama a read (o readline) a volte appendere, nonostante in precedenza chiamando poll. Così ho fatto ricorso a select per scoprire se ci sono dati leggibili. Tuttavia, select senza un timeout può bloccarsi anche se il processo è stato chiuso. Quindi chiamo select in un loop semi-busy con un timeout minuscolo per ogni iterazione (vedi sotto).

Non sono sicuro che sia possibile adattarlo a readline, in quanto readline potrebbe bloccarsi se manca l'\n finale o se il processo non chiude il suo stdout prima di chiudere lo stdin e/o terminarlo. È possibile avvolgere questo in un generatore e ogni volta che si incontra uno \n in stdout_collected, restituire la riga corrente.

Si noti inoltre che nel mio codice reale, sto usando pseudoterminali (pty) per racchiudere i manici del popen (per un input dell'utente più fasullo) ma dovrebbe funzionare senza.

# handle to read from 
handle = self.popen.stdout 

# how many seconds to wait without data 
timeout = 1 

begin = datetime.now() 
stdout_collected = "" 

while self.popen.poll() is None: 
    try: 
     fds = select.select([handle], [], [], 0.01)[0] 
    except select.error, exc: 
     print exc 
     break 

    if len(fds) == 0: 
     # select timed out, no new data 
     delta = (datetime.now() - begin).total_seconds() 
     if delta > timeout: 
      return stdout_collected 

     # try longer 
     continue 
    else: 
     # have data, timeout counter resets again 
     begin = datetime.now() 

    for fd in fds: 
     if fd == handle: 
      data = os.read(handle, 1024) 
      # can handle the bytes as they come in here 
      # self._handle_stdout(data) 
      stdout_collected += data 

# process exited 
# if using a pseudoterminal, close the handles here 
self.popen.wait() 
0

ho scritto un demo con sottoprocesso bash che possono essere facilmente esplorato. A tubo chiuso può essere riconosciuto da '' nell'uscita da readline(), mentre l'uscita da una riga vuota è '\n'.

from subprocess import Popen, PIPE, STDOUT 
p = Popen(['bash'], stdout=PIPE, stderr=STDOUT) 
out = [] 
while True: 
    outdata = p.stdout.readline() 
    if not outdata: 
     break 
    #output_consumer.process_output(outdata) 
    print "* " + repr(outdata) 
    out.append(outdata) 
print "* closed", repr(out) 
print "* returncode", p.wait() 

Esempio di input/output: chiusura del tubo distintamente prima di terminare il processo.Questo è il motivo wait() dovrebbe essere usato al posto di poll()

[prompt] $ python myscript.py 
echo abc 
* 'abc\n' 
exec 1>&- # close stdout 
exec 2>&- # close stderr 
* closed ['abc\n'] 
exit 
* returncode 0 
[prompt] $ 

Il codice ha fatto produrre un numero enorme di stringhe vuote per questo caso.


Esempio: veloce processo chiuso senza '\n' sull'ultima riga:

echo -n abc 
exit 
* 'abc' 
* closed ['abc'] 
* returncode 0 
Problemi correlati