0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

itohalAdvent Calendar 2022

Day 8

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

Last updated at Posted at 2022-12-07

前提

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

subprocess

subprocessは簡単に言えば,プログラムの中からコマンド(やプログラム)を実行するためのものです.全体的な使い方は以下の記事を参考すると良いと思います.

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

subprocess.Popen

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

並行の確認実験

まず,以下のようなshellscriptを用意しました.

sleep_script.sh
#!/bin/bash

echo "start sleep"
sleep 5
echo "wake up"

これ使ってをPopenによる実行とrunによる実行を比較していきます.

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というファイルに実行結果を出力しています.上のプログラムを実行した結果が以下の通りです.

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に対して標準入力するための準備として,引数のstdinsubprocess.PIPEを指定します.また,入力した結果を値として返してもらうには,stdoutstderrにもsubprocess.PIPEを指定します.
そして,標準入力するにはをPopen.communicate使います.ただし,Popen.communicateは一度しか使えません.実験してみましょう.

communicateの実験

実験用のコードは以下にまとめます.

実験用コード
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}")

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)

  • 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つのエラーが起きていますね.

  1. EOFError: EOF when reading a line
    このエラーは,入力が来たという情報をもらったのに,入力がないよというときに出るエラーのようです.この記事1
  2. 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が稼働していない状態でしたい処理を書いたりするときに使用すると思います.

これも試してみましょう.以下のようなコードを用意しました.

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())

実行結果は以下の通りです.

$ 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によるバックグラウンド実行の基本はできたと思います.あとはドキュメントを読んだり,特定のものを検索したりするのがいいと思います.
お疲れ様でした.

  1. pythonのエラー「EOFError: EOF when reading a line」

0
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?