wyjście na żywo z komendy podprocesu

186

Używam skryptu python jako sterownika kodu hydrodynamicznego. Kiedy przychodzi czas na uruchomienie symulacji, używam subprocess.Popendo uruchomienia kodu, zbieram dane wyjściowe ze stdout i stderr do subprocess.PIPE--- następnie mogę wydrukować (i zapisać w pliku dziennika) informacje wyjściowe i sprawdzić, czy nie ma błędów . Problem polega na tym, że nie mam pojęcia, jak postępuje kod. Jeśli uruchomię go bezpośrednio z wiersza poleceń, wyświetli mi się informacja o tym, w której iteracji jest, o której godzinie, jaki jest następny krok itd.

Czy istnieje sposób zarówno przechowywać dane wyjściowe (do rejestrowania i sprawdzania błędów), jak i generować dane wyjściowe przesyłane strumieniowo na żywo?

Odpowiednia sekcja mojego kodu:

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)

Początkowo byłem potokiem run_commandprzez teetak, że kopia udał się bezpośrednio do pliku dziennika, a strumień wciąż wyjście bezpośrednio na terminal - ale w ten sposób, że nie można przechowywać żadnych błędów (na mój zasób wiedzy).


Edytować:

Rozwiązanie tymczasowe:

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

następnie w innym terminalu uruchom tail -f log.txt(st log_file = 'log.txt').

DilithiumMatrix
źródło
1
Być może możesz użyć Popen.polljak w poprzednim pytaniu Przepełnienie stosu .
Paulo Almeida
Niektóre polecenia, które pokazują wskazanie postępu (np. git), Robią to tylko wtedy, gdy ich wyjściem jest „urządzenie tty” (testowane przez libc isatty()). W takim przypadku może być konieczne otwarcie pseudo-tty.
torek 24.08.13
@torek what a (pseudo-) tty?
DilithiumMatrix
2
Urządzenia w systemach uniksopodobnych, które umożliwiają procesowi udawanie użytkownika na porcie szeregowym. Tak na przykład działa ssh (po stronie serwera). Zobacz bibliotekę pty python , a także pexpect .
torek 24.08.13
Re rozwiązanie tymczasowe: nie ma potrzeby, aby zadzwonić flush, i nie ma potrzeby, aby odczytać z rury stderr jeśli podproces produkuje dużo wyjście stderr. W polu komentarza nie ma wystarczająco dużo miejsca, aby to wyjaśnić ...
torek

Odpowiedzi:

169

Możesz to zrobić na dwa sposoby, tworząc iterator z funkcji readlub readlinei wykonaj następujące czynności:

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)

lub

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)

Lub można utworzyć readeri do writerpliku. Przekazać writerdo Popeni odczytać zreader

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

W ten sposób dane będą zapisywane test.logzarówno na standardowym wyjściu, jak i na nim.

Jedyną zaletą podejścia opartego na plikach jest to, że kod nie blokuje się. W międzyczasie możesz robić, co chcesz i czytać, kiedy chcesz, readerw sposób nie blokujący. Podczas korzystania PIPE, reada readlinefunkcje będą blokować aż albo jedna postać jest napisane na rurze lub linia jest napisane na rurze odpowiednio.

