サブプロセスコマンドからのライブ出力


186

流体力学コードのドライバーとしてpythonスクリプトを使用しています。シミュレーションを実行するときが来たらsubprocess.Popen、コードを実行し、stdoutとstderrからの出力を収集してsubprocess.PIPE---次に、出力情報を印刷(およびログファイルに保存)して、エラーをチェックします。問題は、コードの進行状況がわからないことです。コマンドラインから直接実行すると、その反復、時間、次のタイムステップなどについての出力が得られます。

(ロギングとエラーチェックのために)出力を保存し、ライブストリーミング出力を生成する方法はありますか?

私のコードの関連セクション:

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)

もともと私はパイプをrun_command介しteeてパイプしていたため、コピーは直接ログファイルに送られ、ストリームは依然として端末に直接出力されていましたが、その方法では(私の知識に対して)エラーを格納できません。


編集:

一時的な解決策:

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

次に、別のターミナルでtail -f log.txt(st log_file = 'log.txt')を実行します。


1
たぶん、以前のスタックオーバーフローの質問のPopen.pollように使用できます。
Paulo Almeida

進行状況を示すいくつかのコマンド(たとえば、git)は、それらの出力が「ttyデバイス」(libcを介してテストされたisatty())である場合にのみそうします。その場合、疑似ttyを開く必要があるかもしれません。
torek 2013

@torek(疑似)ttyとは何ですか?
DilithiumMatrix 2013

2
プロセスがシリアルポートでユーザーのふりをすることを可能にするUnixライクなシステム上のデバイス。これは、たとえばssh(サーバー側)の動作方法です。python ptyライブラリpexpectも参照してください。
torek 2013

一時的な解決日時:コールする必要はありませんflushし、そこにあるサブプロセスは非常に標準エラー出力を生成する場合標準エラー出力パイプから読み込む必要が。これを説明するためのコメントフィールドに十分なスペースがありません...
torek

回答:


169

これを行うには、readまたはreadline関数からイテレータを作成して次の2つの方法があります。

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)

または

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)

または、readerおよびwriterファイルを作成できます。合格writerPopenとから読み取りますreader

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

これによりtest.log、標準出力だけでなくにもデータが書き込まれます。

ファイルアプローチの唯一の利点は、コードがブロックしないことです。そのため、その間は何でも好きなことができ、いつでも好きなときにreaderノンブロッキングで読むことができます。を使用するPIPEreadreadline関数は、1つの文字がパイプに書き込まれるか、1行がそれぞれパイプに書き込まれるまでブロックされます。


1
ええと:-)ファイルに書き込み、ファイルから読み取り、ループでスリープしますか?また、ファイルの読み取りが完了する前にプロセスが終了する可能性もあります。
ガイサートン2013

13
Pythonの3では、あなたが必要とするiter(process.stdout.readline, b'')(すなわちに渡されたセンチネルITERがあるため、バイナリ文字列にする必要があるb'' != ''
ジョンメラー

3
:バイナリストリームの場合、これを行うfor line in iter(process.stdout.readline, b''): sys.stdout.buffer.write(line)
rrlamichhane

6
以下の変更が必要とされたのPython 3に、@JohnMellorの答えに追加: 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
しかし、出力はライブではありませんか?私の経験では、プロセスの実行が完了するまで待機してから、コンソールに出力します。リンク-> stackoverflow.com/questions/30026045/…–
denis631

91

エグゼクティブサマリー(または「tl; dr」バージョン):が1つ以下の場合は簡単ですがsubprocess.PIPE、そうでない場合は難しいです。

どのようにsubprocess.Popen機能するかについて少し説明するときかもしれません。

(注意:これはPython 2.xの場合ですが、3.xも同様です。Windowsバリアントについては非常にあいまいです。POSIXについてはもっとよく理解しています。)

このPopen関数は、ゼロから3つのI / Oストリームを多少同時に処理する必要があります。これらは表記されstdinstdoutそして、stderrいつものように。

以下を提供できます。

  • None、ストリームをリダイレクトしないことを示します。代わりに通常どおりこれらを継承します。少なくともPOSIXシステムでは、これはPythonのを使用することを意味するのではなくsys.stdout、Pythonの実際の stdout のみを使用することに注意してください。最後にデモをご覧ください。
  • int値。これは(少なくともPOSIXでは)「生の」ファイル記述子です。(傍注:PIPEおよびSTDOUTは実際にはint内部で使用されていますが、「不可能」な記述子であり、-1および-2です。)
  • ストリーム—実際には、filenoメソッドを持つオブジェクト。 Popenを使用してstream.fileno()、そのストリームの記述子を検索し、int値の場合と同様に処理します。
  • subprocess.PIPE、Pythonがパイプを作成する必要があることを示します。
  • subprocess.STDOUTstderrのみ):と同じ記述子を使用するようにPythonに指示しますstdout。これは、に(非None)値を指定した場合にのみ意味がstdoutあり、それでも、を設定した場合にのみ必要ですstdout=subprocess.PIPE。(そうしないと、あなたはちょうどあなたがのために提供される同じ引数を提供することができstdout、例えば、Popen(..., stdout=stream, stderr=stream)。)

