salida en vivo del comando de subproceso

Resuelto DilithiumMatrix asked hace 11 años • 25 respuestas

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.Popenejecutar el código, recopilar la salida desde stdouty stderrhacia 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_commandconexión teepara 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').

DilithiumMatrix avatar Aug 25 '13 01:08 DilithiumMatrix
Aceptado

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 reado readliney 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 readery un writerarchivo. Pase el writeral Popeny 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.logtanto 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 readersin bloqueos. Cuando use las funciones PIPE, ready readlinese bloquearán hasta que se escriba un carácter en la tubería o se escriba una línea en la tubería, respectivamente.

Viktor Kerkez avatar Aug 24 '2013 19:08 Viktor Kerkez

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.Popenfunciona 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 Popenfunción necesita manejar de cero a tres flujos de E/S, de forma algo simultánea. Estos se denotan stdin, stdouty stderrcomo 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 intvalor. Este es un descriptor de archivo "sin formato" (al menos en POSIX). (Nota al margen: PIPEy STDOUTen realidad son ints internamente, pero son descriptores "imposibles", -1 y -2).
  • Una secuencia; en realidad, cualquier objeto con un filenométodo. Popenencontrará el descriptor de esa secuencia, utilizando stream.fileno()y luego procederá como para un intvalor.
  • subprocess.PIPE, lo que indica que Python debería crear una tubería.
  • subprocess.STDOUT( stderrsolo para): dígale a Python que use el mismo descriptor que para stdout. Esto solo tiene sentido si proporcionó un Nonevalor (no) para stdout, e incluso entonces, solo es necesario si establece stdout=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 Nonevalor predeterminado o proporcione explícito None), Pipelo tendrá bastante fácil. Sólo necesita separar el subproceso y dejarlo ejecutar. O, si redirige a una secuencia que no sea PIPEuna into 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, Pipelas 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 stdouty stderrno 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.writemuestra 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 stdoutpero dejarlo stdinsolo 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 stdouty también capturar, stderrpero 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 subprocesscó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.Threadpara acumular resultados para self.stdouty self.stderry hace que el subproceso principal entregue self.stdindatos de entrada (y luego cierre la tubería).

En POSIX, esto se usa pollsi 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 Hen su tubería de salida estándar, pero ehace 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.Popencó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, _communicatedonde 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 stdouty stderren dos canales diferentes (independientemente de cualquier stdinredirección), también deberá evitar el punto muerto. El escenario de punto muerto aquí es diferente: ocurre cuando el subproceso escribe algo largo stderrmientras usted extrae datos de stdout, o viceversa, pero aún está ahí.


La demostración

Prometí demostrar que, sin redirigir, Python subprocessescribe 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 StringIOobjeto no tiene fileno. El segundo omitirá el archivo hellosi agrega stdout=sys.stdoutque ya sys.stdoutha 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).

torek avatar Aug 24 '2013 20:08 torek

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)
Jughead avatar Jul 13 '2016 01:07 Jughead