subprocessモジュールを使って外部コマンドを実行する際には、communicate
メソッドをよく使用していた。
import subprocess
(stdoutdata, stderrdata) = subprocess.Popen(['some', 'command'], stdout=subprocess.PIPE).communicate()
print(stdoutdata)
しかしビルドスクリプトなど、結果を非同期に出力したい場合はcommunicate
メソッドでは実現できない。ライブラリリファレンスにあるとおり、communicate
メソッドはプロセス終了まで待ち、その後に標準出力のデータを返すからだ。
この要求を実現したい場合、ioモジュールを使って子プロセスの出力に対応したストリームを作成する必要がある。
(ref) http://docs.python.jp/2/library/subprocess.html#subprocess.Popen.stdout
(ref) https://gist.github.com/mattbornski/3299031
def exec_process(commands):
process = subprocess.Popen(commands, stdout=subprocess.PIPE, bufsize=-1)
with io.open(process.stdout.fileno(), closefd=False) as stream:
[print(line.rstrip('\n')) for line in stream]
# プロセス終了まで待ち、結果を判定する
process.wait()
if process.returncode != 0:
print('Build process aborts.')
sys.exit(1)
以下、何点かハマった点のメモ
1.io.open
でストリームを生成する際には、ファイルパスまたはファイル記述子((file object).fileno()
で取得可能)を与える必要がある。subprocess.Popen.stdout
のパス名は分からないため、ファイル記述子を与える必要がある。ファイルオブジェクト(=subprocess.Popen.stdout
)をそのまま与えるのではないことに注意する。
(ref) http://docs.python.jp/2/library/io.html#io.open
# process.stdoutをio.openに直接与えると実行時例外で終了する。
def exec_process(commands):
process = subprocess.Popen(commands, stdout=subprocess.PIPE, bufsize=-1)
with io.open(process.stdout, closefd=False) as stream:
[print(line.rstrip('\n')) for line in stream]
2.io.open
でストリームを生成する際、closefd=False
を指定する必要がある。デフォルトではclosefd=True
であり、その状態だと以下のエラーが発生する。
close failed in file object destructor:
IOError: [Errno 9] Bad file descriptor
streamが閉じられるタイミング(=今回の場合、with文を抜ける時)で、子プロセスの掴んでいるファイルオブジェクト(=subprocess.Popen.stdout
)を閉じようとすることが原因だと考えられる。