Viktor Kerkez
źródło
1
Ugh :-) pisać do pliku, czytać z niego i spać w pętli? Istnieje również szansa, że ​​proces zakończy się, zanim skończysz czytać plik.
Guy Sirton,
13
Z Pythonie 3, trzeba iter(process.stdout.readline, b'')(tj Sentinel przeszedł do ITER musi być ciąg binarny, ponieważ b'' != ''.
John Mellor
3
W przypadku strumieni binarnych wykonaj następujące czynności:for line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane
6
Dodając do odpowiedzi @JohnMellor, w Pythonie 3 potrzebne były następujące modyfikacje: 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
ale wyjście nie jest na żywo, prawda? z mojego doświadczenia, po prostu czeka, aż proces się zakończy, a dopiero potem drukuje na konsoli. Link -> stackoverflow.com/questions/30026045/…
denis631
91

Streszczenie (lub wersja „tl; dr”): jest łatwe, gdy jest co najwyżej jeden subprocess.PIPE, w przeciwnym razie jest trudne.

Być może nadszedł czas, aby wyjaśnić trochę, jak to subprocess.Popendziała.

(Zastrzeżenie: dotyczy Pythona 2.x, chociaż wersja 3.x jest podobna; i jestem dość rozmyślny w wariancie Windows. Rozumiem rzeczy POSIX znacznie lepiej.)

PopenFunkcja musi radzić sobie z zera do trzech wejść / wyjść, nieco strumieni jednocześnie. Są one oznaczone stdin, stdouti stderrw zwykły sposób.

Możesz podać:

  • None, wskazując, że nie chcesz przekierowywać strumienia. Zamiast tego odziedziczy je jak zwykle. Zauważ, że przynajmniej w systemach POSIX nie oznacza to, że użyje Pythona sys.stdout, tylko rzeczywiste stdout Pythona ; zobacz demo na końcu.
  • intWartość. Jest to „surowy” deskryptor pliku (przynajmniej w POSIX). (Uwaga dodatkowa: PIPEi STDOUTfaktycznie są intwewnętrznie, ale są „niemożliwymi” deskryptorami, -1 i -2).
  • Strumień - naprawdę każdy obiekt za pomocą filenometody. Popenznajdzie deskryptor dla tego strumienia, używając stream.fileno(), a następnie postępuj jak dla intwartości.
  • subprocess.PIPE, wskazując, że Python powinien utworzyć potok.
  • subprocess.STDOUT( stderrtylko dla ): powiedz Pythonowi, aby używał tego samego deskryptora jak dla stdout. Ma to sens tylko wtedy, gdy podałeś (nie None) wartość stdout, a nawet wtedy jest ona potrzebna tylko wtedy, gdy ją ustawisz stdout=subprocess.PIPE. (W przeciwnym razie możesz podać ten sam argument, który podałeś stdout, np Popen(..., stdout=stream, stderr=stream).).

Najłatwiejsze przypadki (bez rur)

Jeśli nic nie przekierujesz (pozostaw wszystkie trzy jako Nonewartość domyślną lub podaj jawnie None), Pipeto jest całkiem łatwe. Musi po prostu wyodrębnić podproces i pozwolić mu działać. Lub, jeśli przekierować do nie- PIPE-an intlub strumień to fileno()-To nadal łatwe, jak system operacyjny wykonuje całą pracę. Python musi po prostu wyodrębnić podproces, łącząc swoje stdin, stdout i / lub stderr z dostarczonymi deskryptorami plików.

Wciąż łatwa obudowa: jedna rura

Jeśli przekierujesz tylko jeden strumień, Pipenadal będzie to całkiem łatwe. Wybierzmy jeden strumień na raz i obejrzyjmy.

Załóżmy, że chcesz podać jakieś stdin, ale pozwól stdouti stderrnie przekieruj lub przejdź do deskryptora pliku. Jako proces nadrzędny, twój program Python musi po prostu użyć write()do wysłania danych w dół potoku. Możesz to zrobić samodzielnie, np .:

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

lub możesz przekazać dane standardowe, do proc.communicate()których następnie wykonuje stdin.writepowyższe czynności. Nic nie wraca, więc communicate()ma tylko jedną prawdziwą robotę: zamyka dla ciebie rurkę. (Jeśli nie zadzwonisz proc.communicate(), musisz zadzwonić, proc.stdin.close()aby zamknąć potok, aby podproces wiedział, że nie ma już danych.)

Załóżmy, że chcesz schwytać, stdoutale odejdź stdini zostań stderrsam. Znowu jest to łatwe: wystarczy zadzwonić proc.stdout.read()(lub odpowiednik), dopóki nie będzie więcej danych wyjściowych. Ponieważ proc.stdout()jest to normalny strumień we / wy Pythona, możesz używać na nim wszystkich normalnych konstrukcji, takich jak:

for line in proc.stdout:

lub ponownie możesz użyć proc.communicate(), co po prostu robi read()dla ciebie.

Jeśli chcesz tylko przechwytywać stderr, działa tak samo jak w przypadku stdout.

Jest jeszcze jedna sztuczka, zanim sprawy staną się trudne. Załóżmy, że chcesz do wychwytywania stdout, a także uchwycić stderrale na tej samej rurze jako standardowe wyjście:

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

W tym przypadku subprocess„kody”! Cóż, musi to zrobić, więc tak naprawdę nie oszukuje: uruchamia podproces zarówno ze standardowym wyjściem standardowym, jak i ze standardowym stderr skierowanym do (pojedynczego) deskryptora potoku, który wraca do procesu nadrzędnego (Python). Po stronie nadrzędnej jest jeszcze tylko jeden deskryptor potoku do odczytu danych wyjściowych. Wszystkie dane wyjściowe „stderr” pojawią się w proc.stdout, a jeśli zadzwonisz proc.communicate(), wynikiem stderr (druga wartość w krotce) Nonenie będzie ciąg znaków.

Twarde skrzynki: dwie lub więcej rur

Wszystkie problemy pojawiają się, gdy chcesz użyć co najmniej dwóch rur. W rzeczywistości subprocesssam kod ma ten 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:

Ale, niestety, tutaj stworzyliśmy co najmniej dwie, a może trzy, różne rury, więc count(None)zwroty wynoszą 1 lub 0. Musimy robić rzeczy ciężko.

W systemie Windows służy to threading.Threaddo gromadzenia wyników dla self.stdouti self.stderr, a wątek nadrzędny dostarcza self.stdindane wejściowe (a następnie zamyka potok).

W POSIX, polljeśli to możliwe, wykorzystuje to, jeśli to możliwe select, do akumulowania produkcji i dostarczania wejścia standardowego. Wszystko to działa w (pojedynczym) procesie / wątku nadrzędnym.

Wątki lub ankieta / wybór są tutaj potrzebne, aby uniknąć impasu. Załóżmy na przykład, że przekierowaliśmy wszystkie trzy strumienie do trzech oddzielnych potoków. Załóżmy ponadto, że istnieje niewielki limit ilości danych, które można włożyć do potoku, zanim proces zapisu zostanie zawieszony, czekając, aż proces odczytu „wyczyści” potok z drugiego końca. Ustawmy ten mały limit na jeden bajt, tylko dla ilustracji. (W rzeczywistości tak to działa, z tym wyjątkiem, że limit jest znacznie większy niż jeden bajt.)

Jeśli proces nadrzędny (Python) próbuje zapisać kilka bajtów - powiedzmy, 'go\n'do proc.stdinpierwszego bajtu wchodzi, a następnie drugi powoduje zawieszenie procesu Python, czekając, aż podproces odczyta pierwszy bajt, opróżniając potok.

Tymczasem załóżmy, że podproces zdecyduje się wydrukować przyjazne „Cześć! Nie panikuj!” Powitanie. HIdzie do swojej rury stdout, ale epowoduje to do zawieszenia, czekając na jego rodzic, aby przeczytać, że H, opróżnianie rury standardowe wyjście.

Teraz utknęliśmy: proces Python śpi, czekając, aż skończy mówić „idź”, a podproces również śpi, czekając, aż skończy mówiąc „Cześć!

subprocess.PopenKod pozwala uniknąć tego problemu z gwintowania-or-select / sondzie. Gdy bajty mogą przejść przez potoki, idą. Gdy nie mogą, tylko wątek (nie cały proces) musi spać - lub, w przypadku wyboru / odpytywania, proces Pythona czeka jednocześnie na „może pisać” lub „dostępne dane”, zapisuje na standardowe wyjście procesu tylko gdy jest miejsce i odczytuje stdout i / lub stderr tylko wtedy, gdy dane są gotowe. proc.communicate()Kod (właściwie _communicategdzie owłosionej przypadki są obsługiwane) powraca po wszystkich danych stdin (jeśli w ogóle) zostały wysłane, a wszystkie dane stdout i / lub stderr zostały zgromadzone.

Jeśli chcesz przeczytać zarówno stdouti stderrna dwóch różnych rur (niezależnie od wszelkich stdinprzekierowania), trzeba, aby uniknąć impasu też. Scenariusz impasu jest tutaj inny - występuje, gdy podproces zapisuje coś długo, stderrpodczas gdy pobierasz dane stdoutlub odwrotnie - ale wciąż tam jest.


Demo

Obiecałem zademonstrować, że bez przekierowania Python subprocesses pisze do podstawowego interfejsu, a nie sys.stdout. Oto kod:

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

Po uruchomieniu:

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

start show2
hello

Zauważ, że pierwsza procedura zakończy się niepowodzeniem, jeśli dodasz stdout=sys.stdout, ponieważ StringIOobiekt nie ma fileno. Drugi pominie hellojeśli dodasz, stdout=sys.stdoutponieważ sys.stdoutzostał przekierowany do os.devnull.

(Jeśli przekierujesz deskryptor pliku 1 Pythona, podproces będzie podążał za tym przekierowaniem. open(os.devnull, 'w')Wywołanie tworzy strumień, którego wartość fileno()jest większa niż 2.)

Trek
źródło
Hmm Twoje demo wydaje się w końcu wskazywać na przeciwieństwo roszczenia. Przekierowujesz standardowe wyjście Pythona do bufora, ale standardowe wyjście podprocesu nadal przechodzi do konsoli. Jak to jest przydatne? Czy coś brakuje?
Guy Sirton,
@GuySirton: demonstracja pokazuje, że standardowe wyjście podprocesu (gdy nie jest to wyraźnie wskazane sys.stdout) przechodzi do standardowego interfejsu Pythona , a nie standardowego programu Pythona ( sys.). Przyznaję, że to ... dziwne rozróżnienie. Czy istnieje lepszy sposób na sformułowanie tego?
torek 24.08.13
to dobrze wiedzieć, ale naprawdę chcemy uchwycić dane wyjściowe podprocesu, więc zmiana sys.stdout jest fajna, ale myślę, że to nam nie pomaga. Dobra obserwacja, która się komunikuje, wymaga użycia czegoś takiego jak select (), ankieta lub wątki.
Guy Sirton,
Dodałem implementację z select ()
sivann
20

Możemy również użyć domyślnego iteratora plików do odczytu standardowego wyjścia zamiast używania iteracyjnej konstrukcji z readline ().

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)
Jughead
źródło
Najbardziej elegancka odpowiedź tutaj!
Nir
9
To rozwiązanie nie wyświetla się w czasie rzeczywistym. Czeka na zakończenie procesu i wyświetla wszystkie dane wyjściowe naraz. W rozwiązaniu Viktora Kerkeza, jeśli „twoje_polecenie” wyświetla się progresywnie, dane wyjściowe następują stopniowo, o ile „twoje_polecenie” od czasu do czasu opróżnia stdout (z powodu potoku).
Eric H.
1
@Nir, ponieważ nie jest na żywo.
melMass
To rozwiązanie iteruje po domyślnym deskryptorze, więc będzie aktualizować się tylko wtedy, gdy wiersz zostanie zaktualizowany na wyjściu. W przypadku aktualizacji opartej na postaciach należy iterować metodę read (), jak pokazano w rozwiązaniu Viktora. Ale to była przesada w moim przypadku użycia.
Jughead,
11

