通过Python的subprocess模块进行后台执行
前提 tí)
这篇文章是一篇旨在通过 discord.py 的 discord bot 来控制 Minecraft 服务器的 Advent Calendar 文章。因此,我们将使用 Python 程序中的命令来操作 Minecraft 服务器。为此,我们将介绍 subprocess 的 Popen 方法。
子进程 (zǐ
subprocess 是为了从程序中执行命令(包括其他程序)而设计的工具。您可以参考以下文章以获得整体的使用方法。
subprocessの使い方(Python3.6)
私の環境ではPython 3.10.6ですが,同じように使えます
subprocessについてより深く(3系,更新版)
为了同时启动Discord机器人和Minecraft服务器,我们将使用Popen。
子进程.Popen
Popen启动的进程会并行执行。也就是说,不论Popen启动的进程是否已结束,之后写在Popen后面的代码都会执行。让我们通过具体的程序来进行实验吧。
同步验证实验
首先,我准备了以下这样的shell脚本。
#!/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
至少在执行任何一种子进程之前都应该先写入start,但是……
无论如何,我希望你能看到的区别是通过子进程输出的行与test_subprocess.py中的输出行。
在run方式中,hi〜的输出是在脚本最后的输出wake up之后的子进程执行后出现的,而在popen方式中,hi〜的输出早于wake up输出。换句话说,即使子进程没有结束,主程序的处理仍在继续进行。我认为这证实了并行处理。
对于subprocess的标准输入
为了准备向执行的子进程输入标准输入,我们将在参数的stdin中指定subprocess.PIPE。此外,为了将输入结果作为值返回,我们还需要在stdout和stderr中指定subprocess.PIPE。
然后,我们将使用Popen.communicate来进行标准输入。但是,请注意Popen.communicate只能使用一次。让我们进行一次实验。
交流的实验
这里将实验用的代码整理如下。
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
看来出现了两个错误。
-
- EOF错误:读取行时遇到EOF
-
- 这个错误似乎是在收到输入的信息后,却发现没有输入的情况下出现的错误。
数值错误:在启动通信后无法发送输入
正如英文中所述,由于已经执行了通信操作,因此无法再次传递输入。
在这种情况下,无法多次使用通信进行标准输入。那该怎么办呢?我们可以直接向子进程的stdin缓冲区中写入数据。如果需要输入并结束程序,则可以使用communicate,而在第二个输入部分,我们将继续使用communicate。
对于第一个输入,做法如下:
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
我成功地发送了多次输入。
确认和等待子进程的结束
为了检查子进程是否已经结束,可以使用poll()。另外,使用wait()来等待进程的结束。当我们需要实时监控subprocess的运行状态并且验证输出,或者在等待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
调查()的返回值,如果正在执行,则为None。这是为了确认poll()是否已经结束以及它以什么样的returncode结束,如果已结束,返回值将不是None而是其他值。
wait()保持原样,等待结束。
我认为这样就可以了解使用Python的subprocess进行后台执行的基本知识。接下来,建议阅读相关文档或进行特定搜索。辛苦了。