2012-06-22 23 views
5

Sto provando a scrivere un programma python in grado di interagire con altri programmi. Ciò significa inviare stdin e ricevere dati stdout. Non riesco a usare pexpect (sebbene abbia sicuramente ispirato parte del design). Il processo che sto utilizzando in questo momento è questa:Utilizzo del sottoprocesso con select e pty si blocca quando si cattura l'output

  1. Collegare un pty sullo standard output del sottoprocesso
  2. loop fino a quando le uscite di processo parziali controllando subprocess.poll
    • Quando ci sono dati disponibili nel stdout scrivere che i dati immediatamente allo stdout corrente.
  3. Fine!

Sto prototipando un codice (di seguito) che funziona ma sembra avere un difetto che mi infastidisce. Al completamento del processo secondario, il processo padre si blocca se non si specifica un timeout quando si utilizza select.select. Preferirei davvero non impostare un timeout. Sembra solo un po 'sporco. Tuttavia, tutti gli altri modi in cui ho cercato di aggirare il problema non sembrano funzionare. Pexpect sembra aggirarlo usando os.execv e pty.fork invece di subprocess.Popen e pty.openpty una soluzione che non preferisco. Sto facendo qualcosa di sbagliato con come controllo la vita del sottoprocesso? Il mio approccio è errato?

Il codice che sto utilizzando è di seguito. Lo sto usando su Mac OS X 10.6.8, ma ho bisogno che funzioni anche su Ubuntu 12.04.

Questo è il corridore sottoprocesso runner.py:

import subprocess 
import select 
import pty 
import os 
import sys 

def main(): 
    master, slave = pty.openpty() 

    process = subprocess.Popen(['python', 'outputter.py'], 
      stdin=subprocess.PIPE, 
      stdout=slave, stderr=slave, close_fds=True) 

    while process.poll() is None: 
     # Just FYI timeout is the last argument to select.select 
     rlist, wlist, xlist = select.select([master], [], []) 
     for f in rlist: 
      output = os.read(f, 1000) # This is used because it doesn't block 
      sys.stdout.write(output) 
      sys.stdout.flush() 
    print "**ALL COMPLETED**" 

if __name__ == '__main__': 
    main() 

Questo è il codice sottoprocesso outputter.py. Le strane parti casuali servono solo a simulare un programma che emette dati a intervalli casuali. Puoi rimuoverlo se lo desideri. Esso non dovrebbe importare:

import time 
import sys 
import random 

def main(): 
    lines = ['hello', 'there', 'what', 'are', 'you', 'doing'] 
    for line in lines: 
     sys.stdout.write(line + random.choice(['', '\n'])) 
     sys.stdout.flush() 
     time.sleep(random.choice([1,2,3,4,5])/20.0) 
    sys.stdout.write("\ndone\n") 
    sys.stdout.flush() 

if __name__ == '__main__': 
    main() 

Grazie per tutto l'aiuto che tutto può fornire!

nota Extra

Pty viene utilizzato perché voglio assicurare che stdout non è tamponata.

risposta

10

Prima di tutto, os.read blocca, contrariamente a quanto affermi. Tuttavia, non blocca dopo select. Anche os.read su un descrittore di file chiuso restituisce sempre una stringa vuota, che potresti voler controllare.

Il vero problema è che il descrittore del dispositivo master non viene mai chiuso, quindi l'ultimo select è quello che bloccherà. In una rara condizione di competizione, il processo figlio è terminato tra select e process.poll() e il programma si chiude in modo soddisfacente. Il più delle volte però i blocchi selezionati per sempre.

Se installi il gestore di segnale come proposto da izhak, si scatena l'inferno; ogni volta che un processo figlio viene terminato, viene eseguito il gestore di segnale. Dopo che il gestore di segnale è stato eseguito, la chiamata di sistema originale in quel thread non può essere proseguita, in modo che l'invocazione di syscall restituisca un errore diverso da zero, che spesso determina l'eccezione casuale lanciata in python. Ora, se altrove nel tuo programma usi qualche libreria con chiamate di sistema di blocco che non sanno come gestire tali eccezioni, sei in grossi guai (qualsiasi os.read per esempio ovunque ora può lanciare un'eccezione, anche dopo un successo select) .

Pesando che le eccezioni casuali sono state scagliate ovunque contro il polling un po ', non penso che il timeout su select non sembri una cattiva idea. Il tuo processo non sarebbe comunque l'unico (lento) processo di polling sul sistema.

+0

Grazie per la fantastica spiegazione. Ho pensato, dopo un po ', che probabilmente sarebbe stato meglio impostare un timeout. Ho provato la soluzione di izhak ma sì, ho visto un comportamento molto strano dopo averlo fatto. Questo aiuta molto! – ravenac95

+0

Per il mio miglioramento, puoi spiegare perché la mia risposta è venuta meno? Dovrebbe farti evitare di usare qualsiasi timeout. –