Jeśli możesz korzystać z bibliotek stron trzecich, możesz użyć czegoś takiego sarge(ujawnienie: jestem jego opiekunem). Ta biblioteka umożliwia nieblokujący dostęp do strumieni wyjściowych z podprocesów - jest warstwowa nad subprocessmodułem.

Vinay Sajip
źródło
Dobra robota na Sarge, BTW. To rzeczywiście rozwiązuje wymaganie OP, ale może być nieco trudne w przypadku tego przypadku użycia.
deepelement
Jeśli sugerujesz narzędzie, pokaż przynajmniej przykład użycia w tym konkretnym przypadku.
Serhiy
4

Rozwiązanie 1: Zaloguj się stdoutORAZ stderrjednocześnie w czasie rzeczywistym

Proste rozwiązanie, które rejestruje jednocześnie stdout AND stderr, linia po linii w czasie rzeczywistym do pliku dziennika.

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

Rozwiązanie 2: Funkcja read_popen_pipes()umożliwiająca iterację po obu rurach (stdout / stderr), jednocześnie w czasie rzeczywistym

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
źródło
3

Dobrym, ale „ciężkim” rozwiązaniem jest użycie Twisted - patrz na dole.

Jeśli chcesz żyć tylko ze standardowym wyjściem, coś w tym stylu powinno działać:

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