最も簡単なケース(パイプなし)

何もリダイレクトしない場合(3つすべてをデフォルトNone値のままにするかNone、明示的に指定する)、Pipeそれは非常に簡単です。サブプロセスを分離して実行させるだけです。あなたが非にリダイレクトする場合は、PIPE-an intまたはストリームだfileno()OSは、すべての作業を行うよう-itは、簡単にはまだです。Pythonはサブプロセスをスピンオフし、そのstdin、stdout、および/またはstderrを提供されたファイル記述子に接続するだけです。

まだ簡単なケース:1つのパイプ

ストリームを1つだけリダイレクトしPipeても、非常に簡単です。一度に1つのストリームを選択して見てみましょう。

あなたには、いくつかを供給するとしますstdinが、聞かせてstdoutstderr非リダイレクト行く、またはファイル記述子にアクセスしてください。親プロセスとして、Pythonプログラムwrite()はパイプを使用してデータを送信するために使用する必要があるだけです。これは自分で行うことができます。例:

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

または、あなたはに標準入力データを渡すことができproc.communicate()、その後んれ、stdin.write上に示しました。出力は返されないので、communicate()他に1つだけ実際のジョブがあります。パイプも閉じます。(呼び出さない場合は、呼び出してパイプを閉じるproc.communicate()必要がありますproc.stdin.close()。これにより、サブプロセスがデータがなくなったことを認識できます。)

キャプチャしたいstdoutstdinstderr一人にしたいとします。繰り返しますが、簡単ですproc.stdout.read()。出力がなくなるまで(または同等のものを)呼び出すだけです。以来proc.stdout()、通常のPython Iである/ Oあなたはのように、それにすべての通常の構文を使用することができますストリーム:

for line in proc.stdout:

または、もう一度、あなたが使用することができproc.communicate()、単純にしている、read()あなたのため。

のみをキャプチャする場合はstderr、と同じように機能しstdoutます。

物事が困難になる前に、もう1つのトリックがあります。あなたがキャプチャしたいとしstdout、また、キャプチャstderrが、標準出力と同じパイプに:

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

この場合、subprocess「カンニング」!まあ、これを行う必要があるので、実際には不正行為ではありません。親プロセス(Python)にフィードバックする(単一の)パイプ記述子にstdoutとstderrの両方を指定して、サブプロセスを開始します。親側では、出力を読み取るためのパイプ記述子が1つだけあります。すべての「stderr」出力はに表示されproc.stdout、を呼び出すproc.communicate()と、stderrの結果(タプルの2番目の値)はNone文字列ではなくになります。

ハードケース:2つ以上のパイプ

少なくとも2つのパイプを使用したいときに問題が発生します。実際、subprocessコード自体には次のビットがあります。

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:

しかし、悲しいかな、ここでは少なくとも2つ、場合によっては3つの異なるパイプを作成したので、count(None)戻り値は1または0のどちらかになります。

Windowsでは、この用途threading.Threadのための蓄積の結果とself.stdoutしてself.stderr、とは、親スレッドが提供する持っているself.stdin(そして近いとパイプ)入力データを。

POSIXでは、poll利用可能な場合はこれを使用し、そうでない場合はselect、出力を蓄積してstdin入力を配信します。これはすべて、(単一の)親プロセス/スレッドで実行されます。

