杀死子进程后Shell挂起
我知道在StackOverflow上有很多类似的问题,比如这个和这个,还有其他一些,但这些似乎都不适合我现在的情况。而且我对subprocess.Popen()
是怎么回事也不太明白,这让我更困惑。
我想要实现的目标是:启动一个子进程(一个命令行的收音机播放器),这个播放器不仅能把数据输出到终端,还能接收输入——等一会儿——然后结束这个子进程——最后退出命令行。我现在在OSX 10.9上运行的是Python 2.7。
情况 1.
这个代码启动了收音机播放器(但只播放音频!),然后结束了这个进程,最后退出。
import subprocess
import time
p = subprocess.Popen(['/bin/bash', '-c', 'mplayer http://173.239.76.147:8090'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=False,
stderr=subprocess.STDOUT)
time.sleep(5)
p.kill()
情况 2.
这个代码启动了收音机播放器,能输出一些信息,比如电台名称、歌曲、比特率等等,还能接收输入。它结束了子进程,但却没有退出命令行,终端也变得无法使用,即使按了'Ctrl-C'也没用。
p = subprocess.Popen(['/bin/bash', '-c', 'mplayer http://173.239.76.147:8090'],
shell=False)
time.sleep(5)
p.kill()
有没有什么办法可以做到这一点?我甚至在考虑如果没有其他选择的话,是否可以为子进程打开一个从属终端(当然这也是我完全不懂的事情)。谢谢!
2 个回答
我想要实现的目标是:启动一个子进程(一个命令行的广播播放器),它可以在终端输出数据,同时也能接收输入——等一会儿——结束这个子进程——退出命令行。我在OSX 10.9上运行Python 2.7。
在我的系统上,mplayer
可以接受键盘命令,比如用q
来停止播放并退出:
#!/usr/bin/env python
import shlex
import time
from subprocess import Popen, PIPE
cmd = shlex.split("mplayer http://www.swissradio.ch/streams/6034.m3u")
p = Popen(cmd, stdin=PIPE)
time.sleep(5)
p.communicate(b'q')
这个命令会启动mplayer
,调到公共领域的古典音乐;等5秒;然后让mplayer
退出,并等待它完成退出。输出会显示在终端上(也就是Python脚本输出的地方)。
我还尝试过p.kill()
、p.terminate()
和p.send_signal(signal.SIGINT)
(也就是按Ctrl + C)。使用p.kill()
会让人觉得进程卡住了。可能的原因是:p.kill()
会留下某些管道没有关闭,比如如果stdout=PIPE
,那么你的Python脚本可能会在p.stdout.read()
这里卡住,也就是说它杀掉了父进程mplayer
,但可能还有一个子进程在占用这些管道。使用p.terminate()
和p.send_signal(signal.SIGINT)
就不会有这种情况——mplayer
会正常退出。我尝试的这些方法都不需要用到reset
。
我该怎么做才能同时接收来自Python和键盘的输入?我需要两个不同的子进程吗?如何将键盘输入重定向到PIPE?
其实只要去掉stdin=PIPE
,然后调用p.terminate(); p.wait()
,就比用p.communicate(b'q')
简单多了。
如果你想保留stdin=PIPE
,那么一般的原则是:从sys.stdin
读取数据,写入到p.stdin
,直到超时。因为mplayer
只需要单个字母的命令,所以你需要能够一次从sys.stdin
读取一个字符。写入的部分很简单:p.stdin.write(c)
(设置bufsize=0
以避免在Python端的缓冲。mplayer
不对它的stdin进行缓冲,所以你不需要担心这个)。
你不需要两个不同的子进程。要实现timeout
,你可以使用threading.Timer(5, p.stdin.write, [b'q']).start()
,或者在sys.stdin
上使用select.select
来设置超时。
我想知道使用老式的
raw_input
是否有关系?
raw_input()
不适合mplayer
,因为它是读取整行的,而mplayer
只需要一次一个字符的输入。
看起来 mplayer
是用 curses
这个库来工作的,当你用 kill()
或 terminate()
来结束它时,出于某种原因,它没有正确清理这个库的状态。
要恢复终端的状态,你可以使用 reset
命令。
演示:
import subprocess, time
p = subprocess.Popen(['mplayer', 'http://173.239.76.147:8090'])
time.sleep(5)
p.terminate()
p.wait() # important!
subprocess.Popen(['reset']).wait()
print('Hello, World!')
原则上,你也可以使用 stty sane
,但对我来说效果不太好。
正如 Sebastian 指出的那样,上面的代码缺少了一个 wait()
调用(现在已经加上了)。有了这个 wait()
调用 并且 使用 terminate()
,终端就不会出现混乱(所以就不需要使用 reset
了)。
如果没有 wait()
,我有时会遇到 Python 进程和 mplayer
输出混合的问题。
另外,Sebastian 还指出了一个针对 mplayer
的解决方案,就是向 mplayer
的标准输入发送一个 q
来退出它。
我保留了使用 reset
的代码,因为它适用于 任何 使用 curses
库的程序,无论它是否正确关闭这个库,因此在其他情况下如果无法干净退出时可能会很有用。