python subprocess tty pexpect pty

python - Ejecute el comando y obtenga su stdout, stderr por separado en tiempo casi real como en una terminal



subprocess tty (3)

El stdout y el stderr del programa que se está ejecutando se pueden registrar por separado.

No puede usar pexpect porque tanto stdout como stderr van a la misma pty y no hay forma de separarlos después de eso.

El stdout y stderr del programa que se está ejecutando se puede ver casi en tiempo real, de modo que si el proceso secundario se bloquea, el usuario puede ver. (es decir, no esperamos que se complete la ejecución antes de imprimir el stdout / stderr al usuario)

Si la salida de un subproceso no es un tty, entonces es probable que use un búfer en bloque y, por lo tanto, si no produce mucha salida, entonces no será en "tiempo real" , por ejemplo, si el búfer es 4K, entonces su padre El proceso de Python no verá nada hasta que el proceso secundario imprima caracteres 4K y el búfer se desborde o se vacíe explícitamente (dentro del subproceso). Este búfer está dentro del proceso secundario y no hay formas estándar de administrarlo desde afuera. Aquí hay una imagen que muestra los buffers estándar y el buffer de tubería para el command 1 | command2 command 1 | command2 comandos shell2:

El programa que se está ejecutando no sabe que se está ejecutando a través de Python y, por lo tanto, no hará cosas inesperadas (como fragmentar su salida en lugar de imprimirlo en tiempo real o salir porque requiere un terminal para ver su salida).

Parece que quisiste decir lo contrario, es decir, es probable que tu proceso hijo fragmente su salida en lugar de vaciar cada línea de salida lo antes posible si la salida se redirige a una tubería (cuando usas stdout=PIPE en Python). Significa que las soluciones de threading o asincio predeterminadas no funcionarán como en su caso.

Hay varias opciones para solucionarlo:

  • el comando puede aceptar un argumento de línea de grep --line-buffered como grep --line-buffered o python -u , para deshabilitar el búfer en bloque.

  • stdbuf funciona para algunos programas , es decir, puede ejecutar [''stdbuf'', ''-oL'', ''-eL''] + command utilizando la solución de enhebrado o asincio anterior y debería obtener stdout, stderr por separado y las líneas deberían aparecer en casi real hora:

    #!/usr/bin/env python3 import os import sys from select import select from subprocess import Popen, PIPE with Popen([''stdbuf'', ''-oL'', ''-e0'', ''curl'', ''www.google.com''], stdout=PIPE, stderr=PIPE) as p: readable = { p.stdout.fileno(): sys.stdout.buffer, # log separately p.stderr.fileno(): sys.stderr.buffer, } while readable: for fd in select(readable, [], [])[0]: data = os.read(fd, 1024) # read available if not data: # EOF del readable[fd] else: readable[fd].write(data) readable[fd].flush()

  • Finalmente, puede probar la solución pty + select con dos pty s:

    #!/usr/bin/env python3 import errno import os import pty import sys from select import select from subprocess import Popen masters, slaves = zip(pty.openpty(), pty.openpty()) with Popen([sys.executable, ''-c'', r''''''import sys, time print(''stdout'', 1) # no explicit flush time.sleep(.5) print(''stderr'', 2, file=sys.stderr) time.sleep(.5) print(''stdout'', 3) time.sleep(.5) print(''stderr'', 4, file=sys.stderr) ''''''], stdin=slaves[0], stdout=slaves[0], stderr=slaves[1]): for fd in slaves: os.close(fd) # no input readable = { masters[0]: sys.stdout.buffer, # log separately masters[1]: sys.stderr.buffer, } while readable: for fd in select(readable, [], [])[0]: try: data = os.read(fd, 1024) # read available except OSError as e: if e.errno != errno.EIO: raise #XXX cleanup del readable[fd] # EIO means EOF on some systems else: if not data: # EOF del readable[fd] else: readable[fd].write(data) readable[fd].flush() for fd in masters: os.close(fd)

    No sé cuáles son los efectos secundarios del uso de diferentes pty para stdout, stderr. Puede probar si una sola pty es suficiente en su caso, por ejemplo, establezca stderr=PIPE y use p.stderr.fileno() lugar de masters[1] . El comentario en sh source sugiere que hay problemas si stderr not in {STDOUT, pipe}

Estoy tratando de encontrar una manera en Python para ejecutar otros programas de tal manera que:

  1. El stdout y el stderr del programa que se está ejecutando se pueden registrar por separado.
  2. El stdout y stderr del programa que se está ejecutando se puede ver casi en tiempo real, de modo que si el proceso secundario se bloquea, el usuario puede verlo. (es decir, no esperamos que se complete la ejecución antes de imprimir el stdout / stderr al usuario)
  3. Criterios adicionales: el programa que se está ejecutando no sabe que se está ejecutando a través de Python y, por lo tanto, no hará cosas inesperadas (como fragmentar su salida en lugar de imprimirlo en tiempo real o salir porque requiere un terminal para ver su salida) . Este pequeño criterio significa que tendremos que usar una empanada, creo.

Esto es lo que tengo hasta ahora ... Método 1:

def method1(command): ## subprocess.communicate() will give us the stdout and stderr sepurately, ## but we will have to wait until the end of command execution to print anything. ## This means if the child process hangs, we will never know.... proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable=''/bin/bash'') stdout, stderr = proc.communicate() # record both, but no way to print stdout/stderr in real-time print '' ######### REAL-TIME ######### '' ######## Not Possible print '' ########## RESULTS ########## '' print ''STDOUT:'' print stdout print ''STDOUT:'' print stderr

Método 2

