我正在尝试编写一个能够与其他程序交互的python程序。这意味着发送stdin并接收stdout数据。我不能使用pexpect(尽管它确实激发了一些设计)。我现在正在使用的流程是:
subprocess.poll
循环直到子进程退出
我一直在制作一些代码(下面)的原型,但它似乎有一个瑕疵让我烦恼。子进程完成后,如果在使用select.select
时未指定超时,则父进程将挂起。我真的不想设置超时。它看起来有点脏。但是,我尝试解决这个问题的所有其他方法似乎都不起作用。 Pexpect似乎通过使用os.execv
和pty.fork
代替subprocess.Popen
和pty.openpty
来解决这个问题,我不喜欢这个解决方案。我是如何检查子流程的生命周期的?我的方法不正确吗?
我正在使用的代码如下。我在Mac OS X 10.6.8上使用它,但我也需要它在Ubuntu 12.04上工作。
这是子流程转轮runner.py
:
import subprocess
import select
import pty
import os
import sys
def main():
master, slave = pty.openpty()
process = subprocess.Popen(['python', 'outputter.py'],
stdin=subprocess.PIPE,
stdout=slave, stderr=slave, close_fds=True)
while process.poll() is None:
# Just FYI timeout is the last argument to select.select
rlist, wlist, xlist = select.select([master], [], [])
for f in rlist:
output = os.read(f, 1000) # This is used because it doesn't block
sys.stdout.write(output)
sys.stdout.flush()
print "**ALL COMPLETED**"
if __name__ == '__main__':
main()
这是子流程代码outputter.py
。 奇怪的随机部分只是模拟以随机间隔输出数据的程序。如果您愿意,可以将其删除。应该没关系:
import time
import sys
import random
def main():
lines = ['hello', 'there', 'what', 'are', 'you', 'doing']
for line in lines:
sys.stdout.write(line + random.choice(['', '\n']))
sys.stdout.flush()
time.sleep(random.choice([1,2,3,4,5])/20.0)
sys.stdout.write("\ndone\n")
sys.stdout.flush()
if __name__ == '__main__':
main()
感谢您提供的所有帮助!
额外注意
使用pty是因为我想确保不缓冲stdout。
答案 0 :(得分:11)
首先,os.read
会阻止,与您声明的情况相反。但是,它不会在select
之后阻止。对于已关闭的文件描述符,os.read
也始终返回一个空字符串,您可能需要检查该字符串。
然而,真正的问题是主设备描述符永远不会关闭,因此最终select
是将阻止的设备描述符。在罕见的竞争条件下,子进程已退出select
和process.poll()
,您的程序退出很好。但大多数情况下,选择块永远存在。
如果按照izhak的建议安装信号处理程序,那么一切都会破裂;每当子进程终止时,都会运行信号处理程序。运行信号处理程序后,该线程中的原始系统调用无法继续,因此syscall调用返回非零errno,这通常会导致在python中抛出一些随机异常。现在,如果您的程序中的其他位置使用某个库以及任何不知道如何处理此类异常的阻塞系统调用,则会遇到大麻烦(例如,任何地方的任何os.read
现在都可以抛出异常,即使在成功的select
)。
称重在任何地方抛出随机异常以防止轮询,我不认为select
上的超时听起来不错。无论如何,你的过程仍然不是系统上唯一的(缓慢的)轮询过程。
答案 1 :(得分:8)
您可以更改许多内容以使代码正确无误。我能想到的最简单的事情就是在分叉后关闭父进程的slave fd副本,这样当子进程退出并关闭自己的slave fd时,父进程select.select()
会将主进程标记为可读取,随后的os.read()
将给出一个空结果,您的程序将完成。 (在从属fd的两个副本关闭之前,pty主站不会看到从属端关闭。)
所以,只需一行:
os.close(slave)
..在subprocess.Popen
电话后立即放置,应该解决您的问题。
但是,根据您的要求,可能会有更好的答案。正如其他人所说,你不需要pty只是为了避免缓冲。您可以使用裸os.pipe()
代替pty.openpty()
(并将返回值视为完全相同)。裸露的OS管道永远不会缓冲;如果子进程没有缓冲其输出,那么您的select()
和os.read()
调用也不会看到缓冲。不过,您仍然需要os.close(slave)
行。
但是你可能因为不同的原因需要一个pty。如果你的一些子程序希望在大多数时间以交互方式运行,那么他们可能会检查他们的stdin是否是一个pty并且根据答案行为不同(很多常见的实用程序都这样做)。如果你真的希望孩子认为它有一个为它分配的终端,那么pty
模块就是你要走的路。根据您的运行方式runner.py
,您可能需要从使用subprocess
切换到pty.fork()
,以便孩子设置会话ID并预先打开pty(或查看pty.py的源代码,看它是做什么的,并复制子进程对象的preexec_fn可调用的相应部分。
答案 2 :(得分:0)
据我了解,您不需要使用pty
。 runner.py
可以修改为
import subprocess
import sys
def main():
process = subprocess.Popen(['python', 'outputter.py'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while process.poll() is None:
output = process.stdout.readline()
sys.stdout.write(output)
sys.stdout.flush()
print "**ALL COMPLETED**"
if __name__ == '__main__':
main()
可以使用 process.stdout.read(1)
代替process.stdout.readline()
来实现子进程中每个字符的实时输出。
注意:如果您不需要子进程的实时输出,请使用Popen.communicate来避免轮询循环。
答案 3 :(得分:0)
当您的子流程退出时 - 您的父流程会收到SIGCHLD信号。默认情况下,此信号被忽略,但您可以拦截它:
import sys
import signal
def handler(signum, frame):
print 'Child has exited!'
sys.exit(0)
signal.signal(signal.SIGCHLD, handler)
该信号还应该打破阻塞系统调用以选择'或者'阅读' (或者你所处的任何东西)让你在处理函数中做任何你必须做的事情(清理,退出等)。