6

Popenを使用してサブプロセスを起動し、生成時にほぼリアルタイムで出力を消費するPythonプログラムがあります。関連するループのコードは次のとおりです。

def run(self, output_consumer):
    self.prepare_to_run()
    popen_args = self.get_popen_args()
    logging.debug("Calling popen with arguments %s" % popen_args)
    self.popen = subprocess.Popen(**popen_args)
    while True:
        outdata = self.popen.stdout.readline()
        if not outdata and self.popen.returncode is not None:
            # Terminate when we've read all the output and the returncode is set
            break
        output_consumer.process_output(outdata)
        self.popen.poll()  # updates returncode so we can exit the loop
    output_consumer.finish(self.popen.returncode)
    self.post_run()

def get_popen_args(self):
    return {
        'args': self.command,
        'shell': False, # Just being explicit for security's sake
        'bufsize': 0,   # More likely to see what's being printed as it happens
                        # Not guarantted since the process itself might buffer its output
                        # run `python -u` to unbuffer output of a python processes
        'cwd': self.get_cwd(),
        'env': self.get_environment(),
        'stdout': subprocess.PIPE,
        'stderr': subprocess.STDOUT,
        'close_fds': True,  # Doesn't seem to matter
    }

これは本番マシンではうまく機能しますが、開発マシンでは、.readline()特定のサブプロセスが完了すると呼び出しがハングします。つまり、「処理が完了しました」という最終出力行を含むすべての出力を正常に処理しますが、再度ポーリングreadlineして戻ることはありません。このメソッドは、私が呼び出すほとんどのサブプロセスの開発マシンで適切に終了しますが、それ自体が多くのサブプロセスを呼び出す1つの複雑なbashスクリプトでは常に終了できません。

出力が終了する何行も前に、 (通常は)値以外の値popen.returncodeに設定されることに注意してください。したがって、それが設定されているときにループから抜け出すことはできません。そうしないと、プロセスの最後に吐き出され、読み取りを待ってバッファリングされているすべてのものが失われます。問題は、その時点でバッファをフラッシュしているときに、最後の呼び出しがハングするため、いつ終了したかがわからないことです。呼び出しもハングします。電話をかけると、最後のすべてのキャラクターが出てきますが、最後の行の後でハングします。 常にです。私が最後にいることをどのように知ることができますか?None0readline()read()read(1)popen.stdout.closedFalse

すべてのシステムは、Ubuntu12.04LTSでpython2.7.3を実行しています。FWIWは、を使用しstderrてマージされています。stdoutstderr=subprocess.STDOUT

なぜ違いがあるのですか?stdoutなんらかの理由で閉まりませんか?サブサブプロセスは、それを何らかの形で開いたままにするために何かを行うことができますか?開発ボックスのターミナルからプロセスを起動しているのに、本番環境ではデーモンとして起動されているためsupervisordでしょうか?それはパイプの処理方法を変更しますか?もしそうなら、どのようにそれらを正規化しますか?

4

6 に答える 6

3

メイン コード ループは正しく見えます。別のプロセスがパイプを開いたままにしているために、パイプが閉じていない可能性があります。たとえば、スクリプトが書き込みを行うバックグラウンド プロセスを起動するとstdout、パイプは閉じません。他の子プロセスがまだ実行されていませんか?

アイデアは、が設定されているのを確認したらモードを変更することです.returncode。メイン プロセスが完了したことを確認したら、バッファからすべての出力を読み取りますが、待機状態にならないようにしてください。selectを使用して、タイムアウト付きでパイプから読み取ることができます。数秒のタイムアウトを設定すると、待機中の子プロセスにスタックすることなくバッファをクリアできます。

于 2013-04-25T22:57:06.773 に答える
2

問題の原因となっている「1 つの複雑な bash スクリプト」の内容を知らなければ、正確な原因を特定することはできません。

