前提
この記事は,discord.pyを使ったdiscord botからminecraft serverを制御することが目標のアドベントカレンダーの記事です.
そのため,pythonプログラム内からコマンドを使ってminecraft serverを動かします.そのための方法としてsubprocessのPopenを利用しますので,このPopenを説明します.
subprocess
subprocessは簡単に言えば,プログラムの中からコマンド(やプログラム)を実行するためのものです.全体的な使い方は以下の記事を参考すると良いと思います.
-
subprocessの使い方(Python3.6)
- 私の環境ではPython 3.10.6ですが,同じように使えます
- subprocessについてより深く(3系,更新版)
ここではdiscord botとminecraft serverを並行で起動したいため,Popen
を使用します.
subprocess.Popen
Popen
で起動したプロセスは並行に実行されます.つまり,Popen
で起動したものが終了しているかどうかに関わらず,Popenの後に記述されたコードは実行されます.
具体的なプログラムで実験いきましょう.
並行の確認実験
まず,以下のようなshellscriptを用意しました.
#!/bin/bash
echo "start sleep"
sleep 5
echo "wake up"
これ使ってをPopen
による実行とrun
による実行を比較していきます.
import subprocess
script = "./sleep_script.sh"
with open("log.txt", "wb") as f:
f.write(b"(hello)\n\n")
with open("log.txt", "ab") as f:
f.write(b"start subprocess run\n")
process_run = subprocess.run(script, stdout=f)
f.write(b"hi. subprocess run\n\n")
f.write(b"start subprocess popen\n")
process_popen = subprocess.Popen(script, stdout=f)
f.write(b"hi. subprocess popen\n")
log.txt
というファイルに実行結果を出力しています.上のプログラムを実行した結果が以下の通りです.
(hello)
start sleep
wake up
start subprocess run
hi. subprocess run
start subprocess popen
hi. subprocess popen
start sleep
wake up
違いがわかると思います.ですが,その,私が思ってた順序とは違いますね......なんでかは正直わかりません......申し訳ありません.
想定していた実行結果は
(hello)
start sleep
wake up
start subprocess run
hi. subprocess run
start subprocess popen
hi. subprocess popen
start sleep
wake up
です.少なくともどちらもsubprocessを実行する前にstartの書き込みをしているはずなのですが......
ともかく,見てほしい違いはsubprocessによる出力行とtest_subprocess.pyでの出力行です.
run
の方ではscriptの最後の出力wake up
の後にsubprocess実行の後の行であるhi ~
の出力が,一方popen
の方ではhi ~
の出力がwake up
よりも先に出力されています.つまり,subprocessが終了せずともメインのプログラムの方の処理が続けられていることになります.並行処理が確認できた,と思います.
subprocessに対しての標準入力
実行したsubprocessに対して標準入力するための準備として,引数のstdin
にsubprocess.PIPE
を指定します.また,入力した結果を値として返してもらうには,stdout
やstderr
にもsubprocess.PIPE
を指定します.
そして,標準入力するにはをPopen.communicate
使います.ただし,Popen.communicate
は一度しか使えません.実験してみましょう.
communicateの実験
実験用のコードは以下にまとめます.
実験用コード
import time
string1 = input()
print(f"1: {string1}")
print("sleep")
time.sleep(3)
print("wake up")
string2 = input()
print(f"2: {string2}")
import subprocess
process = subprocess.Popen(
["python3", "wait_input.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
def printOutErrs(outs, errs):
print("\nout")
print(outs.rstrip().decode())
print("\nerr")
print(errs.rstrip().decode())
try:
print("input")
outs, errs = process.communicate(b"first", timeout=5)
printOutErrs(outs, errs)
except subprocess.TimeoutExpired:
print("timeout")
process.kill()
outs, errs = process.communicate()
printOutErrs(outs, errs)
try:
print("input")
outs, errs = process.communicate(b"second", timeout=5)
printOutErrs(outs, errs)
except subprocess.TimeoutExpired:
print("timeout")
process.kill()
outs, errs = process.communicate()
printOutErrs(outs, errs)
-
wait_input.py
- 入力を受け取ってそれを出力後3秒待機し再び入力を受け取って出力する
-
test_com.py
-
wait_input.py
をsubprocessで実行し,communicateを二度実行する
-
という感じのコードです.実行してみた結果は以下の通りです.
$ python3 test_com.py
input
out
1: first
sleep
wake up
err
Traceback (most recent call last):
File "/home/iharuki/test/popen/wait_input.py", line 10, in <module>
string2 = input()
EOFError: EOF when reading a line
status: 1
input
Traceback (most recent call last):
File "/home/iharuki/test/popen/test_com.py", line 37, in <module>
outs, errs = process.communicate(b"second", timeout=5)
File "/usr/lib/python3.10/subprocess.py", line 1127, in communicate
raise ValueError("Cannot send input after starting communication")
ValueError: Cannot send input after starting communication
2つのエラーが起きていますね.
- EOFError: EOF when reading a line
このエラーは,入力が来たという情報をもらったのに,入力がないよというときに出るエラーのようです.この記事1 - ValueError: Cannot send input after starting communication
これは英文の通りですね.communicateを一度行っているため,その後に再び入力を渡すことができません.
このようにcommunicateによる標準入力は複数回行うことができません.ではどうするかというと,subprocessのstdinのバッファに対して直接書き込みます.入力をしてプログラムを終了する場合はcommunicateでよいだめ,2番目の入力部分では引き続きcommunicateを使います.
1番目の入力でどうするかというと,以下の通りです.
import subprocess
process = subprocess.Popen(
["python3", "wait_input.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
def printOutErrs(outs, errs):
print("\nout")
print(outs.rstrip().decode())
print("\nerr")
print(errs.rstrip().decode())
+ process.stdin.write(b"write buffer\n")
+ process.stdin.flush()
- try:
- print("input")
- outs, errs = process.communicate(b"first")
- printOutErrs(outs, errs)
- except subprocess.TimeoutExpired:
- print("timeout")
- process.kill()
- outs, errs = process.communicate()
- printOutErrs(outs, errs)
try:
print("input")
outs, errs = process.communicate(b"second")
printOutErrs(outs, errs)
except subprocess.TimeoutExpired:
print("timeout")
process.kill()
outs, errs = process.communicate()
printOutErrs(outs, errs)
ここで大事なことは,write
で改行\n
を入れることとflush
でバッファをstdinに書き込むことです.改行を入れることについては,自分が入力することを考えればよいです.入力の最後はENTERを押して確定しますので,それのために改行が必要です.flush
については,vimやnanoのエディタを使ったことがある人ならわかるのではないでしょうか.変更点の保存は,バッファを書き込むことでできます.それがflush
と同じものです.
このように変更して実行した結果が以下の通りです.
$ python3 test_com.py
input
out
1: write buffer
sleep
wake up
2: second
err
無事に複数回の入力を送ることができました.
subprocessの終了の確認と待機
subprocessが終了しているかどうかを確認するにはpoll()
を使います.また,終了を待機するには wait()
を使用します.subprocessの稼働状況を確認してリアルタイムで出力を確認したり,終了の待機をしてsubprocessが稼働していない状態でしたい処理を書いたりするときに使用すると思います.
これも試してみましょう.以下のようなコードを用意しました.
import subprocess
import time
process = subprocess.Popen("./sleep_script.sh", stdout=subprocess.PIPE)
print(f"status is: {process.poll()}")
while process.poll() is None:
print("waiting......")
time.sleep(1)
btext = process.stdout.readline()
if btext != b"":
print(btext.rstrip().decode())
print(f"finished. status is: {process.poll()}")
print("start")
process = subprocess.Popen("./sleep_script.sh", stdout=subprocess.PIPE)
print(f"status is: {process.poll()}")
process.wait()
print("end")
for btext in process.stdout.readlines():
print(btext.rstrip().decode())
実行結果は以下の通りです.
$ python3 test_finish.py
status is: None
waiting......
start sleep
waiting......
wake up
waiting......
finished. status is: 0
start
status is: None
end
start sleep
wake up
poll()
の返り値は,実行している状態であればNone
となっています.これはpoll()
が終了しているかどうかの確認としてどういう状態で終了したかのreturncodeを返すため,終了している場合はNone
以外の値が入ります.
`wait()はそのままですね,終了を待機します.
これでpythonのsubprocessによるバックグラウンド実行の基本はできたと思います.あとはドキュメントを読んだり,特定のものを検索したりするのがいいと思います.
お疲れ様でした.