salida en vivo del comando de subproceso

186

Estoy usando un script python como controlador para un código hidrodinámico. Cuando llega el momento de ejecutar la simulación, utilizo subprocess.Popenpara ejecutar el código, recopilar la salida de stdout y stderr en un 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 está progresando el código. Si lo ejecuto directamente desde la línea de comando, me da información sobre en qué iteración está, a qué hora, cuál es el siguiente paso de tiempo, etc.

¿Hay alguna manera de almacenar la salida (para el registro y la comprobació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)

Al principio yo estaba canalizando el run_commandmedio teepara que una copia fue directamente al archivo de registro, y la corriente de salida sigue directamente al terminal -, pero de esa manera no puedo almacenar cualquier error (a mi un profundo conocimiento).


Editar:

Solución temporal:

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
fuente
1
Tal vez pueda usarlo Popen.pollcomo en una pregunta anterior de desbordamiento de pila .
Paulo Almeida
Algunos comandos que muestran indicación de progreso (p. Ej. git) Lo hacen solo si su salida es un "dispositivo tty" (probado a través de libc isatty()). En ese caso, es posible que deba abrir un pseudo-tty.
torek
@torek ¿qué es un (pseudo) tty?
DilithiumMatrix
2
Dispositivos en sistemas similares a Unix que permiten que un proceso finja ser un usuario en un puerto serie. Así es como funciona ssh (lado del servidor), por ejemplo. Ver python pty library , y también pexpect .
torek
Re solución temporal: no hay necesidad de llamar flush, y no es necesario leer desde la tubería stderr si el subproceso produce gran parte de salida stderr. No hay suficiente espacio en un campo de comentarios para explicar esto ...
torek

Respuestas:

169

Tiene dos formas de hacerlo, ya sea creando un iterador a partir de las funciones reado readliney do:

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for c in iter(lambda: process.stdout.read(1), ''):  # replace '' with b'' for Python 3
        sys.stdout.write(c)
        f.write(c)

o

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for line in iter(process.stdout.readline, ''):  # replace '' with b'' for Python 3
        sys.stdout.write(line)
        f.write(line)

O puede crear ay readerun writerarchivo. Pase el writeral Popeny lea desde elreader

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á los datos escritos test.logtanto en la salida estándar como en la salida estándar.

La única ventaja del enfoque de archivo es que su código no se bloquea. Por lo tanto, puede hacer lo que quiera mientras tanto y leer siempre que lo desee de readeruna manera sin bloqueos. Cuando se utiliza PIPE, ready readlinelas funciones bloquearán hasta que el carácter de uno se escribe en la tubería o una línea se escribe en el tubo, respectivamente.

Viktor Kerkez
fuente
1
Ugh :-) escribir en un archivo, leerlo y dormir en el bucle? También existe la posibilidad de que el proceso termine antes de que haya terminado de leer el archivo.
Guy Sirton el
13
Con Python 3, usted necesita iter(process.stdout.readline, b'')(es decir, el centinela pasado a iter debe ser una cadena binaria, ya que b'' != ''.
John Mellor
3
Para secuencias binarias, haga esto:for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane
66
Agregando a la respuesta de @JohnMellor, en Python 3 se necesitaban las siguientes modificaciones: process = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) for line in iter(process.stdout.readline, b'') sys.stdout.write(line.decode(sys.stdout.encoding))
bergercookie
44
pero la salida no es en vivo, ¿verdad? en mi experiencia, solo espera hasta que el proceso termine de ejecutarse y solo luego se imprime en la consola. Enlace -> stackoverflow.com/questions/30026045/…
denis631
91

Resumen ejecutivo (o versión "tl; dr"): es fácil cuando hay a lo sumo subprocess.PIPE, de lo contrario es difícil.

Puede que sea hora de explicar un poco sobre cómo subprocess.Popenfunciona.

(Advertencia: esto es para Python 2.x, aunque 3.x es similar; y estoy bastante confuso con la variante de Windows. Entiendo las cosas de POSIX mucho mejor).

