pythonのsubprocessによるバックグラウンド実行

# 前提
この記事は，discord.pyを使ったdiscord botからminecraft serverを制御することが目標のアドベントカレンダーの記事です．
そのため，pythonプログラム内からコマンドを使ってminecraft serverを動かします．そのための方法としてsubprocessのPopenを利用しますので，このPopenを説明します．

# subprocess

subprocessは簡単に言えば，プログラムの中からコマンド(やプログラム)を実行するためのものです．全体的な使い方は以下の記事を参考すると良いと思います．
- [subprocessの使い方(Python3.6)](https://qiita.com/caprest/items/0245a16825789b0263ad)
    - 私の環境ではPython 3.10.6ですが，同じように使えます
- [subprocessについてより深く（3系，更新版）](https://qiita.com/HidKamiya/items/e192a55371a2961ca8a4#poll%E3%81%AB%E3%82%88%E3%82%8B%E7%8A%B6%E6%85%8B%E7%A2%BA%E8%AA%8D%E3%81%A8%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E5%87%BA%E5%8A%9B%E7%AE%A1%E7%90%86)

ここではdiscord botとminecraft serverを並行で起動したいため，`Popen`を使用します．

# subprocess.Popen

`Popen`で起動したプロセスは並行に実行されます．つまり，`Popen`で起動したものが終了しているかどうかに関わらず，Popenの後に記述されたコードは実行されます．
具体的なプログラムで実験いきましょう．

## 並行の確認実験

まず，以下のようなshellscriptを用意しました．
```bash: sleep_script.sh
#!/bin/bash

echo "start sleep"
sleep 5
echo "wake up"
```
これ使ってを`Popen`による実行と`run`による実行を比較していきます．
```python: test_subprocess.py
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`というファイルに実行結果を出力しています．上のプログラムを実行した結果が以下の通りです．

```text: 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の実験
実験用のコードは以下にまとめます．

<details><summary>実験用コード</summary>

```python: wait_input.py
import time

string1 = input()
print(f"1: {string1}")

print("sleep")
time.sleep(3)
print("wake up")

string2 = input()
print(f"2: {string2}")

```

```python: test_com.py
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)

```

</details>

- `wait_input.py`
    - 入力を受け取ってそれを出力後3秒待機し再び入力を受け取って出力する
- `test_com.py`
    - `wait_input.py`をsubprocessで実行し，communicateを二度実行する

という感じのコードです．実行してみた結果は以下の通りです．
```text
$ 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つのエラーが起きていますね．

1. EOFError: EOF when reading a line
このエラーは，入力が来たという情報をもらったのに，入力がないよというときに出るエラーのようです．[この記事](https://gonimaru.com/python%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E3%80%8Ceoferror-eof-when-reading-a-line%E3%80%8D/)[^err_EOF]
1. ValueError: Cannot send input after starting communication
これは英文の通りですね．communicateを一度行っているため，その後に再び入力を渡すことができません．

このようにcommunicateによる標準入力は複数回行うことができません．ではどうするかというと，subprocessのstdinのバッファに対して直接書き込みます．入力をしてプログラムを終了する場合はcommunicateでよいだめ，2番目の入力部分では引き続きcommunicateを使います．
1番目の入力でどうするかというと，以下の通りです．

```diff_python: 
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`と同じものです．

このように変更して実行した結果が以下の通りです．
```bash
$ python3 test_com.py 
input

out
1: write buffer
sleep
wake up
2: second

err
```

無事に複数回の入力を送ることができました．

## subprocessの終了の確認と待機

subprocessが終了しているかどうかを確認するには`poll()`を使います．また，終了を待機するには `wait()`を使用します．subprocessの稼働状況を確認してリアルタイムで出力を確認したり，終了の待機をしてsubprocessが稼働していない状態でしたい処理を書いたりするときに使用すると思います．

これも試してみましょう．以下のようなコードを用意しました．
```python: test_finish.py
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())
```

実行結果は以下の通りです．
```bash
$ 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](https://docs.python.org/ja/3/library/subprocess.html#subprocess.Popen.returncode)を返すため，終了している場合は`None`以外の値が入ります．
`wait()はそのままですね，終了を待機します．

これでpythonのsubprocessによるバックグラウンド実行の基本はできたと思います．あとは[ドキュメント](https://docs.python.org/ja/3/library/subprocess.html#)を読んだり，特定のものを検索したりするのがいいと思います．
お疲れ様でした．

[^err_EOF]: [pythonのエラー「EOFError: EOF when reading a line」](https://gonimaru.com/python%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E3%80%8Ceoferror-eof-when-reading-a-line%E3%80%8D/)