+0

Ho implementato i tuoi suggerimenti in [la risposta a una domanda correlata] (http://stackoverflow.com/a/12471855/4279) – jfs

0

Da quello che ho capito, non è necessario utilizzare pty. runner.py possono essere modificati come

import subprocess 
import sys 

def main(): 
     process = subprocess.Popen(['python', 'outputter.py'], 
         stdin=subprocess.PIPE, 
         stdout=subprocess.PIPE, stderr=subprocess.PIPE) 

     while process.poll() is None: 
       output = process.stdout.readline() 
       sys.stdout.write(output) 
       sys.stdout.flush() 
     print "**ALL COMPLETED**" 

if __name__ == '__main__': 
     main() 

process.stdout.read(1) possono essere usati al posto di process.stdout.readline() per l'uscita in tempo reale per carattere dal sottoprocesso.

Nota: se non si richiede l'emissione in tempo reale dal sottoprocesso, utilizzare Popen.communicate per evitare il ciclo di polling.

+0

panickal: Grazie per la risposta, ma in realtà voglio garantire che qualsiasi output non sia bufferizzato, da qui la necessità di pty. Modificherò la domanda per chiarire che è un requisito. – ravenac95

+0

Se i programmi 'runner.py' stanno interagendo con quelli di python, è possibile aggiungere' python -u' al comando Popen per abilitare l'output non bufferizzato. Ho provato con 'outputter.py' e ha funzionato. – panickal

+0

sfortunatamente, non saranno sempre applicazioni python: -/ – ravenac95

0

Quando il processo secondario termina, il processo padre riceve il segnale SIGCHLD.Per impostazione predefinita, questo segnale viene ignorato, ma è possibile intercettarlo:

import sys 
import signal 

def handler(signum, frame): 
    print 'Child has exited!' 
    sys.exit(0) 

signal.signal(signal.SIGCHLD, handler) 

Il segnale dovrebbe rompere la chiamata di sistema di bloccaggio per 'selezionare' o 'leggere' (o qualsiasi altra cosa si è in) e ti permettono di fare tutto ciò che è necessario (pulizia, uscita, ecc.) nella funzione di gestore.

8

Ci sono un certo numero di cose che puoi cambiare per rendere il tuo codice corretto. La cosa più semplice che posso pensare è solo chiudere la copia del processo genitore dello slave fd dopo la foratura, in modo che quando il bambino chiude e chiude il proprio fd slave, il genitore select.select() segnerà il master come disponibile per la lettura, e il successivo os.read() darà un risultato vuoto e il programma verrà completato. (Il master Pty non vedrà lo schiavo finisce come essere chiuso fino a quando entrambi i le copie del fd schiavi sono chiusi.)

Quindi, solo una linea:

os.close(slave) 

..placed subito dopo la Chiamata subprocess.Popen, dovrebbe risolvere il tuo problema.

Tuttavia, ci sono risposte migliori, a seconda di quali sono esattamente le vostre esigenze. Come notato da qualcun altro, non è necessario un pty solo per evitare il buffering. È possibile utilizzare un valore pty.openpty()pty.openpty() (e considerare esattamente il valore restituito allo stesso valore). Una pipa del sistema operativo non sarà mai bufferizzata; se il processo figlio non esegue il buffering dell'output, le tue chiamate select() e os.read() non vedranno neanche il buffering. Avresti comunque bisogno della linea os.close(slave).

Ma è possibile che sia necessario un pty per diversi motivi. Se alcuni dei tuoi programmi figlio si aspettano di essere eseguiti in modo interattivo per la maggior parte del tempo, potrebbero verificare se il loro stdin è pty e si comporta in modo diverso a seconda della risposta (molte utilità comuni lo fanno). Se vuoi davvero che il bambino pensi che abbia un terminale assegnato, allora il modulo pty è la strada da percorrere. A seconda di come verrà eseguito runner.py, potrebbe essere necessario passare dall'uso di subprocess a pty.fork(), in modo che il figlio abbia il suo ID di sessione impostato e il pty pre-aperto (o vedere l'origine di pty.py per vedere cosa fa e duplicare le parti appropriate nell'oggetto preexec_fn dell'oggetto sottoprocesso richiamabile).

+0

Infatti, il descrittore slave non è stato chiuso, e il mio male per non averlo notato. Tuttavia, questa linea non è ancora abbastanza da sola, dal momento che os.read reagisce all'uccisione del processo figlio con errno = EIO, quindi tutte le letture devono essere protette con try-except controllando errno = EIO e il motivo dietro di esso. –

+0

Hmm, non ci dovrebbero essere motivi per ottenere EIO durante la lettura da una pipe. Dal lato della lettura, dovresti ottenere una breve lettura sotto la semantica POSIX (quindi in questo caso, la stringa vuota - il pitone EOF). –

+1

Beh, ho provato e ho ottenuto EIO, 80% di tempo, su linux 3.2 –