salida en vivo del comando de subproceso
Estoy usando un script en Python como controlador para un código hidrodinámico. Cuando llega el momento de ejecutar la simulación, suelo subprocess.Popen
ejecutar el código, recopilar la salida desde stdout
y stderr
hacia subprocess.PIPE
--- luego puedo imprimir (y guardar en un archivo de registro) la información de salida y verificar si hay errores. El problema es que no tengo idea de cómo avanza el código. Si lo ejecuto directamente desde la línea de comando, me da información sobre en qué iteración se encuentra, a qué hora, cuál es el siguiente paso de tiempo, etc.
¿Hay alguna manera de almacenar la salida (para registro y verificación de errores) y también producir una salida de transmisión en vivo?
La sección relevante de mi código:
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)
Originalmente estaba canalizando la run_command
conexión tee
para que una copia fuera directamente al archivo de registro y la transmisión aún saliera directamente al terminal, pero de esa manera no puedo almacenar ningún error (que yo sepa).
Mi solución temporal hasta ahora:
ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
log_file.flush()
luego, en otra terminal, ejecute tail -f log.txt
(st log_file = 'log.txt'
).
TLDR para Python 3:
import subprocess
import sys
with open("test.log", "wb") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for c in iter(lambda: process.stdout.read(1), b""):
sys.stdout.buffer.write(c)
f.buffer.write(c)
Tienes dos formas de hacer esto, ya sea creando un iterador a partir de las funciones read
o readline
y haciendo:
import subprocess
import sys
# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
# replace "" with b'' for Python 3
for c in iter(lambda: process.stdout.read(1), ""):
sys.stdout.write(c)
f.write(c)
o
import subprocess
import sys
# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
# replace "" with b"" for Python 3
for line in iter(process.stdout.readline, ""):
sys.stdout.write(line)
f.write(line)
O puede crear un archivo reader
y un writer
archivo. Pase el writer
al Popen
y lea delreader
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())
De esta manera tendrás los datos escritos test.log
tanto en la salida como en la salida estándar.
La única ventaja del enfoque de archivos es que su código no se bloquea. Así que puedes hacer lo que quieras mientras tanto y leer cuando quieras reader
sin bloqueos. Cuando use las funciones PIPE
, read
y readline
se bloquearán hasta que se escriba un carácter en la tubería o se escriba una línea en la tubería, respectivamente.
Resumen ejecutivo (o versión "tl;dr"): es fácil cuando hay como máximo uno subprocess.PIPE
, de lo contrario es difícil.
Quizás sea el momento de explicar un poco cómo subprocess.Popen
funciona esto.
(Advertencia: esto es para Python 2.x, aunque 3.x es similar; y estoy bastante confuso con la variante de Windows. Entiendo mucho mejor las cosas de POSIX).
La Popen
función necesita manejar de cero a tres flujos de E/S, de forma algo simultánea. Estos se denotan stdin
, stdout
y stderr
como de costumbre.
Puedes proporcionar:
None
, lo que indica que no deseas redirigir la transmisión. En su lugar, los heredará como de costumbre. Tenga en cuenta que en los sistemas POSIX, al menos, esto no significa que usará Python , solo la salida estándar realsys.stdout
de Python ; ver demostración al final.- Un
int
valor. Este es un descriptor de archivo "sin formato" (al menos en POSIX). (Nota al margen:PIPE
ySTDOUT
en realidad sonint
s internamente, pero son descriptores "imposibles", -1 y -2). - Una secuencia; en realidad, cualquier objeto con un
fileno
método.Popen
encontrará el descriptor de esa secuencia, utilizandostream.fileno()
y luego procederá como para unint
valor. subprocess.PIPE
, lo que indica que Python debería crear una tubería.subprocess.STDOUT
(stderr
solo para): dígale a Python que use el mismo descriptor que parastdout
. Esto solo tiene sentido si proporcionó unNone
valor (no) parastdout
, e incluso entonces, solo es necesario si establecestdout=subprocess.PIPE
. (De lo contrario, puede proporcionar el mismo argumento que proporcionóstdout
, por ejemplo,Popen(..., stdout=stream, stderr=stream)
).
Los casos más fáciles (sin tuberías)
Si no redirige nada (deje los tres como None
valor predeterminado o proporcione explícito None
), Pipe
lo tendrá bastante fácil. Sólo necesita separar el subproceso y dejarlo ejecutar. O, si redirige a una secuencia que no sea PIPE
una int
o una secuencia fileno()
, sigue siendo fácil, ya que el sistema operativo hace todo el trabajo. Python solo necesita separar el subproceso, conectando su entrada estándar, salida estándar y/o salida estándar a los descriptores de archivo proporcionados.
El caso todavía fácil: una tubería
Si redirige solo una transmisión, Pipe
las cosas aún serán bastante fáciles. Elijamos una transmisión a la vez y miremos.
Supongamos que desea proporcionar algunos archivos stdin
, pero dejarlos stdout
y stderr
no ser redirigidos, o ir a un descriptor de archivo. Como proceso principal, su programa Python simplemente necesita usarlo write()
para enviar datos por el canal. Puedes hacerlo tú mismo, por ejemplo:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc
o puede pasar los datos estándar a proc.communicate()
, que luego hace lo que se stdin.write
muestra arriba. No hay resultados que regresen, por lo que communicate()
solo tiene otro trabajo real: también cierra la tubería por usted. (Si no llama, proc.communicate()
debe hacerlo proc.stdin.close()
para cerrar la tubería, de modo que el subproceso sepa que no llegan más datos).
Supongamos que quieres capturar stdout
pero dejarlo stdin
solo stderr
. Nuevamente, es fácil: simplemente llame proc.stdout.read()
(o equivalente) hasta que no haya más resultados. Dado que proc.stdout()
es un flujo de E/S de Python normal, puedes usar todas las construcciones normales en él, como:
for line in proc.stdout:
o, nuevamente, puedes usar proc.communicate()
, que simplemente hace el trabajo read()
por ti.
Si desea capturar únicamente stderr
, funciona igual que con stdout
.
Hay un truco más antes de que las cosas se pongan difíciles. Supongamos que desea capturar stdout
y también capturar, stderr
pero en el mismo canal que la salida estándar:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
En este caso, subprocess
¡"trampas"! Bueno, tiene que hacer esto, por lo que en realidad no es trampa: inicia el subproceso con su stdout y su stderr dirigidos al (único) descriptor de tubería que retroalimenta a su proceso principal (Python). En el lado principal, nuevamente hay solo un descriptor de canalización para leer la salida. Toda la salida "stderr" aparece en proc.stdout
, y si llama proc.communicate()
, el resultado stderr (segundo valor en la tupla) será None
, no una cadena.
Los casos difíciles: dos o más tubos
Todos los problemas surgen cuando se desea utilizar al menos dos tuberías. De hecho, el subprocess
código en sí tiene este 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:
Pero, por desgracia, aquí hemos hecho al menos dos, y tal vez tres, tuberías diferentes, por lo que los count(None)
resultados son 1 o 0. Debemos hacer las cosas de la manera más difícil.
En Windows, esto se utiliza threading.Thread
para acumular resultados para self.stdout
y self.stderr
y hace que el subproceso principal entregue self.stdin
datos de entrada (y luego cierre la tubería).
En POSIX, esto se usa poll
si está disponible; de lo contrario select
, para acumular resultados y entregar entradas estándar. Todo esto se ejecuta en el proceso/hilo principal (único).
Aquí se necesitan hilos o encuestas/selección para evitar un punto muerto. Supongamos, por ejemplo, que hemos redirigido las tres secuencias a tres tuberías separadas. Supongamos además que hay un pequeño límite en la cantidad de datos que se pueden introducir en una tubería antes de que se suspenda el proceso de escritura, esperando que el proceso de lectura "limpie" la tubería desde el otro extremo. Establezcamos ese pequeño límite en un solo byte, solo a modo de ilustración. (De hecho, así es como funcionan las cosas, excepto que el límite es mucho mayor que un byte).
Si el proceso principal (Python) intenta escribir varios bytes, digamos, 'go\n'
en proc.stdin
, el primer byte entra y luego el segundo hace que el proceso de Python se suspenda, esperando que el subproceso lea el primer byte, vaciando la tubería.
Mientras tanto, supongamos que el subproceso decide imprimir un mensaje amistoso "¡Hola! ¡Que no cunda el pánico!". saludo. Entra H
en su tubería de salida estándar, pero e
hace que se suspenda, esperando a que su padre lea eso H
, vaciando la tubería de salida estándar.
Ahora estamos estancados: el proceso de Python está dormido, esperando terminar de decir "ir", y el subproceso también está dormido, esperando terminar de decir "¡Hola! ¡Que no cunda el pánico!".
El subprocess.Popen
código evita este problema con threading-or-select/poll. Cuando los bytes pueden pasar por las tuberías, se van. Cuando no pueden, solo un subproceso (no todo el proceso) tiene que dormir o, en el caso de seleccionar/sondear, el proceso de Python espera simultáneamente "puede escribir" o "datos disponibles", escribe en la entrada estándar del proceso. solo cuando hay espacio, y lee su salida estándar y/o stderr solo cuando los datos están listos. El proc.communicate()
código (en realidad, _communicate
donde se manejan los casos complicados) regresa una vez que se han enviado todos los datos estándar (si los hay) y se han acumulado todos los datos estándar y/o stderr.
Si desea leer ambos stdout
y stderr
en dos canales diferentes (independientemente de cualquier stdin
redirección), también deberá evitar el punto muerto. El escenario de punto muerto aquí es diferente: ocurre cuando el subproceso escribe algo largo stderr
mientras usted extrae datos de stdout
, o viceversa, pero aún está ahí.
La demostración
Prometí demostrar que, sin redirigir, Python subprocess
escribe en la salida estándar subyacente, no en sys.stdout
. Entonces, aquí hay un código:
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()
Cuando se ejecuta:
$ python out.py
start show1
hello
in buffer: sys.stdout being buffered
start show2
hello
Tenga en cuenta que la primera rutina fallará si agrega stdout=sys.stdout
, ya que un StringIO
objeto no tiene fileno
. El segundo omitirá el archivo hello
si agrega stdout=sys.stdout
que ya sys.stdout
ha sido redirigido a os.devnull
.
(Si redirige el descriptor de archivo-1 de Python, el subproceso seguirá esa redirección. La open(os.devnull, 'w')
llamada produce una secuencia fileno()
mayor que 2).
También podemos usar el iterador de archivos predeterminado para leer stdout en lugar de usar la construcción iter con readline()
.
import subprocess
import sys
process = subprocess.Popen(
your_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
for line in process.stdout:
sys.stdout.write(line)