デッドロックを回避するには、スレッドまたはポーリング/選択が必要です。たとえば、3つのストリームすべてを3つの別々のパイプにリダイレクトしたとします。さらに、書き込みプロセスが一時停止し、読み取りプロセスがパイプをもう片方から「一掃」するのを待機する前に、パイプに詰め込むことができるデータ量に小さな制限があるとします。説明のために、その小さな制限を1バイトに設定してみましょう。(実際には、制限が1バイトよりもはるかに大きいことを除いて、これはどのように機能するかです。)

親(Python)プロセスがいくつかのバイトを書き込もうとすると、たとえば'go\n'toのproc.stdin場合、最初のバイトが入り、次に2番目のプロセスがPythonプロセスを中断し、サブプロセスが最初のバイトを読み取るのを待って、パイプを空にします。

一方、サブプロセスが「Hello!Do n't Panic!」というフレンドリーなメッセージを印刷することにしたとします。挨拶。Hその標準出力パイプになりますが、eその親がそれを読むのを待って、中断することの原因となるH標準出力パイプを空にし、。

これでスタックしました。Pythonプロセスはスリープ状態で、「go」と言うのを完了するのを待っています。また、サブプロセスもスリープ状態で、「Hello!Do n't Panic!」と言うのを完了するのを待っています。

subprocess.Popenコードはスレッド-または選択/世論調査で、この問題を回避することができます。バイトがパイプを通過できるとき、それらは行きます。できない場合は、スレッド全体(プロセス全体ではなく)だけがスリープ状態になります。または、select / pollの場合、Pythonプロセスは「書き込み可能」または「データが利用可能」になるまで同時に待機し、プロセスの標準入力に書き込みます空きがある場合にのみ、データの準備ができている場合にのみstdoutやstderrを読み取ります。proc.communicate()コード(実際には_communicateすべての標準入力データ(もしあれば)一度ヘアリーケースが処理される)戻って送信された、すべてのstdoutおよび/またはstderrのデータが蓄積されています。

(リダイレクトに関係なく)両方stdoutstderr2つの異なるパイプで読み取る場合はstdin、デッドロックも回避する必要があります。ここでのデッドロックシナリオは異なりstderrます。stdoutつまり、からデータをプルしている間にサブプロセスが何かに長い書き込みを行った場合、またはその逆の場合に発生しますが、それでもまだあります。


デモ

リダイレクトされずに、Pythonがsubprocess、ではなく、基になるstdoutに書き込むことを示すことを約束しましたsys.stdout。だから、ここにいくつかのコードがあります:

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

実行すると:

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

start show2
hello

オブジェクトにはないstdout=sys.stdoutので、追加すると最初のルーチンは失敗することに注意してください。2番目は、にリダイレクトされているため、追加した場合は省略されます。StringIOfilenohellostdout=sys.stdoutsys.stdoutos.devnull

(Pythonのfile-descriptor-1 をリダイレクトすると、サブプロセスそのリダイレクトに従います。open(os.devnull, 'w')呼び出しにより、fileno()2より大きいストリームが生成されます。)


うーん。あなたのデモは結局、主張の反対を示しているようです。Pythonのstdoutをバッファーにリダイレクトしますが、サブプロセスstdoutはまだコンソールに行きます。それはどのように役立ちますか?何か不足していますか?
ガイサートン2013

@GuySirton:デモでは、サブプロセスstdoutが(明示的にに指示されていない場合sys.stdout)、Python プログラムの()stdout ではなく、Pythonの stdoutに移動することが示されています。私が認めるものは...奇妙な違いです。これを表現するより良い方法はありますか?sys.
torek 2013

知っておくと便利ですが、ここでサブプロセスの出力をキャプチャしたいので、sys.stdoutを変更するのはかっこいいですが、役に立たないと思います。通信は、select()、poll、threadsなどを使用している必要があります。
ガイサートン2013


select()を使用して実装を追加しました
sivann

20

readline()でiterコンストラクトを使用する代わりに、stdoutを読み取るためにデフォルトのファイル反復子を使用することもできます。

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)

ここで最もエレガントな答え!
Nir

9
このソリューションはリアルタイムでは表示されません。プロセスが完了するまで待機し、すべての出力を一度に表示します。Viktor Kerkezのソリューションでは、「your_command」が段階的に表示された場合、「your_command」が時々stdoutをフラッシュする限り(パイプのため)、出力は段階的に続きます。
エリックH.

1
@Nirライブではないため。
melMass

