使用子带选择和捕捉PTY输出时挂起(Using subprocess with select and

2019-07-30 01:59发布

我试图写一个Python程序,能够与其它程序进行交互。 这意味着发送stdin和接收标准输出数据。 我不能使用Pexpect的(虽然它确实启发了一些设计的)。 我现在使用的过程是这样的:

  1. 附上PTY的子进程的stdout
  2. 循环直到通过检查子退出subprocess.poll
    • 当在标准输出可用的数据立即写数据到当前标准输出。
  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()

这是子代码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不是缓冲。

Answer 1:

首先, os.read确实块,相反,你的状态是什么。 但是,这并不阻止后select 。 此外os.read在一个封闭的文件描述符总是返回一个空字符串,你可能要检查。

然而,真正的问题是,在主设备描述符被从未关闭的,从而最终的select是一个将阻塞。 在罕见的竞争状态,子进程已经退出之间selectprocess.poll()和你的程序退出很好。 大多数时候却选择块,直到永远。

如果你安装了信号处理程序提出izhak大乱; 每当一个子进程终止时,信号处理程序运行。 信号处理程序运行后,在该线程原来的系统调用无法继续,这样的系统调用调用返回非零错误,其结果往往是在Python抛出一些随机的异常。 现在,如果其他地方的程序时使用一些库,任何阻塞的系统调用了不知道如何处理这样的例外,你是在一个大麻烦(任何os.read例如在任何地方现在可以抛出一个异常,即使成功select )。

权衡投票的任何地方抛出一个比特是随机的例外,我不认为在超时select听起来并不坏主意。 你的过程中仍然难以成为唯一的(慢)查询系统上的过程呢。



Answer 2:

有许多事情可以改变,以使你的代码是正确的。 我能想到的最简单的事情就是分叉后关闭从FD的父进程的副本,这样,当孩子退出并关闭自身的从FD,父母的select.select()将迎来主为可供阅读,和随后os.read()会给出一个空的结果和你的程序将完成。 (pty主不会看到从属端为被关闭,直到FD被关闭从属的两个副本。)

所以,只有一行:

os.close(slave)

..placed后立即subprocess.Popen通话,应该解决您的问题。

不过,也有可能是更好的答案,取决于你的需求是什么。 至于别人注意,你并不需要一个PTY只是为了避免缓冲。 你可以使用裸os.pipe()代替pty.openpty()和治疗的返回值完全一样)。 裸OS管决不会缓冲器; 如果子进程没有缓冲它的输出,那么你的select()os.read()调用将不会看到任何缓冲。 你仍然需要os.close(slave)线,虽然。

但是,它可能是你需要为不同的原因PTY。 如果一些孩子计划预计要运行交互式多的时间,那么他们可能会被检查,看看他们的标准输入是一个PTY和行为的不同取决于答案(大量的常用工具做到这一点)。 如果你真的希望孩子认为它有分配给它的一个终端,那么pty模块是要走的路。 根据您将如何运行runner.py ,您可能需要使用转subprocesspty.fork()从而使孩子都有会话ID集和PTY预先打开(或查看pty.py源看看它做什么和你的子对象的preexec_fn调用)复制相应的部分。



Answer 3:

据我了解,你不需要使用ptyrunner.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避免轮询循环。



Answer 4:

当您的孩子过程出来-你的父进程被SIGCHLD信号。 默认情况下,这个信号被忽略,但你可以拦截它:

import sys
import signal

def handler(signum, frame):
    print 'Child has exited!'
    sys.exit(0)

signal.signal(signal.SIGCHLD, handler)

该信号也应该打破封锁系统调用来“选择”或“读”(或任何你是),让你做任何你在处理函数必须(清理,​​退出等)。



文章来源: Using subprocess with select and pty hangs when capturing output