Python: podproces.wywołanie, stdout do pliku, stderr do pliku, wyświetlanie stderr na ekranie w czasie rzeczywistym

Mam narzędzie wiersza poleceń (właściwie kilka), dla którego piszę wrapper w Pythonie.

Narzędzie jest zwykle używane w następujący sposób:

 $ path_to_tool -option1 -option2 > file_out

Użytkownik otrzymuje wynik zapisany do file_out, a także jest w stanie zobaczyć różne komunikaty o statusie narzędzia podczas jego działania.

Chcę replikować to zachowanie, jednocześnie rejestrując stderr (komunikaty o statusie) do pliku.

Mam to:

from subprocess import call
call(['path_to_tool','-option1','option2'], stdout = file_out, stderr = log_file)

To działa dobrze z tym, że stderr nie jest napisany na ekran. Oczywiście mogę dodać kod, aby wydrukować zawartość log_file na ekranie, ale wtedy użytkownik zobaczy go po wszystkim, a nie w czasie, gdy to się dzieje.

Podsumowując, pożądane zachowanie to:

  1. użyj call () lub subprocess ()
  2. bezpośrednie wyjście stdout do pliku
  3. bezpośrednio stderr do pliku, jednocześnie zapisując stderr na ekranie w czasie rzeczywistym, tak jakby narzędzie zostało wywołane bezpośrednio z linii poleceń.

Mam czuję, że albo brakuje mi czegoś naprawdę prostego, albo To jest o wiele bardziej skomplikowane niż myślałem...dzięki za pomoc!

EDIT: to musi działać tylko na Linuksie.

Author: Ben S., 2013-08-21

3 answers

Możesz to zrobić z

, ale to nie jest trywialne. Jeśli spojrzysz na często używane argumenty W dokumentach, zobaczysz, że możesz przekazać PIPE jako argument stderr, który tworzy nowy potok, przekazuje jedną stronę potoku procesowi potomnemu i udostępnia drugą stronę jako atrybut stderr.*

Więc będziesz musiał obsługiwać tę rurę, pisząc na ekran i do pliku. Ogólnie rzecz biorąc, uzyskanie właściwych szczegółów jest bardzo trudne.** W Twoim przypadku jest tylko jedna rura i planujesz ją serwisować synchronicznie, więc nie jest tak źle.

import subprocess
proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
for line in proc.stderr:
    sys.stdout.write(line)
    log_file.write(line)
proc.wait()

(zauważ, że są pewne problemy z używaniem for line in proc.stderr:-zasadniczo, jeśli to, co czytasz, okaże się z jakiegokolwiek powodu nie buforowane, możesz siedzieć i czekać na nowy wiersz, nawet jeśli w rzeczywistości jest połowa wartości linii danych do przetworzenia. Możesz odczytywać fragmenty na raz, powiedzmy, read(128), a nawet read(1), aby uzyskać dane bardziej płynnie, jeśli to konieczne. Jeśli potrzebujesz w rzeczywistości pobieramy każdy bajt, gdy tylko nadejdzie, i nie możemy sobie pozwolić na koszt read(1), musisz ustawić rurę w trybie nieblokującym i odczytywać asynchronicznie.)


Ale jeśli korzystasz z Uniksa, łatwiej byłoby użyć tee, aby zrobić to za Ciebie.

Aby uzyskać szybkie i brudne rozwiązanie, możesz użyć powłoki, aby przejść przez nią. Coś takiego:

subprocess.call('path_to_tool -option1 option2 2|tee log_file 1>2', shell=True,
                stdout=file_out)

Ale nie chcę debugować rurociągów powłoki; zróbmy to w Pythonie, jak pokazano w docs :

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stderr)
tool.stderr.close()
tee.communicate()

Wreszcie, istnieje kilkanaście lub więcej owijarek wyższego poziomu wokół podprocesów i / lub powłoki na PyPI-sh, shell, shell_command, shellout, iterpipes, sarge, cmd_utils, commandwrapper, itd. Wyszukaj "shell"," podprocess"," process"," command line", ITD. i znajdź taki, który Ci się podoba, który sprawia, że problem jest banalny.


Co jeśli trzeba zebrać zarówno stderr i stdout?

Najprostszym sposobem na to jest przekierowanie jednego do drugiego, jako Sven Marnach sugeruje w komentarzu. Po prostu zmień Popen parametry tak:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

A potem wszędzie, gdzie użyłeś tool.stderr, użyj tool.stdout zamiast-np. dla ostatniego przykładu:

tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stdout)
tool.stdout.close()
tee.communicate()

Ale to ma kilka kompromisów. Oczywiście mieszanie tych dwóch strumieni oznacza, że nie możesz zalogować stdout do file_out i stderr do log_file, lub skopiować stdout do stdout i stderr do stderr. Oznacza to również, że kolejność może być niedeterministyczna-jeśli podproces zawsze zapisuje dwie linie do stderr przed napisaniem czegokolwiek na stdout, możesz skończyć otrzymywaniem kilku stdout między tymi dwoma liniami po zmieszaniu strumieni. Oznacza to, że muszą współdzielić tryb buforowania stdout, więc jeśli opierasz się na tym, że linux / glibc gwarantuje, że stderr będzie buforowany liniowo (chyba że podproces jawnie go zmieni), to może to już nie być prawdą.


