saída ao vivo do comando subprocesso

186

Estou usando um script python como um driver para um código hidrodinâmico. Quando chega a hora de executar a simulação, eu uso subprocess.Popenpara executar o código, coletar a saída de stdout e stderr em um subprocess.PIPE--- então posso imprimir (e salvar em um arquivo de log) as informações de saída e verificar se há erros . O problema é que não tenho ideia de como o código está progredindo. Se eu executá-lo diretamente a partir da linha de comando, ele fornecerá informações sobre em que iteração está, a que horas, qual será o próximo passo etc.

Existe uma maneira de armazenar a saída (para registro e verificação de erros) e também produzir uma saída de transmissão ao vivo?

A seção relevante do meu 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, eu estava canalizando a run_commandpassagem teepara que uma cópia fosse diretamente para o arquivo de log e o fluxo ainda saísse diretamente para o terminal - mas, dessa forma, não posso armazenar nenhum erro (para o meu conhecimento).


Editar:

Solução temporária:

ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
    log_file.flush()

depois, em outro terminal, execute tail -f log.txt(st log_file = 'log.txt').

DilithiumMatrix
fonte
1
Talvez você possa usar Popen.pollcomo em uma pergunta anterior do Stack Overflow .
Paulo Almeida
Alguns comandos que mostram indicação de progresso (por exemplo, git) fazem isso apenas se sua saída for um "dispositivo tty" (testado via libc isatty()). Nesse caso, talvez você precise abrir um pseudo-tty.
torek
@torek o que é um (pseudo-) tty?
precisa saber é o seguinte
2
Dispositivos em sistemas do tipo Unix que permitem que um processo finja ser um usuário em uma porta serial. É assim que o ssh (lado do servidor) funciona, por exemplo. Veja a biblioteca python pty e também o pexpect .
torek
Re solução temporária: não há necessidade de chamar flush, e não é necessário ler a partir do tubo stderr se o subprocesso produz muito saída stderr. Não há espaço suficiente em um campo de comentário para explicar isso ...
Torek

Respostas:

169

Você tem duas maneiras de fazer isso, criando um iterador a partir das funções readou readlinee faça:

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)

ou

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)

Ou você pode criar um readere um writerarquivo. Passe o writerpara Popene leia a partir doreader

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())

Dessa forma, você terá os dados gravados na test.logsaída padrão e na saída.

A única vantagem da abordagem de arquivo é que seu código não é bloqueado. Enquanto isso, você pode fazer o que quiser e ler sempre que quiser, de readeruma maneira sem bloqueio. Quando você usa PIPE, as funções reade readlineserão bloqueadas até que um caractere seja gravado no canal ou uma linha seja gravada no canal respectivamente.