このソリューションはデフォルトの記述子で反復するため、出力で行が更新されたときにのみ更新されます。文字ベースの更新の場合、Viktorのソリューションに示されているように、read()メソッドで反復する必要があります。しかし、それは私のユースケースではやり過ぎでした。
Jughead

11

サードパーティのライブラリを使用できる場合は、次のようなものを使用できる場合がありますsarge(開示:私はそのメンテナーです)。このライブラリは、サブプロセスからの出力ストリームへのノンブロッキングアクセスを可能にします- subprocessモジュールの上に階層化されます。


sargeのすばらしい作品です。これは確かにOPの要件を解決しますが、そのユースケースでは少し重いかもしれません。
deepelement 2017年

ツールを提案する場合は、少なくともこの正確なケースの使用例を示してください。
Serhiy

4

解決策1:ログインstdoutstderr同時にリアルタイムで

stdoutとstderrの両方を同時に1行ずつリアルタイムでログファイルに記録するシンプルなソリューション。

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

解決策2:read_popen_pipes()両方のパイプ(stdout / stderr)を同時にリアルタイムで反復できるようにする関数

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

3

良いが「重い」ソリューションはTwistedを使用することです-下部を参照してください。

stdoutのみを使用して生活したい場合は、これらの行に沿って何かが機能するはずです。

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

(read()を使用すると、役に立たない「ファイル」全体を読み取ろうとしますが、ここで実際に使用できるのは、現在パイプ内にあるすべてのデータを読み取るものです)

スレッドでこれにアプローチしようとするかもしれません、例えば:

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

これで、2つのスレッドを持つことでstderrも追加できるようになりました。

注しかし、サブプロセスのドキュメントは、これらのファイルを直接使用して阻止して使用することを推奨していますcommunicate()(主に私は上記の問題はないと思うデッドロックに関係)と同様に、それは本当にそうですので、解決策は少しklunkyあるsubprocessモジュールはかなりまでではありません仕事http://www.python.org/dev/peps/pep-3145/も参照)と他のことを調べる必要があります。

より複雑なソリューションは、次のようにTwistedを使用することです:https : //twistedmatrix.com/documents/11.1.0/core/howto/process.html

Twistedでこれを行う方法は、を使用してプロセスを作成し、それを出力として非同期に処理reactor.spawnprocess()するProcessProtocolことです。TwistedサンプルPythonコードはこちらです:https : //twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py


ありがとう!私はちょうどこのようなものを試しました(@PauloAlmeidaのコメントに基づいていますが、subprocess.Popenへの呼び出しはブロックしています-つまり、whileループが返されるとそれだけになります...
DilithiumMatrix

1
それは何が起こっているのかではありません。それはすぐにwhileループに入り、read()サブプロセスが終了して親プロセスEOFがパイプで受信するまで呼び出しをブロックしています。
アルプ

@Alpおもしろい!そうです。
DilithiumMatrix 2013

ええ、私はこれを投稿するには速すぎました。実際には正しく機能せず、簡単に修正できません。図面テーブルに戻ります。
ガイサートン2013

1
@zhermes:したがって、read()の問題は、EOFまで出力全体を読み取ろうとするため、役に立たないということです。readline()が役立ちます。必要なのはそれだけです(実際には長い行も問題になる可能性があります)。また、起動するプロセスのバッファリングにも注意する必要があります...
ガイサートン2013

3

これらすべての答えに加えて、1つの簡単なアプローチは次のようにすることもできます。

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

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

    if not line:
        break

    print(line.strip())

読み取り可能である限り、読み取り可能なストリームをループし、結果が空の場合は停止します。

ここで重要なのは、出力がある限りreadline()行を(\n最後に)返し、それが実際に最後にある場合は空にすることです。

これが誰かを助けることを願っています。


3

上記すべてに基づいて、少し変更したバージョン(python3)をお勧めします。

  • whileループ呼び出しreadline(提案されたiterソリューションは私にとって永遠にブロックされるように見えました-Python 3、Windows 7)
  • 構造化されているため、ポーリングがnot-を返した後、読み取りデータの処理を複製する必要はありません。None
  • stderrがstdoutにパイプされ、両方の出力出力が読み取られる
  • cmdの終了値を取得するコードを追加しました。

コード:

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

returncode私の場合、その部分は非常に重要でした。
スターダスト

2

ラインバッファリングされた出力が機能するように見えます。その場合、次のようなものが適しています。(注意:テストされていません。)これは、サブプロセスのstdoutをリアルタイムでのみ提供します。stderrとstdoutの両方をリアルタイムで使用したい場合は、でより複雑な処理を行う必要があります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...

2

stdout直接設定しないのはなぜsys.stdoutですか?また、ログに出力する必要がある場合は、fのwriteメソッドをオーバーライドできます。

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)