La Popenfunción necesita lidiar con flujos de E / S de cero a tres, algo simultáneamente. Estos se indican stdin, stdouty stderrcomo de costumbre.

Puedes proporcionar:

  • None, lo que indica que no desea redirigir la transmisión. Los heredará como de costumbre. Tenga en cuenta que en los sistemas POSIX, al menos, esto no significa que utilizará Python sys.stdout, solo el stdout real de Python ; ver demo al final.
  • Un intvalor Este es un descriptor de archivo "en bruto" (al menos en POSIX). (Nota al margen: PIPEy en STDOUTrealidad son ints internamente, pero son descriptores "imposibles", -1 y -2.)
  • Una secuencia, realmente, cualquier objeto con un filenométodo. Popenencontrará el descriptor para esa secuencia, usando 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): 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 lo 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 el Nonevalor predeterminado o el suministro explícito None), Pipees bastante fácil. Solo necesita escindir el subproceso y dejarlo correr. O bien, si redirige a un no PIPE-an into un fileno()flujo- todavía es fácil, ya que el sistema operativo hace todo el trabajo. Python solo necesita escindir el subproceso, conectando su stdin, stdout y / o stderr a los descriptores de archivo proporcionados.

El caso aún fácil: una tubería

Si redirige solo una transmisión, Pipeaún tiene las cosas bastante fáciles. Vamos a elegir una transmisión a la vez y mirar.

Supongamos que quieres suministrar algo stdin , pero deje ir stdouty stderrvaya sin redireccionar, o vaya a un descriptor de archivo. Como proceso principal, su programa Python simplemente necesita usar write()para enviar datos por la tubería. Puede hacerlo usted mismo, por ejemplo:

proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc

o puede pasar los datos stdin a proc.communicate() , que luego hace lo que se stdin.writemuestra arriba. No hay salida de retorno, por lo que communicate()solo tiene otro trabajo real: también cierra la tubería por usted. (Si no llama proc.communicate(), debe llamar proc.stdin.close()para cerrar la tubería, de modo que el subproceso sepa que no hay más datos entrando).

Supongamos que desea capturar stdoutpero salir stdinystderr solo. Nuevamente, es fácil: solo llame proc.stdout.read()(o equivalente) hasta que no haya más resultados. Como proc.stdout()es una secuencia de E / S Python normal, puede usar todas las construcciones normales en ella, como:

for line in proc.stdout:

o, de nuevo, puedes usar proc.communicate(), lo que simplemente hace read()por ti.

Si solo quieres capturar stderr , funciona igual que con stdout.

Hay un truco más antes de que las cosas se pongan difíciles. Suponga que desea capturar stdout, y también capturar stderrpero en la misma tubería que stdout:

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

En este caso, subprocess"trucos"! Bueno, tiene que hacer esto, por lo que no es realmente una trampa: inicia el subproceso con su stdout y su stderr directamente en el descriptor de tubería (único) que retroalimenta a su proceso padre (Python). En el lado primario, nuevamente hay un solo descriptor de tubería para leer la salida. Toda la salida "stderr" aparece enproc.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 tuberías

Todos los problemas surgen cuando 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 el count(None) retornos son 1 o 0. Debemos hacer las cosas de la manera difícil.

En Windows, esto se utiliza threading.Threadpara acumular resultados para self.stdoutyself.stderr , y hace que el subproceso principal entregue self.stdindatos de entrada (y luego cierre la tubería).

En POSIX, esto usa pollsi está disponible, de lo contrarioselect , para acumular resultados y entregar entradas stdin. Todo esto se ejecuta en el proceso / hilo principal (único).

Se necesitan hilos o sondeo / selección aquí para evitar un punto muerto. Supongamos, por ejemplo, que hemos redirigido las tres transmisiones a tres tuberías separadas. Supongamos además que hay un pequeño límite en la cantidad de datos que se pueden insertar en una tubería antes de que se suspenda el proceso de escritura, esperando que el proceso de lectura "limpie" la tubería del otro extremo. Establezcamos ese pequeño límite en un solo byte, solo como ilustración. (De hecho, así es como funcionan las cosas, excepto que el límite es mucho mayor que un byte).