Viktor Kerkez
fonte
1
Ugh :-) escreva em um arquivo, leia-o e durma no loop? Há também uma chance de o processo terminar antes que você termine de ler o arquivo.
precisa saber é o seguinte
13
Com Python 3, você precisa iter(process.stdout.readline, b'')(ou seja, a sentinela passado para iter precisa ser uma string binária, uma vez b'' != ''.
John Mellor
3
Para fluxos binários, faça o seguinte:for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane 5/17/17
6
Adicionando à resposta de @JohnMellor, no Python 3 foram necessárias as seguintes modificações: 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
4
mas a saída não é ao vivo, é? na minha experiência, ele apenas espera até que o processo termine a execução e só depois imprime no console. Link -> stackoverflow.com/questions/30026045/…
denis631
91

Resumo executivo (ou versão "tl; dr"): é fácil quando há no máximo um subprocess.PIPE, caso contrário, é difícil.

Talvez seja hora de explicar um pouco sobre como subprocess.Popenfunciona.

(Advertência: isso é para Python 2.x, embora 3.x seja semelhante; e eu sou bastante confuso com a variante do Windows. Entendo muito melhor o material POSIX.)

A Popenfunção precisa lidar com zero a três fluxos de E / S, um pouco simultaneamente. Estes são indicados stdine stdout, stderrcomo de costume.

Você pode fornecer:

  • None, indicando que você não deseja redirecionar o fluxo. Ele os herdará como de costume. Observe que, nos sistemas POSIX, pelo menos, isso não significa que ele usará o Python sys.stdout, apenas o stdout real do Python ; veja a demonstração no final.
  • Um intvalor Este é um descritor de arquivo "bruto" (pelo menos no POSIX). (Nota lateral: PIPEe STDOUTna verdade são ints internamente, mas são descritores "impossíveis", -1 e -2.)
  • Um fluxo - realmente, qualquer objeto com um filenométodo. Popenencontrará o descritor para esse fluxo, usando stream.fileno()e, em seguida, prossiga como para um intvalor.
  • subprocess.PIPE, indicando que o Python deve criar um canal.
  • subprocess.STDOUT( stderrapenas): diga ao Python para usar o mesmo descritor que para stdout. Isso só faz sentido se você forneceu um Nonevalor (não ) para stdout, e mesmo assim, é necessário apenas se você definir stdout=subprocess.PIPE. (Caso contrário, você pode apenas fornecer o mesmo argumento fornecido stdout, por exemplo Popen(..., stdout=stream, stderr=stream),.)

Os casos mais fáceis (sem tubos)

Se você não redirecionar nada (deixe todos os três como o Nonevalor padrão ou o fornecimento explícitos None), Pipeé fácil. Ele só precisa desativar o subprocesso e deixá-lo funcionar. Ou, se você redirecionar para um não PIPE- intou um fluxo fileno()- ainda é fácil, pois o sistema operacional faz todo o trabalho. O Python só precisa desativar o subprocesso, conectando seu stdin, stdout e / ou stderr aos descritores de arquivo fornecidos.

O caso ainda fácil: um tubo

Se você redirecionar apenas um fluxo, as Pipecoisas ainda serão fáceis. Vamos escolher um fluxo de cada vez e assistir.

Suponha que você queira fornecer alguns stdin, mas deixe stdoute stderrvá sem redirecionar ou vá para um descritor de arquivo. Como processo pai, seu programa Python simplesmente precisa usar write()para enviar dados pelo canal . Você pode fazer isso sozinho, por exemplo:

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

ou você pode passar os dados stdin para proc.communicate(), o que faz o stdin.writemostrado acima. Não há saída retornando, portanto, communicate()há apenas outro trabalho real: ele também fecha o cano para você. (Se você não ligar, proc.communicate()deverá ligar proc.stdin.close()para fechar o canal, para que o subprocesso saiba que não há mais dados chegando.)

Suponha que você deseja capturar stdout, mas licença stdine stderrsó. Novamente, é fácil: basta ligar proc.stdout.read()(ou equivalente) até que não haja mais saída. Como proc.stdout()é um fluxo de E / S normal do Python, você pode usar todas as construções normais, como:

for line in proc.stdout:

ou, novamente, você pode usar proc.communicate(), o que simplesmente faz isso read()por você.

Se você deseja capturar apenas stderr, funciona da mesma forma que com stdout.

Há mais um truque antes que as coisas fiquem difíceis. Suponha que você queira capturar stdoute também capture, stderrmas no mesmo canal que stdout:

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

Neste caso, subprocess"fraudes"! Bem, ele tem que fazer isso, para que não seja realmente trapaça: ele inicia o subprocesso com seu stdout e seu stderr direcionados para o (único) descritor de pipe que retorna ao processo pai (Python). No lado pai, há novamente apenas um descritor de pipe único para ler a saída. Toda a saída "stderr" é exibida proc.stdoute, se você chamar proc.communicate(), o resultado do stderr (segundo valor na tupla) será None, não uma sequência.

Os casos difíceis: dois ou mais tubos

Todos os problemas surgem quando você deseja usar pelo menos dois tubos. De fato, o subprocesspróprio código tem 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:

Mas, infelizmente, aqui fizemos pelo menos dois e talvez três tubos diferentes, então os count(None)retornos são 1 ou 0. Devemos fazer as coisas da maneira mais difícil.

No Windows, isso costuma threading.Threadacumular resultados para self.stdoute self.stderr, e o thread pai fornece self.stdindados de entrada (e fecha o canal).

No POSIX, ele usa, pollse disponível, caso contrário select, para acumular saída e fornecer entrada stdin. Tudo isso é executado no processo / thread pai (único).

Threads ou poll / select são necessários aqui para evitar conflitos. Suponha, por exemplo, que redirecionemos todos os três fluxos para três canais separados. Suponha ainda que haja um pequeno limite para a quantidade de dados que podem ser inseridos em um tubo antes que o processo de gravação seja suspenso, aguardando que o processo de leitura "limpe" o tubo da outra extremidade. Vamos definir esse pequeno limite para um único byte, apenas para ilustração. (Na verdade, é assim que as coisas funcionam, exceto que o limite é muito maior que um byte.)

Se o processo pai (Python) tentar gravar vários bytes - digamos, 'go\n'para proc.stdin, o primeiro byte entra e o segundo faz com que o processo Python seja suspenso, aguardando o subprocesso ler o primeiro byte, esvaziando o canal.

Enquanto isso, suponha que o subprocesso decida imprimir um amigável "Olá! Não entre em pânico!" cumprimento. Ele Hentra no tubo stdout, mas efaz com que seja suspenso, esperando que seu pai leia isso H, esvaziando o tubo stdout.

Agora estamos paralisados: o processo Python está adormecido, esperando para terminar de dizer "vá" e o subprocesso também está adormecido, esperando para terminar de dizer "Olá! Não entre em pânico!".

O subprocess.Popencódigo evita esse problema com threading-or-select / poll. Quando os bytes podem passar por cima dos tubos, eles vão. Quando não podem, apenas um segmento (não todo o processo) precisa dormir - ou, no caso de seleção / pesquisa, o processo Python espera simultaneamente por "pode ​​escrever" ou "dados disponíveis", grava no stdin do processo somente quando houver espaço e lê seu stdout e / ou stderr somente quando os dados estiverem prontos. O proc.communicate()código (na verdade, _communicateonde os casos cabeludos são tratados) retorna uma vez que todos os dados stdin (se houver) foram enviados e todos os dados stdout e / ou stderr foram acumulados.

Se você quiser ler os dois stdoute stderrem dois canais diferentes (independentemente de qualquer stdinredirecionamento), também precisará evitar o conflito. O cenário de impasse aqui é diferente - ocorre quando o subprocesso grava stderralgum tempo enquanto você extrai dados stdoutou vice-versa - mas ainda está lá.


The Demo

Prometi demonstrar que, não redirecionado, o Python subprocesses grava no stdout subjacente, não sys.stdout. Então, aqui está um 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()

Quando executado:

$ python out.py
start show1
hello
in buffer: sys.stdout being buffered

start show2
hello

Observe que a primeira rotina falhará se você adicionar stdout=sys.stdout, pois um StringIOobjeto não possui fileno. O segundo omitirá o hellose você adicionar stdout=sys.stdoutuma vez sys.stdoutque foi redirecionado para os.devnull.

(Se você redirecionar de Python arquivo descritor de-1, o sub-processo vai seguir esse redirecionamento. A open(os.devnull, 'w')chamada produz um fluxo cujo fileno()é maior do que 2.)

torek
fonte
Hmm. Sua demonstração parece mostrar o oposto da reivindicação no final. Você está redirecionando o stdout do Python para o buffer, mas o subprocesso stdout ainda está indo para o console. Como isso é útil? Estou esquecendo de algo?
Guy Sirton
@GuySirton: a demonstração mostra que o subprocesso stdout (quando não direcionado explicitamente sys.stdout) vai para o stdout do Python , não para o stdout do programa python ( sys.). O que eu admito é uma ... distinção estranha. Existe uma maneira melhor de expressar isso?
Torek
é bom saber, mas realmente queremos capturar a saída do subprocesso aqui, portanto, mudar o sys.stdout é legal, mas não nos ajuda, eu acho. Boa observação de que a comunicação deve estar usando algo como select (), poll ou threads.
Guy Sirton
Eu adicionei uma implementação com select ()
sivann 25/17/17
20

Também podemos usar o iterador de arquivo padrão para ler stdout em vez de usar a construção de iter com readline ().

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)
Jughead
fonte
A resposta mais elegante aqui!
Nir
9
Esta solução não é exibida em tempo real. Espera até que o processo esteja concluído e exibe toda a saída de uma só vez. Na solução de Viktor Kerkez, se "your_command" for exibido progressivamente, a saída seguirá progressivamente, desde que "your_command" libere stdout de tempos em tempos (por causa do pipe).
Eric H.
1
@ Nir porque não é ao vivo.
melMass 04/11/19
Esta solução interage com o descritor padrão, portanto, somente será atualizada quando uma linha for atualizada na saída. Para uma atualização baseada em caracteres, você precisa iterar no método read () conforme mostrado na solução do Viktor. Mas isso foi um exagero para o meu caso de uso.
Jughead
11