def method2(command): ## Using pexpect to run our command in a pty, we can see the child''s stdout in real-time, ## however we cannot see the stderr from "curl google.com", presumably because it is not connected to a pty? ## Furthermore, I do not know how to log it beyond writing out to a file (p.logfile). I need the stdout and stderr ## as strings, not files on disk! On the upside, pexpect would give alot of extra functionality (if it worked!) proc = pexpect.spawn(''/bin/bash'', [''-c'', command]) print '' ######### REAL-TIME ######### '' proc.interact() print '' ########## RESULTS ########## '' ######## Not Possible

Método 3:

def method3(command): ## This method is very much like method1, and would work exactly as desired ## if only proc.xxx.read(1) wouldn''t block waiting for something. Which it does. So this is useless. proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable=''/bin/bash'') print '' ######### REAL-TIME ######### '' out,err,outbuf,errbuf = '''','''','''','''' firstToSpeak = None while proc.poll() == None: stdout = proc.stdout.read(1) # blocks stderr = proc.stderr.read(1) # also blocks if firstToSpeak == None: if stdout != '''': firstToSpeak = ''stdout''; outbuf,errbuf = stdout,stderr elif stderr != '''': firstToSpeak = ''stderr''; outbuf,errbuf = stdout,stderr else: if (stdout != '''') or (stderr != ''''): outbuf += stdout; errbuf += stderr else: out += outbuf; err += errbuf; if firstToSpeak == ''stdout'': sys.stdout.write(outbuf+errbuf);sys.stdout.flush() else: sys.stdout.write(errbuf+outbuf);sys.stdout.flush() firstToSpeak = None print '''' print '' ########## RESULTS ########## '' print ''STDOUT:'' print out print ''STDERR:'' print err

Para probar estos métodos, deberá import sys,subprocess,pexpect

Pexpect es Python puro y se puede tener con

sudo pip install pexpect

Creo que la solución involucrará el módulo pty de python, que es algo así como un arte negro que no puedo encontrar a nadie que sepa cómo usarlo. Quizás SO sabe :) Como aviso, le recomiendo que use ''curl www.google.com'' como comando de prueba, porque por algún motivo imprime su estado en stderr: D

ACTUALIZACIÓN-1:
OK, entonces la biblioteca pty no es apta para el consumo humano. Los documentos, esencialmente, son el código fuente. Aquí no funcionará ninguna solución presentada que esté bloqueando y no asincrónica. El método Threads / Queue de Padraic Cunningham funciona muy bien, aunque no es posible agregar soporte pty, y es ''sucio'' (para citar #python de Freenode). Parece que la única solución adecuada para el código estándar de producción es usar el marco Twisted, que incluso admite pty como un interruptor booleano para ejecutar procesos exactamente como si fueran invocados desde el shell. Pero agregar Twisted a un proyecto requiere una reescritura total de todo el código. Esto es un fastidio total: /

ACTUALIZACIÓN-2:

Se proporcionaron dos respuestas, una de las cuales aborda los dos primeros criterios y funcionará bien donde solo necesita tanto stdout como stderr usando Threads and Queue . La otra respuesta utiliza select , un método sin bloqueo para leer descriptores de archivos, y pty, un método para "engañar" al proceso generado para que crea que se está ejecutando en un terminal real como si se ejecutara directamente desde Bash, pero puede o no Es posible que no tenga efectos secundarios. Desearía poder aceptar ambas respuestas, porque el método "correcto" realmente depende de la situación y de por qué estás subprocesando en primer lugar, pero, por desgracia, solo podía aceptar una.


Si bien la respuesta de JF Sebastian ciertamente resuelve el corazón del problema, estoy ejecutando Python 2.7 (que no estaba en el criterio original), así que solo estoy lanzando esto a cualquier otro viajero cansado que solo quiera cortar / pegar un código. Todavía no lo he probado completamente, pero en todos los comandos que he probado parece funcionar perfectamente :) es posible que desee cambiar .decode (''ascii'') a .decode (''utf-8''), todavía estoy probando ese bit fuera.

#!/usr/bin/env python2.7 import errno import os import pty import sys from select import select import subprocess stdout = '''' stderr = '''' command = ''curl google.com ; sleep 5 ; echo "hey"'' masters, slaves = zip(pty.openpty(), pty.openpty()) p = subprocess.Popen(command, stdin=slaves[0], stdout=slaves[0], stderr=slaves[1], shell=True, executable=''/bin/bash'') for fd in slaves: os.close(fd) readable = { masters[0]: sys.stdout, masters[1]: sys.stderr } try: print '' ######### REAL-TIME ######### '' while readable: for fd in select(readable, [], [])[0]: try: data = os.read(fd, 1024) except OSError as e: if e.errno != errno.EIO: raise del readable[fd] finally: if not data: del readable[fd] else: if fd == masters[0]: stdout += data.decode(''ascii'') else: stderr += data.decode(''ascii'') readable[fd].write(data) readable[fd].flush() except: pass finally: p.wait() for fd in masters: os.close(fd) print '''' print '' ########## RESULTS ########## '' print ''STDOUT:'' print stdout print ''STDERR:'' print stderr


Si desea leer desde stderr y stdout y obtener el resultado por separado, puede usar un subproceso con una cola, no demasiado probado pero algo como lo siguiente:

import threading import queue def run(fd, q): for line in iter(fd.readline, ''''): q.put(line) q.put(None) def create(fd): q = queue.Queue() t = threading.Thread(target=run, args=(fd, q)) t.daemon = True t.start() return q, t process = Popen(["curl","www.google.com"], stdout=PIPE, stderr=PIPE, universal_newlines=True) std_q, std_out = create(process.stdout) err_q, err_read = create(process.stderr) while std_out.is_alive() or err_read.is_alive(): for line in iter(std_q.get, None): print(line) for line in iter(err_q.get, None): print(line)