これは機能しません。サブプロセスモジュールは、stdoutファイル記述子をフォークして、渡されたファイルオブジェクトのファイル記述子に設定します。書き込みメソッドが呼び出されることは決してありません(少なくとも、それがstderrのサブプロセスで実行されていることです。標準出力でも同じです)。
t.animal 2017年

2

上記のすべての解決策は、stderrとstdoutの出力の分離(複数のパイプ)に失敗するか、OSパイプバッファーがいっぱいのときに永久にブロックされ、コマンドを実行しているコマンドの出力が速すぎる場合に発生します(Pythonでこれに関する警告が表示されます) poll()サブプロセスのマニュアル)。私が見つけた唯一の信頼できる方法は、selectによるものでしたが、これは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()

別の方法は、パイプごとに1つのスレッドをスピンオフすることです。各スレッドは、他のスレッドをブロックすることなく、パイプでブロックI / Oを実行できます。しかし、これには一連の問題が伴います。すべての方法には不快な点があります。最も不快に感じる方法を選択するだけです。:-)
torek 2017年

2

以前の回答と同様ですが、Python3を使用するWindowsで次の解決策が私のために機能し、リアルタイムで印刷およびログインする一般的な方法を提供しました(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()

2

このsubprocess.communicate方法は少し誤解を招くと思います。実際には、で指定したstdoutstderrを埋めますsubprocess.Popen

ただし、のstdoutおよびstderrパラメーターにsubprocess.PIPE提供できるものから読み取ると、最終的にOSパイプバッファーがいっぱいになり、アプリがデッドロックになります(特に、使用する必要がある複数のプロセス/スレッドがある場合)。subprocess.Popensubprocess

私が提案するソリューションは、stdoutstderrにファイルを提供し、デッドロックから読み取る代わりにファイルのコンテンツを読み取ることですPIPE。これらのファイルは、tempfile.NamedTemporaryFile()-によって書き込まれている間、読み取りのためにアクセスすることもできますsubprocess.communicate

以下は使用例です。

        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

そして、これは、それが何をするかを説明するために私が提供できる限り多くのコメントで使用する準備できているソースコードです:

python 2を使用している場合は、pypiからsubprocess32パッケージの最新バージョンを最初にインストールしてください。


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



1

これは、私のプロジェクトの1つで使用しているクラスです。サブプロセスの出力をログにリダイレクトします。最初は書き込みメソッドを単に上書きしようとしましたが、サブプロセスがそれを呼び出さないため、機能しません(リダイレクトはファイル記述子レベルで発生します)。だから私は自分のパイプを使っています、それはサブプロセスモジュールでそれが行われる方法に似ています。これには、すべてのロギング/印刷ロジックをアダプターにカプセル化するという利点があり、ロガーのインスタンスを単にに渡すことができますPopensubprocess.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())

ログを必要とせず、単に使用したいprint()場合は、コードの大部分を削除してクラスを短くすることができます。__enter__and __exit__メソッドで展開して呼び出しfinished__exit__コンテキストとして簡単に使用することもできます。


1

Pythonicソリューションはどれも私にとってはうまくいきませんでした。proc.stdout.read()または同様のものが永久にブロックされる可能性があることが判明しました。

したがって、私はteeこのように使用します:

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

このソリューションは、すでにを使用している場合に便利ですshell=True

${PIPESTATUS}コマンドチェーン全体の成功ステータスをキャプチャします(Bashでのみ使用可能)。を省略した場合&& exit ${PIPESTATUS}tee失敗しないため、これは常にゼロを返します。

unbuffer「パイプバッファ」がいっぱいになるまで待ちすぎるのではなく、各行を端末にすぐに印刷するために必要になる場合があります。ただし、アンバッファーはアサートの終了ステータスを飲み込みます(SIG Abort)...

2>&1 stderrorもファイルに記録します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.