Se você puder usar bibliotecas de terceiros, poderá usar algo como sarge(divulgação: eu sou o mantenedor). Essa biblioteca permite acesso sem bloqueio aos fluxos de saída dos subprocessos - está em camadas sobre o subprocessmódulo.

Vinay Sajip
fonte
Bom trabalho no sarge, BTW. Isso realmente resolve os requisitos do OP, mas pode ser um pouco pesado para esse caso de uso.
deepelement
Se você está sugerindo uma ferramenta, pelo menos mostre um exemplo de uso para este caso exato.
Serhiy
4

Solução 1: log stdoutAND stderrsimultaneamente em tempo real

Uma solução simples que registra simultaneamente stdout AND stderr, linha por linha em tempo real, em um arquivo de log.

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()

Solução 2: Uma função read_popen_pipes()que permite iterar nos dois pipes (stdout / stderr), simultaneamente em tempo 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
fonte
3

Uma solução boa, porém "pesada", é usar o Twisted - veja a parte inferior.

Se você estiver disposto a viver apenas com algo stdout nesse sentido, deve 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

(Se você usa read (), ele tenta ler o "arquivo" inteiro que não é útil, o que realmente poderíamos usar aqui é algo que lê todos os dados que estão no canal no momento)

Pode-se também tentar abordar isso com rosqueamento, por exemplo:

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

