捕获输出时,使用带有select和pty的子进程挂起

rav*_*c95 6 python select subprocess pty

我正在尝试编写一个能够与其他程序交互的python程序.这意味着发送stdin并接收stdout数据.我不能使用pexpect(尽管它确实激发了一些设计).我现在正在使用的过程是这样的:

  1. 将pty附加到子进程的stdout
  2. 循环直到子进程通过检查退出 subprocess.poll
    • 当stdout中有可用数据时,该数据立即写入当前标准输出.
  3. 完!

我一直在制作一些代码(下面)的原型,但它似乎有一个瑕疵让我烦恼.子进程完成后,如果我在使用时未指定超时,则父进程将挂起select.select.我真的不想设置超时.它看起来有点脏.但是,我尝试解决这个问题的所有其他方法似乎都不起作用.Pexpect似乎通过使用os.execvpty.fork不是subprocess.Popenpty.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()
Run Code Online (Sandbox Code Playgroud)

这是子进程代码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()
Run Code Online (Sandbox Code Playgroud)

感谢您提供的任何帮助!

额外的说明

使用pty是因为我想确保stdout没有缓冲.

Ant*_*ala 11

首先,os.read确实阻止,与你声明的相反.但是,它不会阻止select.此外os.read,在关闭的文件描述符上,始终返回一个空字符串,您可能需要检查该字符串.

然而,真正的问题是主设备描述符永远不会被关闭,因此最终select将是阻塞的设备描述符.在罕见的竞争状态,子进程已经退出之间select以及process.poll()和你的程序退出很好.大多数时候,选择块永远存在.

如果按照izhak的建议安装信号处理程序,那么所有的地狱都会破裂; 每当子进程终止时,都会运行信号处理程序.运行信号处理程序后,该线程中的原始系统调用无法继续,因此syscall调用返回非零errno,这通常会导致在python中抛出一些随机异常.现在,如果您的程序中的其他位置使用某个库以及任何不知道如何处理此类异常的阻塞系统调用,那么您就会遇到大麻烦(os.read例如任何地方都可以抛出异常,即使成功之后也是如此select).

称重在任何地方抛出随机异常以防止轮询,我认为超时select不会听起来不错.无论如何,您的过程仍然很难成为系统上唯一(慢)的轮询过程.


the*_*aul 8

您可以更改许多内容以使代码正确无误.我能想到的最简单的事情就是在分叉之后关闭父进程的slave fd副本,这样当子进出并关闭自己的slave fd时,父进程select.select()会将master标记为可读,后续os.read()将给出一个空的结果,你的程序将完成.(在从属fd的两个副本都关闭之前,pty主站不会看到从属端关闭.)

所以,只需一行:

os.close(slave)
Run Code Online (Sandbox Code Playgroud)

电话后立即放置subprocess.Popen,应该解决你的问题.

但是,根据您的要求,可能会有更好的答案.正如其他人所说,你不需要pty只是为了避免缓冲.您可以使用裸os.pipe()代替pty.openpty()(并将返回值完全相同).裸露的OS管道永远不会缓冲; 如果子进程没有缓冲其输出,那么你select()os.read()调用也不会看到缓冲.但是你仍然需要这os.close(slave)条线.

但是你可能因为不同的原因需要一个pty.如果你的一些子程序希望在大多数时间以交互方式运行,那么他们可能会检查他们的stdin是否是一个pty并且根据答案行为不同(很多常见的实用程序都这样做).如果你真的希望孩子认为它有一个分配给它的终端,那么pty模块就是你要走的路.根据您的运行方式runner.py,您可能需要从使用切换subprocesspty.fork(),以便孩子设置其会话ID并预先打开pty(或者查看pty.py的来源以查看它的作用并复制相应的您的子进程对象的preexec_fn可调用的部分).