初めに
この記事ではタイトルにある通り,subprocessを終了しない状態の出力を最後まで取得する方法を説明します.ただ,これを解決するまでの紆余曲折を時系列風にまとめています.結論だけ見たい場合は,概要や,目次から結論の部分まで飛んでください.
前提
私は現在,discord botからminecraft serverをsubprocessのPopenで起動させて操作しています.そして,minecraft serverに対してコマンド(help等)を入力するとそれに応じた結果を返してくれます.botからこのコマンドを入力したいな~と思っています.
発生した問題
コマンドを入力したいな~と思っているのですが,出力の受け取りがどうもうまくいかない.
出力の受け取りにはPopen.stdout.readline()
を利用しているのですが,どうも読み取れる出力がない場合待機してしまうようです.待機してしまうとプログラムを終了させるしかないです.非同期処理ではないですし,非同期処理にしたとしても一連の流れは同期してますのでその部分が停止してしまうため,どちらにせよ終了するしかありません.
また,出力が存在しているかどうかを人間が想定するのも無理がある気がする.
communicate()
は複数回呼び出せない.
どうしよう.
しかも,まだ読み込めていない出力がある可能性があります.このとき,何も考えず読み込むと余計なものも読み込んでしまいます.読み込む行数がわかればなんとかなりますが,想定は難しいでしょう.
どうしよう.
試したこと
stdout.read()
出力の全てをファイルの最後まで取得します.出力の最後はEOFではないので待機します.
次!
stdout.readline(1)
stdout.readline()
は引数に数値を渡すことで,読み込む文字数を指定できます.これを改行まで読み込むとstdout.readline()
と同じような操作になるわけですね.
次の文字がb''
だったりすればうまくいきそうだと思いました.そもそも読み込むものがないんだってば.
次!
iter(proc.stdout.readline,b'')
これは,Pythonでデッドロックを回避しながらサブプロセスの標準出力を1行ずつ読み込むというサイトに書いてあった方法です.
もしかして出力読み込めないのデッドロックだったのか?と思って試してみました.だめでした
次!
どんな出力になるかを事前に知る
コマンドを入力した直後は確実に出力が存在します.よって,どんな出力になるかを事前に知れば,読み込んだものに特定の文字/文字列が入っているかどうかで終了の判断をすればよさそうです.試してみると,これはうまくいきます!やったぜ!
???「全部のコマンドに対してそうやるの?」
......
次......
次がない......
stdout
そもそもsubprocessのstdoutは何者なんだということを把握していないことに気が付きました.
ドキュメントを見てみると,open()
で開いたやつみたいです.stdinも同様です.
stdoutもstdinも実質同じ(?)ならstdoutにもflush()
があってそれが以前の出力をなくしてくれたりしないかな~と思って試してみたりしましたが違うもののようです.
python 標準出力のフラッシュ sys.stdout.flushという記事を見ると,バッファに存在するものを出力に書き込むやつみたいです.
なるほどね~.思ってたやつとは違ったな,残念.
[Python] 標準出力で上書きしたり追記したりするという記事も見つけました.
プログレスバーとかの表示は多分こうやってるんだろうな,なるほどね~
......
出力になにか書き込んで,それが読み込まれるまでループさせればいいんじゃね?
まさに天才の発想
試してみましょう.
buffer_end_bytes = b'Unknown'
self.process.stdout.write(buffer_end_bytes)
self.process.stdout.flush()
line = self.process.stdout.readline()
print(line)
while buffer_end_bytes not in line:
line = self.process.stdout.readline()
print(line)
Traceback (most recent call last):
(略)
io.UnsupportedOperation: write
ど゛う゛し゛て゛.vscodeの予測に出てきたのみだめなのか
ドキュメント「If the stdout argument was PIPE, this attribute is a readable stream object as returned by open().」
readonlyってこと?この記事を見る限りそうっぽい.
じゃあ,stdinでやるしかねえ
結論
minecraft serverに対して特定のコマンドを入力するとそれに対応した出力結果が返ってきます.それはコマンドによって,状況によって行数も内容も変わってきます.ただ,ある入力だけは,時刻以外は全て同じ出力を返してくれます.それは,コマンドではない入力です.papermcでは,
[01:22:57] [Server thread/INFO]: Unknown command. Type "/help" for help.
といった出力結果が常に返ってきます.これを利用します.今後はダミー入力と呼びます.
まず,得たいコマンドを入力する前に,読み込まれていない出力を捨てます.そのために,ダミー入力をして,Unknown
というbyte列が読み込まれるまで出力を読み込みます.
# 以前のログを取得することで捨てる
buffer_end_bytes = b'Unknown'
self.process.stdin.write(b'a\n')
self.process.stdin.flush()
line = self.process.stdout.readline()
while buffer_end_bytes not in line:
line = self.process.stdout.readline()
続いて,コマンドをserverに入力し結果を出力させ,再びダミー入力をしてUnknown
というbyte列が読み込まれるまで出力を読み込みます.コマンドの結果は複数行になる可能性があるためlistに格納します.また,出力結果の受け取りはstrがいいので,bytesからstrに変換しています.
print("write")
self.process.stdin.write(b"command")
self.process.stdin.flush()
print("damy")
self.process.stdin.write(b"a\n")
self.process.stdin.flush()
print("get log")
log_bytes: bytes = self.process.stdout.readline()
logs = [log_bytes.rstrip().decode()]
while buffer_end_bytes.decode() not in logs[-1]:
log_bytes: bytes = self.process.stdout.readline()
logs.append(log_bytes.rstrip().decode())
print(logs)
logs = logs[:-1]
print(logs)
コードの関係上,Unknown
が含まれている行も追加されるため,最後にそれを除去しています.
b"command"
部分にb"list"
を渡すと,
write
damy
get log
['[01:22:56 INFO]: There are 0 of a max of 20 players online:', '[01:22:56 INFO]: Unknown command. Type "/help" for help.']
['[01:22:56 INFO]: There are 0 of a max of 20 players online:']
無事に出力がとれました.
以上が,subprocessを終了しない状態の出力を最後まで取得する方法でした.
お疲れ様でした.