ただし、Python スクリプトを で実行すると機能すると主張しているという事実に焦点を当てるとsupervisord、サブプロセスが stdin から読み取ろうとしている場合、または stdin が tty である場合に動作が異なる場合にスタックする可能性があります。推定)supervisordからリダイレクトされ/dev/nullます。

test.shこの最小限の例は、私の例が標準入力から読み取ろうとするサブプロセスを実行する場合にうまく対処しているようです...

import os
import subprocess

f = subprocess.Popen(args='./test.sh',
                     shell=False,
                     bufsize=0,
                     stdin=open(os.devnull, 'rb'),
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT,
                     close_fds=True)

while 1:
    s = f.stdout.readline()
    if not s and f.returncode is not None:
        break
    print s.strip()
    f.poll()
print "done %d" % f.returncode

それ以外の場合は、いつでもノンブロッキング readを使用するようにフォールバックし、「プロセスが完了しました」という最終出力行が表示されたときに救済できますが、これは少しハックです。

于 2013-04-26T10:45:43.383 に答える
2

readline() または read() を使用すると、ハングすることはありません。returncode や poll() をチェックする必要はありません。プロセスが終了したことがわかっているときにハングしている場合は、他の人が前に言ったように、パイプを開いたままにしているサブプロセスである可能性が最も高いです。

これをデバッグするためにできることは 2 つあります: * 現在の複雑なスクリプトではなく、最小限のスクリプトで再現してみる、または * その複雑なスクリプトを実行して、strace -f -e clone,execve,exit_groupそのスクリプトが何を開始しているかを確認し、メイン スクリプトを生き残っているプロセスがあるかどうかを確認します。 (メイン スクリプトが exit_group を呼び出すタイミングを確認してください。その後 strace がまだ待機している場合は、子がまだ生きています)。

于 2013-04-26T22:19:39.777 に答える
1

read以前に を呼び出していたにもかかわらず、 (またはreadline) への呼び出しがハングすることがあることがわかりましたpoll。そこでselect、読み取り可能なデータがあるかどうかを確認するために電話をかけました。ただし、selectプロセスが閉じられた場合、タイムアウトがなければハングすることもあります。そのため、反復ごとにわずかなタイムアウトを設定して、セミビジー ループで select を呼び出します (以下を参照)。

これをreadlineに適応できるかどうかはわかりません。最終行\nがない場合、またはプロセスがstdinを閉じたり終了したりする前にstdoutを閉じない場合、readlineがハングする可能性があるためです。これをジェネレーターでラップすると\n、stdout_collected で a が発生するたびに、現在の行が得られます。

また、実際のコードでは、疑似端末 (pty) を使用して popen ハンドルをラップしています (ユーザー入力をより厳密に偽造するため) が、なくても機能するはずです。

# handle to read from
handle = self.popen.stdout

# how many seconds to wait without data
timeout = 1

begin = datetime.now()
stdout_collected = ""

while self.popen.poll() is None:
    try:
        fds = select.select([handle], [], [], 0.01)[0]
    except select.error, exc:
        print exc
        break

    if len(fds) == 0:
        # select timed out, no new data
        delta = (datetime.now() - begin).total_seconds()
        if delta > timeout:
            return stdout_collected

        # try longer
        continue
    else:
        # have data, timeout counter resets again
        begin = datetime.now()

    for fd in fds:
        if fd == handle:
            data = os.read(handle, 1024)
            # can handle the bytes as they come in here
            # self._handle_stdout(data)
            stdout_collected += data

# process exited
# if using a pseudoterminal, close the handles here
self.popen.wait()
于 2013-04-30T13:06:59.013 に答える
0

sdterr を STDOUT に設定するのはなぜですか?

サブプロセスで communicate() 呼び出しを行う本当の利点は、stdout 応答と stderr メッセージを含むタプルを取得できることです。

ロジックが成功または失敗に依存する場合、これらは役立つ場合があります。

また、行を反復しなければならないという苦痛からも解放されます。Communicate() はすべてを提供し、完全なメッセージが受信されたかどうかについて未解決の質問はありません。

于 2013-04-27T09:44:51.197 に答える