Jeśli trzeba obsłużyć dwa procesy oddzielnie, staje się to trudniejsze. Wcześniej mówiłem, że serwisowanie rury w locie jest łatwe, o ile masz tylko jedną rurę i możesz ją serwisować synchronicznie. Jeśli masz dwie rury, to oczywiście nie jest już prawdą. Wyobraź sobie, że czekasz na tool.stdout.read(), a nowe dane przychodzą z tool.stderr. Jeśli jest zbyt dużo danych, może to spowodować przepełnienie rury i zablokowanie podprocesu. Ale nawet jeśli tak się nie stanie, oczywiście nie będziesz w stanie odczytać i zalogować danych stderr, dopóki coś nie pojawi się ze stdout.

Jeśli użyjesz rozwiązania pipe-through - tee, unikniesz początkowy problem ... ale tylko przez stworzenie nowego projektu, który jest tak samo zły. Masz dwa tee instancje, a podczas gdy dzwonisz communicate na jednym, drugi siedzi i czeka w nieskończoność.

Tak czy inaczej, potrzebujesz jakiegoś mechanizmu asynchronicznego. Można to zrobić za pomocą wątków, reaktora select, czegoś w rodzaju gevent itp.

Oto szybki i brudny przykład:

proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def tee_pipe(pipe, f1, f2):
    for line in pipe:
        f1.write(line)
        f2.write(line)
t1 = threading.Thread(target=tee_pipe, args=(proc.stdout, file_out, sys.stdout))
t2 = threading.Thread(target=tee_pipe, args=(proc.stderr, log_file, sys.stderr))
t3 = threading.Thread(proc.wait)
t1.start(); t2.start(); t3.start()
t1.join(); t2.join(); t3.join()

Są jednak pewne przypadki, w których to nie zadziała. (Problemem jest kolejność w którym przybywają SIGCHLD i SIGPIPE/EPIPE/EOF. Nic z tego nie wpłynie na nas tutaj, ponieważ nie wysyłamy żadnych informacji ... ale nie ufaj mi w tym bez przemyślenia i / lub testowania.) The subprocess.communicate Funkcja od 3.3+ pobiera wszystkie fiddly szczegóły w prawo. Ale może być o wiele prostsze użycie jednej z implementacji async-subprocess wrapper, które można znaleźć na PyPI i ActiveState, a nawet podprocesów z pełnowartościowego frameworka async, takiego jak Pokręcone.


* dokumenty nie wyjaśniają, czym są rury, prawie tak, jakby oczekiwały, że będziesz starą ręką Uniksa C... ale niektóre przykłady, szczególnie w sekcji zastąpienie starszych funkcji modułem subprocess , pokazują, jak są używane, i to jest dość proste.

** najtrudniejsze jest uporządkowanie dwóch lub więcej rur prawidłowo. Jeśli czekasz na jednej rurze, druga może przepełnić się i zablokować, uniemożliwiając oczekiwanie na drugiej. Jedyne łatwe sposobem na obejście tego jest stworzenie wątku do obsługi każdej rury. (Na większości platform *nix można zamiast tego użyć reaktora select lub poll, ale zrobienie tego międzyplatformowego jest niezwykle trudne.) źródło do modułu, szczególnie communicate i jego pomocników, pokazuje jak to zrobić. (Podlinkowałem do 3.3, ponieważ we wcześniejszych wersjach, communicate sama w sobie źle robi pewne ważne rzeczy...) dlatego, jeśli to możliwe, chcesz użyć communicate, jeśli potrzebujesz więcej niż jednej rury. W Twoim przypadku nie możesz użyj communicate, ale na szczęście nie potrzebujesz więcej niż jednej rury.

 56
Author: abarnert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-22 17:36:35

Myślę, że to, czego szukasz, to coś w stylu:

import sys, subprocess
p = subprocess.Popen(cmdline,
                     stdout=sys.stdout,
                     stderr=sys.stderr)

Aby dane wyjściowe / log były zapisywane do pliku, zmodyfikowałbym moje cmdline, aby zawierały zwykłe przekierowania, tak jak robiłoby się to na zwykłej powłoce / powłoce Linuksa. Na przykład dodałbym tee do wiersza poleceń: cmdline += ' | tee -a logfile.txt'

Mam nadzieję, że to pomoże.
 0
Author: Brandt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-11 12:20:34

Musiałem wprowadzić kilka zmian w odpowiedzi @abarnert na Python 3. To chyba działa:

def tee_pipe(pipe, f1, f2):
    for line in pipe:
        f1.write(line)
        f2.write(line)

proc = subprocess.Popen(["/bin/echo", "hello"],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)

# Open the output files for stdout/err in unbuffered mode.
out_file = open("stderr.log", "wb", 0)
err_file = open("stdout.log", "wb", 0)

stdout = sys.stdout
stderr = sys.stderr

# On Python3 these are wrapped with BufferedTextIO objects that we don't
# want.
if sys.version_info[0] >= 3:
    stdout = stdout.buffer
    stderr = stderr.buffer

# Start threads to duplicate the pipes.
out_thread = threading.Thread(target=tee_pipe,
                              args=(proc.stdout, out_file, stdout))
err_thread = threading.Thread(target=tee_pipe,
                              args=(proc.stderr, err_file, stderr))

out_thread.start()
err_thread.start()

# Wait for the command to finish.
proc.wait()

# Join the pipe threads.
out_thread.join()
err_thread.join()
 0
Author: Timmmm,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-16 12:13:38