Agora, poderíamos adicionar stderr potencialmente, tendo dois threads.

Observe, no entanto, a documentação do subprocesso desencoraja o uso desses arquivos diretamente e recomenda o uso communicate()(principalmente relacionados a deadlocks que acho que não são um problema acima) e as soluções são um pouco desajeitadas, então realmente parece que o módulo do subprocesso não é suficiente o trabalho (veja também: http://www.python.org/dev/peps/pep-3145/ ) e precisamos procurar outra coisa.

Uma solução mais envolvida é usar o Twisted, como mostrado aqui: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

A maneira como você faz isso com o Twisted é criar seu processo usando reactor.spawnprocess()e fornecendo um ProcessProtocolque processa a saída de forma assíncrona. O código Python de amostra Twisted está aqui: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py

Guy Sirton
fonte
Obrigado! Eu apenas tentei algo assim (com base no comentário de @PauloAlmeida, mas a minha chamada para subprocess.Popen está bloqueando - isto é, só vem para o loop while uma vez ele retorna ...
DilithiumMatrix
1
Não é isso que está acontecendo. Ele está entrando no loop while imediatamente e bloqueando a read()chamada até o subprocesso sair e o processo pai receber EOFno canal.
Alp
@ Alp interessante! então é.
precisa saber é o seguinte
Sim, eu fui rápido demais para postar isso. Na verdade, ele não funciona corretamente e não pode ser facilmente corrigido. de volta para a mesa de desenho.
Guy Sirton
1
@zhermes: Então o problema com o read () é que ele tentará ler toda a saída até o EOF, o que não é útil. readline () ajuda e pode ser tudo o que você precisa (linhas realmente longas também podem ser um problema). Você também precisa tomar cuidado para amortecer no processo você está lançando ...
Guy Sirton
3

Além de todas essas respostas, uma abordagem simples também pode ser a seguinte:

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

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

    if not line:
        break

    print(line.strip())

Faça um loop pelo fluxo legível, desde que seja legível e, se obtiver um resultado vazio, pare-o.

A chave aqui é que readline()retorna uma linha (com \nno final), desde que haja uma saída e vazia, se realmente estiver no final.

Espero que isso ajude alguém.

kabirbaidhya
fonte
3

Com base em todas as opções acima, sugiro uma versão ligeiramente modificada (python3):

  • while loop chamando readline (a solução iter sugerida pareceu bloquear para sempre - Python 3, Windows 7)
  • para que o manuseio dos dados lidos não precise ser duplicado depois que a pesquisa retornarNone
  • stderr canalizado para stdout para que ambas as saídas de saída sejam lidas
  • Adicionado código para obter o valor de saída do 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
fonte
A returncodeparte foi crucial no meu caso.
stardust
2

Parece que a saída com buffer de linha funcionará para você; nesse caso, algo como o seguinte pode ser adequado. (Advertência: não foi testado.) Isso fornecerá o stdout do subprocesso em tempo real. Se você deseja ter stderr e stdout em tempo real, precisará fazer algo mais complexo 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...
Alpes
fonte
2

Por que não definir stdoutdiretamente para sys.stdout? E se você também precisar enviar para um log, poderá simplesmente substituir o método de gravação 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
fonte
Isso não funcionaria: o módulo do subprocesso bifurca e define o stdoutdescritor de arquivo para o descritor de arquivo do objeto de arquivo passado. O método write nunca seria chamado (pelo menos é o que o subprocesso faz para stderr, eu acho que é o mesmo para stdout).
t.animal
2

Todas as soluções acima que eu tentei falharam ao separar as saídas stderr e stdout (vários pipes) ou bloqueadas para sempre quando o buffer do pipe do SO estava cheio, o que acontece quando o comando que você está executando produz muito rapidamente (há um aviso para isso em python manual do subprocesso poll (). A única maneira confiável que encontrei foi através do select, mas esta é uma solução apenas para 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
fonte
Outra alternativa é girar uma rosca por tubo. Cada thread pode bloquear E / S no tubo, sem bloquear os outros threads. Mas isso introduz seu próprio conjunto de questões. Todos os métodos têm aborrecimentos, basta escolher qual deles você acha menos irritante. :-)
torek 25/05
2

Semelhante às respostas anteriores, mas a seguinte solução funcionou para mim no Windows usando o Python3 para fornecer um método comum para imprimir e efetuar logon em tempo real ( obtendo-saída em tempo real usando 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
fonte
2

Eu acho que o subprocess.communicatemétodo é um pouco enganador: ele realmente preenche o stdout e o stderr que você especificar no subprocess.Popen.

No entanto, lendo o subprocess.PIPEque você pode fornecer aos subprocess.Popen's stdout e stderr parâmetros acabará por preencher buffers tubos OS e impasse seu aplicativo (especialmente se você tem vários processos / threads que usam mustsubprocess ).

Minha solução proposta é fornecer arquivos stdout e stderr - e ler o conteúdo dos arquivos em vez de ler no deadlock PIPE. Esses arquivos podem ser tempfile.NamedTemporaryFile()- que também podem ser acessados ​​para leitura enquanto estão sendo gravados porsubprocess.communicate .

Abaixo está um exemplo de uso:

        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

E este é o código fonte que está pronto para ser usado com tantos comentários quanto eu poderia fornecer para explicar o que ele faz:

Se você estiver usando python 2, primeiro instale a versão mais recente do pacote subprocess32 da 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
fonte
1

Aqui está uma aula que estou usando em um dos meus projetos. Ele redireciona a saída de um subprocesso para o log. No começo, tentei simplesmente substituir o método write, mas isso não funciona como o subprocesso nunca o chamará (o redirecionamento ocorre no nível do editor de arquivos). Então, eu estou usando meu próprio pipe, semelhante a como é feito no subprocesso-módulo. Isso tem a vantagem de encapsular toda a lógica de log / impressão no adaptador e você pode simplesmente transmitir instâncias do logger para 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())

Se você não precisa de log, mas simplesmente deseja usá- print()lo, obviamente pode remover grandes partes do código e manter a classe mais curta. Você também pode expandi-lo por um __enter__e __exit__método e chamar finishedem __exit__para que você possa facilmente utilizá-la como contexto.

t.animal
fonte
1

Nenhuma das soluções Pythonic funcionou para mim. Descobriu-se que proc.stdout.read()ou similar pode bloquear para sempre.

Portanto, eu uso teeassim:

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

Esta solução é conveniente se você já estiver usando shell=True.

${PIPESTATUS}captura o status de sucesso de toda a cadeia de comando (disponível apenas no Bash). Se eu omitisse o && exit ${PIPESTATUS}, isso sempre retornaria zero, pois teenunca falha.

unbufferpode ser necessário imprimir cada linha imediatamente no terminal, em vez de esperar muito tempo até que o "buffer do tubo" seja preenchido. No entanto, o buffer não engole o status de saída de assert (SIG Abort) ...

2>&1 também registra stderror no arquivo.

Mike76
fonte