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
comogrep --line-buffered
opython -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 dospty
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, establezcastderr=PIPE
y usep.stderr.fileno()
lugar demasters[1]
. El comentario ensh
source sugiere que hay problemas sistderr not in {STDOUT, pipe}
Estoy tratando de encontrar una manera en Python para ejecutar otros programas de tal manera que:
- El stdout y el stderr del programa que se está ejecutando se pueden registrar por separado.
- 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)
- 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 utilizaselect
, 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)