Si el proceso padre (Python) intenta escribir varios bytes, por ejemplo, 'go\n'paraproc.stdin , entra el primer byte 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, suponga que el subproceso decide imprimir un amistoso "¡Hola! ¡No se asuste!" saludo. El Hentra en su tubería estándar, pero ehace que se suspenda, esperando que su padre lea esoH , vaciando la tubería estándar.

Ahora estamos atascados: el proceso de Python está dormido, esperando terminar de decir "ir", y el subproceso también está dormido, esperando terminar de decir "¡Hola! ¡No se asuste!".

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 hilo (no todo el proceso) tiene que dormir, o, en el caso de select / poll, el proceso de Python espera simultáneamente "puede escribir" o "datos disponibles", escribe en el stdin del proceso solo cuando hay espacio, y lee su stdout y / o stderr solo cuando los datos están listos. El proc.communicate()código (en realidad_communicate donde se manejan los casos peludos) regresa una vez que se han enviado todos los datos stdin (si los hay) y se han acumulado todos los datos stdout 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 extrae datos stdout, o viceversa, pero sigue ahí.


La demo

Prometí demostrar que, sin redireccionar, Python subprocesses escribir en el stdout subyacente, no 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 hellosi agrega stdout=sys.stdoutya que sys.stdoutse ha redirigido a os.devnull.

(Si redirige archivo descriptor-1 de Python, el subproceso va a seguir ese cambio de dirección. La open(os.devnull, 'w')llamada produce una corriente cuya fileno()es mayor que 2.)

torek
fuente
Hmm Su demostración parece mostrar lo contrario del reclamo al final. Estás redirigiendo la salida estándar de Python en el búfer, pero la salida estándar del subproceso sigue yendo a la consola. ¿Cómo es eso útil? ¿Me estoy perdiendo de algo?
Guy Sirton el
@GuySirton: la demostración muestra que la salida estándar del subproceso (cuando no se indica explícitamente sys.stdout) va a la salida estándar de Python , no a la salida estándar del programa de Python ( sys.). Lo que admito es una ... extraña distinción. ¿Hay una mejor manera de expresar esto?
torek
es bueno saberlo, pero realmente queremos capturar la salida del subproceso aquí, por lo que cambiar sys.stdout es genial, pero no nos ayuda, creo. Una buena observación de que comunicarse debe estar usando algo como select (), sondeo o hilos.
Guy Sirton el
He agregado una implementación con select ()
sivann
20

También podemos usar el iterador de archivo predeterminado para leer stdout en lugar de usar iter construct con readline ().

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)
Jughead
fuente
¡La respuesta más elegante aquí!
Nir
9
Esta solución no se muestra en tiempo real. Espera hasta que finalice el proceso y muestra todos los resultados a la vez. En la solución de Viktor Kerkez, si "your_command" se muestra progresivamente, la salida sigue progresivamente, siempre que "your_command" elimine stdout de vez en cuando (debido a la tubería).
Eric H.
1
@Nir porque no es en vivo.
melMass el
Esta solución itera en el descriptor predeterminado, por lo que solo se actualizará cuando una línea se actualice en la salida. Para una actualización basada en caracteres, debe iterar en el método read () como se muestra en la solución de Viktor. Pero eso fue una exageración para mi caso de uso.
Jughead
11

Si puede usar bibliotecas de terceros, es posible que pueda usar algo como sarge(divulgación: soy su responsable). Esta biblioteca permite el acceso sin bloqueo a las secuencias de salida de los subprocesos: se superpone en capas sobre el subprocessmódulo.

Vinay Sajip
fuente
Buen trabajo en sarge, por cierto. De hecho, eso resuelve el requisito del OP, pero podría ser un poco pesado para ese caso de uso.
profundización
Si está sugiriendo una herramienta, al menos muestre un ejemplo de uso para este caso exacto.
Serhiy
4

Solución 1: Log stdoutY stderrsimultáneamente en tiempo real

Una solución simple que registra tanto stdout como stderr simultáneamente, línea por línea en tiempo real en un archivo de registro.

import subprocess as sp
from concurrent.futures import ThreadPoolExecutor