(Jeśli użyjesz read (), to spróbuje odczytać cały „plik”, co nie jest przydatne, tak naprawdę moglibyśmy użyć tutaj czegoś, co odczytuje wszystkie dane, które są teraz w potoku)

Można również spróbować podejść do tego za pomocą wątków, np .:

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

Teraz możemy potencjalnie dodać również stderr, mając dwa wątki.

Zauważ jednak, że dokumenty podprocesów odradzają bezpośrednie korzystanie z tych plików i zaleca się ich używać communicate()(głównie dotyczy zakleszczeń, które moim zdaniem nie są problemem powyżej), a rozwiązania są trochę nieporadne, więc wygląda na to, że moduł podprocesów nie jest całkiem gotowy zadanie (patrz także: http://www.python.org/dev/peps/pep-3145/ ) i musimy spojrzeć na coś innego.

Bardziej zaangażowanym rozwiązaniem jest użycie Twisted, jak pokazano tutaj: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

Sposób, w jaki to robisz za pomocą Twisted, polega na utworzeniu procesu za pomocą reactor.spawnprocess()i zapewnieniu, ProcessProtocolże następnie przetwarza dane wyjściowe asynchronicznie. Przykładowy kod Pythona Twisted znajduje się tutaj: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py

Guy Sirton
źródło
Dzięki! Właśnie próbowałem czegoś takiego (na podstawie komentarza @PauloAlmeida, ale moje wezwanie do podprocesu. Otwarcie blokuje - tzn. Przychodzi do pętli while tylko po powrocie ...
DilithiumMatrix
1
Nie o to chodzi. Od razu wchodzi w pętlę while, a następnie blokuje read()wywołanie, dopóki podproces nie zakończy się i proces nadrzędny nie odbierze EOFw potoku.
Alp
@Alp ciekawe! tak jest.
DilithiumMatrix
Tak, zbyt szybko to opublikowałem. W rzeczywistości nie działa poprawnie i nie można go łatwo naprawić. powrót do tabeli rysunkowej.
Guy Sirton,
1
@zhermes: Problem z read () polega na tym, że spróbuje odczytać cały wynik do EOF, co nie jest przydatne. readline () pomaga i może być wszystkim, czego potrzebujesz (naprawdę długie linie mogą również stanowić problem). Musisz także uważać na buforowanie w procesie, który uruchamiasz ...
Guy Sirton,
3

Oprócz wszystkich tych odpowiedzi jedno proste podejście może wyglądać następująco:

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

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

    if not line:
        break

    print(line.strip())

Zapętlaj czytelny strumień, dopóki jest on czytelny, a jeśli otrzyma pusty wynik, zatrzymaj go.

Kluczem tutaj jest to, że readline()zwraca linię (z \nna końcu), o ile istnieje wyjście i jest puste, jeśli naprawdę jest na końcu.

Mam nadzieję, że to komuś pomoże.

kabirbaidhya
źródło
3

Na podstawie wszystkich powyższych proponuję nieco zmodyfikowaną wersję (python3):

  • podczas wywoływania pętli readline (sugerowane rozwiązanie iterowe wydawało mi się blokować na zawsze - Python 3, Windows 7)
  • tak ustrukturyzowane, że obsługa odczytanych danych nie musi być duplikowana po zwróceniu ankiety -None
  • stderr podłączony do standardowego wyjścia, aby oba wyjścia były odczytywane
  • Dodano kod, aby uzyskać wartość wyjściową cmd.

Kod:

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
źródło
Ta returncodeczęść była w moim przypadku kluczowa.
gwiezdny pył,
2

Wygląda na to, że wyjście buforowane wierszem będzie dla ciebie działać, w takim przypadku może pasować coś takiego jak poniżej. (Zastrzeżenie: nie zostało przetestowane). To da stdout podprocesu tylko w czasie rzeczywistym. Jeśli chcesz mieć stderr i stdout w czasie rzeczywistym, musisz zrobić coś bardziej złożonego 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...
Turnia
źródło
2

Dlaczego nie ustawić stdoutbezpośrednio na sys.stdout? A jeśli musisz również wyprowadzać dane do dziennika, możesz po prostu przesłonić metodę zapisu 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
źródło
To nie zadziałałoby: moduł podprocesu rozwidla i ustawia stdoutdeskryptor pliku na deskryptor pliku przekazywanego obiektu pliku. Metoda write nigdy nie zostałaby wywołana (przynajmniej tak działa podproces dla stderr, wydaje mi się, że jest taki sam dla stdout).
t
2

Wszystkie powyższe rozwiązania, których próbowałem, nie były w stanie oddzielić wyjścia stderr i stdout (wiele potoków) lub zostały zablokowane na zawsze, gdy bufor potoku systemu operacyjnego był pełny, co dzieje się, gdy polecenie, które uruchamiasz, uruchamia się zbyt szybko (pojawia się ostrzeżenie w pythonie poll () instrukcja podprocesu). Jedyny niezawodny sposób, który znalazłem, to wybór, ale jest to rozwiązanie tylko dla 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
źródło
Inną alternatywą jest odkręcenie jednego gwintu na rurę. Każdy wątek może blokować wejścia / wyjścia na rurze, bez blokowania innych wątków. Ale to wprowadza własny zestaw problemów. Wszystkie metody mają irytujące, po prostu wybierasz te, które uważasz za najmniej denerwujące. :-)
torek
2

Podobne do poprzednich odpowiedzi, ale następujące rozwiązanie działało dla mnie w systemie Windows przy użyciu Python3, aby zapewnić wspólną metodę drukowania i logowania w czasie rzeczywistym ( pobieranie-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
źródło
2

Myślę, że subprocess.communicatemetoda jest nieco myląca: w rzeczywistości wypełnia stdout i stderr , które określasz w subprocess.Popen.

Jednak czytanie z subprocess.PIPEktórych można dostarczać do subprocess.Popen„s stdout i stderr parametrów ostatecznie zapełni się bufory rur OS i impasu swoją aplikację (zwłaszcza jeśli masz wiele procesów / wątków, które należy wykorzystać subprocess).

Moim proponowanym rozwiązaniem jest dostarczenie stdout i stderr plików - i odczytanie zawartości plików zamiast czytania z impasu PIPE. Pliki te mogą być tempfile.NamedTemporaryFile()- do których można również uzyskać dostęp w celu odczytu podczas ich zapisywania przez subprocess.communicate.

Poniżej znajduje się przykładowe użycie:

        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

A to jest kod źródłowy, który jest gotowy do użycia z tyloma komentarzami, ile mógłbym podać, aby wyjaśnić, co robi:

Jeśli używasz Pythona 2, najpierw zainstaluj najnowszą wersję pakietu subprocess32 z 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
źródło
1

Oto klasa, której używam w jednym z moich projektów. Przekierowuje wyjście podprocesu do dziennika. Na początku próbowałem po prostu nadpisać metodę write, ale to nie działa, ponieważ podproces nigdy jej nie wywoła (przekierowanie odbywa się na poziomie skryptu plików). Używam więc własnego potoku, podobnie jak w module podprocesu. Ma to tę zaletę, że hermetyzuje całą logikę rejestrowania / drukowania w adapterze i można po prostu przekazać instancje rejestratora do 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())

Jeśli nie potrzebujesz rejestrować, ale po prostu chcesz z niego skorzystać print(), możesz oczywiście usunąć duże części kodu i skrócić klasę. Można również rozszerzyć ją __enter__oraz __exit__metody i nazywają finishedsię __exit__tak, że można łatwo używać go jako kontekst.

t. zwierzę
źródło
1

Żadne z rozwiązań Pythonic nie działało dla mnie. Okazało się, że coś proc.stdout.read()podobnego może blokować na zawsze.

Dlatego używam w teeten sposób:

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

To rozwiązanie jest wygodne, jeśli już używasz shell=True.

${PIPESTATUS}przechwytuje status powodzenia całego łańcucha poleceń (dostępne tylko w Bash). Gdybym pominął && exit ${PIPESTATUS}, to zawsze zwróciłoby zero, ponieważ teenigdy nie zawodzi.

unbuffermoże być konieczne do wydrukowania każdej linii natychmiast do terminala, zamiast czekać zbyt długo, aż „bufor bufora” zostanie wypełniony. Jednak unbuffer połyka status wyjścia aser (SIG Abort) ...

2>&1 loguje również stderror do pliku.

Mike76
źródło