def log_popen_pipe(p, stdfile):

    with open("mylog.txt", "w") as f:

        while p.poll() is None:
            f.write(stdfile.readline())
            f.flush()

        # Write the rest from the buffer
        f.write(stdfile.read())


with sp.Popen(["ls"], stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    with ThreadPoolExecutor(2) as pool:
        r1 = pool.submit(log_popen_pipe, p, p.stdout)
        r2 = pool.submit(log_popen_pipe, p, p.stderr)
        r1.result()
        r2.result()

Solución 2: una función read_popen_pipes()que le permite iterar sobre ambas tuberías (stdout / stderr), simultáneamente en tiempo real

import subprocess as sp
from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

# The function in use:

with sp.Popen(my_cmd, stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):
        print(out_line, end='')
        print(err_line, end='')

    return p.poll()
Rotareti
fuente
3

Una solución buena pero "de peso pesado" es usar Twisted - vea la parte inferior.

Si estás dispuesto a vivir solo con stdout, algo así debería funcionar:

import subprocess
import sys
popenobj = subprocess.Popen(["ls", "-Rl"], stdout=subprocess.PIPE)
while not popenobj.poll():
   stdoutdata = popenobj.stdout.readline()
   if stdoutdata:
      sys.stdout.write(stdoutdata)
   else:
      break
print "Return code", popenobj.returncode

(Si usa read () intenta leer todo el "archivo" que no es útil, lo que realmente podríamos usar aquí es algo que lea todos los datos que están en este momento)

También se podría tratar de abordar esto con hilos, por ejemplo:

import subprocess
import sys
import threading

popenobj = subprocess.Popen("ls", stdout=subprocess.PIPE, shell=True)

def stdoutprocess(o):
   while True:
      stdoutdata = o.stdout.readline()
      if stdoutdata:
         sys.stdout.write(stdoutdata)
      else:
         break

t = threading.Thread(target=stdoutprocess, args=(popenobj,))
t.start()
popenobj.wait()
t.join()
print "Return code", popenobj.returncode

Ahora también podríamos agregar stderr al tener dos hilos.

Sin embargo, tenga en cuenta que los documentos de subproceso desalientan el uso de estos archivos directamente y recomiendan su uso communicate()(principalmente preocupado por los puntos muertos que creo que no es un problema anterior) y las soluciones son un poco complicadas, por lo que realmente parece que el módulo de subproceso no está a la altura el trabajo (ver también: http://www.python.org/dev/peps/pep-3145/ ) y tenemos que mirar algo más.

Una solución más complicada es usar Twisted como se muestra aquí: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

La forma de hacer esto con Twisted es crear su proceso usando reactor.spawnprocess()y proporcionando un proceso ProcessProtocolque luego procese la salida de forma asincrónica. El código de Python de muestra Twisted está aquí: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py

Guy Sirton
fuente
¡Gracias! Acabo de intentar algo como esto (basado en el comentario de @PauloAlmeida, pero mi llamado al subproceso. Popen está bloqueando, es decir, solo llega al bucle while una vez que regresa ...
DilithiumMatrix
1
Eso no es lo que está pasando. Entra en el ciclo while de inmediato y luego bloquea la read()llamada hasta que el subproceso salga y el proceso principal reciba EOFen la tubería.
Alp
@Alp interesante! así es.
DilithiumMatrix
Sí, fui demasiado rápido para publicar esto. En realidad, no funciona correctamente y no se puede solucionar fácilmente. De vuelta a la mesa de dibujo.
Guy Sirton el
1
@zhermes: Entonces, el problema con read () es que intentará leer toda la salida hasta EOF, lo que no es útil. readline () ayuda y puede ser todo lo que necesita (las líneas realmente largas también pueden ser un problema). También debe tener cuidado con el almacenamiento en búfer en el proceso que está iniciando ...
Guy Sirton
3

Además de todas estas respuestas, un enfoque simple también podría ser el siguiente:

process = subprocess.Popen(your_command, stdout=subprocess.PIPE)

while process.stdout.readable():
    line = process.stdout.readline()

    if not line:
        break

    print(line.strip())

Recorra el flujo legible siempre que sea legible y si obtiene un resultado vacío, deténgalo.

La clave aquí es que readline()devuelve una línea (con \nal final) siempre que haya una salida y vacía si realmente está al final.

Espero que esto ayude a alguien.

kabirbaidhya
fuente
3

Basado en todo lo anterior, sugiero una versión ligeramente modificada (python3):

  • mientras que la línea de llamada de bucle readline (La solución iter sugerida parecía bloquear para siempre para mí - Python 3, Windows 7)
  • estructurado para que no sea necesario duplicar el manejo de los datos leídos después de que la encuesta no devuelvaNone
  • stderr entubado en stdout para que se lean ambas salidas de salida
  • Código agregado para obtener el valor de salida de cmd.

Código:

import subprocess
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT, universal_newlines=True)
while True:
    rd = proc.stdout.readline()
    print(rd, end='')  # and whatever you want to do...
    if not rd:  # EOF
        returncode = proc.poll()
        if returncode is not None:
            break
        time.sleep(0.1)  # cmd closed stdout, but not exited yet

# You may want to check on ReturnCode here
mrtnlrsn
fuente
La returncodeparte fue crucial en mi caso.
polvo de estrellas
2

Parece que la salida con buffer de línea funcionará para usted, en cuyo caso algo como lo siguiente podría ser adecuado. (Advertencia: no se ha probado). Esto solo proporcionará el stdout del subproceso en tiempo real. Si desea tener stderr y stdout en tiempo real, tendrá que hacer algo más complejo select.

proc = subprocess.Popen(run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
while proc.poll() is None:
    line = proc.stdout.readline()
    print line
    log_file.write(line + '\n')
# Might still be data on stdout at this point.  Grab any
# remainder.
for line in proc.stdout.read().split('\n'):
    print line
    log_file.write(line + '\n')
# Do whatever you want with proc.stderr here...
Montaña
fuente
2

¿Por qué no establecer stdoutdirectamente en sys.stdout? Y si necesita generar también en un registro, simplemente puede anular el método de escritura de f.

import sys
import subprocess

class SuperFile(open.__class__):

    def write(self, data):
        sys.stdout.write(data)
        super(SuperFile, self).write(data)

f = SuperFile("log.txt","w+")       
process = subprocess.Popen(command, stdout=f, stderr=f)
xaav
fuente
Eso no funcionaría: el módulo de subproceso se bifurca y establece el stdoutdescriptor de archivo en el descriptor de archivo del objeto de archivo pasado. El método de escritura nunca se llamaría (al menos eso es lo que hace el subproceso para stderr, supongo que es lo mismo para stdout).
t.animal
2

Todas las soluciones anteriores que probé no lograron separar la salida stderr y stdout (varias tuberías) o bloquearon para siempre cuando el búfer de la tubería del sistema operativo estaba lleno, lo que sucede cuando el comando que está ejecutando sale demasiado rápido (hay una advertencia para esto en python encuesta () manual de subproceso). La única forma confiable que encontré fue a través de select, pero esta es una solución solo posix:

import subprocess
import sys
import os
import select
# returns command exit status, stdout text, stderr text
# rtoutput: show realtime output while running
def run_script(cmd,rtoutput=0):
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    poller = select.poll()
    poller.register(p.stdout, select.POLLIN)
    poller.register(p.stderr, select.POLLIN)

    coutput=''
    cerror=''
    fdhup={}
    fdhup[p.stdout.fileno()]=0
    fdhup[p.stderr.fileno()]=0
    while sum(fdhup.values()) < len(fdhup):
        try:
            r = poller.poll(1)
        except select.error, err:
            if err.args[0] != EINTR:
                raise
            r=[]
        for fd, flags in r:
            if flags & (select.POLLIN | select.POLLPRI):
                c = os.read(fd, 1024)
                if rtoutput:
                    sys.stdout.write(c)
                    sys.stdout.flush()
                if fd == p.stderr.fileno():
                    cerror+=c
                else:
                    coutput+=c
            else:
                fdhup[fd]=1
    return p.poll(), coutput.strip(), cerror.strip()
sivann
fuente
Otra alternativa es hilar un hilo por tubería. Cada hilo puede bloquear el bloqueo de E / S en la tubería, sin bloquear los otros hilos. Pero esto introduce su propio conjunto de problemas. Todos los métodos tienen molestias, simplemente eliges cuáles te resultan menos molestos. :-)
torek
2

Similar a las respuestas anteriores, pero la siguiente solución funcionó para mí en Windows usando Python3 para proporcionar un método común para imprimir e iniciar sesión en tiempo real ( getting-realtime-output-using-python ):

def print_and_log(command, logFile):
    with open(logFile, 'wb') as f:
        command = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)

        while True:
            output = command.stdout.readline()
            if not output and command.poll() is not None:
                f.close()
                break
            if output:
                f.write(output)
                print(str(output.strip(), 'utf-8'), flush=True)
        return command.poll()
scottysbasement
fuente
2

Creo que el subprocess.communicatemétodo es un poco engañoso: en realidad llena el stdout y el stderr que especifique en el subprocess.Popen.

Sin embargo, la lectura de lo subprocess.PIPEque puede proporcionar a los parámetros stdout y stderrsubprocess.Popen 's eventualmente llenará los búferes de tubería del sistema operativo y bloqueará su aplicación (especialmente si tiene múltiples procesos / hilos que deben usar ).subprocess

Mi solución propuesta es proporcionar archivos stdout y stderr , y leer el contenido de los archivos en lugar de leerlos desde el punto muerto PIPE. Estos archivos pueden ser tempfile.NamedTemporaryFile(), a los que también se puede acceder para leer mientras están siendo escritos por ellos subprocess.communicate.

A continuación se muestra un uso de muestra:

        try:
            with ProcessRunner(('python', 'task.py'), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

Y este es el código fuente que está listo para ser utilizado con tantos comentarios como pueda proporcionar para explicar lo que hace:

Si está utilizando python 2, asegúrese de instalar primero la última versión del paquete subprocess32 de pypi.


import os
import sys
import threading
import time
import tempfile
import logging

if os.name == 'posix' and sys.version_info[0] < 3:
    # Support python 2
    import subprocess32 as subprocess
else:
    # Get latest and greatest from python 3
    import subprocess

logger = logging.getLogger(__name__)


class ProcessError(Exception):
    """Base exception for errors related to running the process"""


class ProcessTimeout(ProcessError):
    """Error that will be raised when the process execution will exceed a timeout"""


class ProcessRunner(object):
    def __init__(self, args, env=None, timeout=None, bufsize=-1, seconds_to_wait=0.25, **kwargs):
        """
        Constructor facade to subprocess.Popen that receives parameters which are more specifically required for the
        Process Runner. This is a class that should be used as a context manager - and that provides an iterator
        for reading captured output from subprocess.communicate in near realtime.

        Example usage:


        try:
            with ProcessRunner(('python', task_file_path), env=os.environ.copy(), seconds_to_wait=0.01) as process_runner:
                for out in process_runner:
                    print(out)
        catch ProcessError as e:
            print(e.error_message)
            raise

        :param args: same as subprocess.Popen
        :param env: same as subprocess.Popen
        :param timeout: same as subprocess.communicate
        :param bufsize: same as subprocess.Popen
        :param seconds_to_wait: time to wait between each readline from the temporary file
        :param kwargs: same as subprocess.Popen
        """
        self._seconds_to_wait = seconds_to_wait
        self._process_has_timed_out = False
        self._timeout = timeout
        self._process_done = False
        self._std_file_handle = tempfile.NamedTemporaryFile()
        self._process = subprocess.Popen(args, env=env, bufsize=bufsize,
                                         stdout=self._std_file_handle, stderr=self._std_file_handle, **kwargs)
        self._thread = threading.Thread(target=self._run_process)
        self._thread.daemon = True

    def __enter__(self):
        self._thread.start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._thread.join()
        self._std_file_handle.close()

    def __iter__(self):
        # read all output from stdout file that subprocess.communicate fills
        with open(self._std_file_handle.name, 'r') as stdout:
            # while process is alive, keep reading data
            while not self._process_done:
                out = stdout.readline()
                out_without_trailing_whitespaces = out.rstrip()
                if out_without_trailing_whitespaces:
                    # yield stdout data without trailing \n
                    yield out_without_trailing_whitespaces
                else:
                    # if there is nothing to read, then please wait a tiny little bit
                    time.sleep(self._seconds_to_wait)

            # this is a hack: terraform seems to write to buffer after process has finished
            out = stdout.read()
            if out:
                yield out

        if self._process_has_timed_out:
            raise ProcessTimeout('Process has timed out')

        if self._process.returncode != 0:
            raise ProcessError('Process has failed')

    def _run_process(self):
        try:
            # Start gathering information (stdout and stderr) from the opened process
            self._process.communicate(timeout=self._timeout)
            # Graceful termination of the opened process
            self._process.terminate()
        except subprocess.TimeoutExpired:
            self._process_has_timed_out = True
            # Force termination of the opened process
            self._process.kill()

        self._process_done = True

    @property
    def return_code(self):
        return self._process.returncode


Victor Klapholz
fuente
1

Aquí hay una clase que estoy usando en uno de mis proyectos. Redirige la salida de un subproceso al registro. Al principio intenté simplemente sobrescribir el método de escritura, pero eso no funciona, ya que el subproceso nunca lo llamará (la redirección ocurre en el nivel del descriptor de archivo). Así que estoy usando mi propia tubería, similar a cómo se hace en el módulo de subproceso. Esto tiene la ventaja de encapsular toda la lógica de registro / impresión en el adaptador y simplemente puede pasar instancias del registrador a Popen:subprocess.Popen("/path/to/binary", stderr = LogAdapter("foo"))

class LogAdapter(threading.Thread):

    def __init__(self, logname, level = logging.INFO):
        super().__init__()
        self.log = logging.getLogger(logname)
        self.readpipe, self.writepipe = os.pipe()

        logFunctions = {
            logging.DEBUG: self.log.debug,
            logging.INFO: self.log.info,
            logging.WARN: self.log.warn,
            logging.ERROR: self.log.warn,
        }

        try:
            self.logFunction = logFunctions[level]
        except KeyError:
            self.logFunction = self.log.info

    def fileno(self):
        #when fileno is called this indicates the subprocess is about to fork => start thread
        self.start()
        return self.writepipe

    def finished(self):
       """If the write-filedescriptor is not closed this thread will
       prevent the whole program from exiting. You can use this method
       to clean up after the subprocess has terminated."""
       os.close(self.writepipe)

    def run(self):
        inputFile = os.fdopen(self.readpipe)

        while True:
            line = inputFile.readline()

            if len(line) == 0:
                #no new data was added
                break

            self.logFunction(line.strip())

Si no necesita iniciar sesión pero simplemente quiere usarlo print(), obviamente puede eliminar grandes porciones del código y acortar la clase. También podría ampliarlo por una __enter__y __exit__método y llamar finishedde __exit__manera que fácilmente se podría utilizar como contexto.

t.animal
fuente
1

Ninguna de las soluciones Pythonic funcionó para mí. Resultó que proc.stdout.read()o similar puede bloquear para siempre.

Por lo tanto, uso teeasí:

subprocess.run('./my_long_running_binary 2>&1 | tee -a my_log_file.txt && exit ${PIPESTATUS}', shell=True, check=True, executable='/bin/bash')

Esta solución es conveniente si ya la está utilizando shell=True.

${PIPESTATUS}captura el estado de éxito de toda la cadena de comandos (solo disponible en Bash). Si omitiera el && exit ${PIPESTATUS}, esto siempre devolvería cero ya que teenunca falla.

unbufferpuede ser necesario para imprimir cada línea inmediatamente en el terminal, en lugar de esperar demasiado tiempo hasta que se llene el "buffer de tubería". Sin embargo, unbuffer se traga el estado de salida de afirmar (SIG Abortar) ...

2>&1 también registra stderror en el archivo.

Mike76
fuente