7

我正在尝试编写一个能够与其他程序交互的 python 程序。这意味着发送标准输入和接收标准输出数据。我不能使用 pexpect(尽管它确实启发了一些设计)。我现在使用的过程是这样的:

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

我一直在对一些代码(如下)进行原型设计,这些代码有效,但似乎有一个让我烦恼的缺陷。子进程完成后,如果我在使用select.select. 我真的不想设置超时。只是看起来有点脏。但是,我试图解决这个问题的所有其他方法似乎都不起作用。Pexpect 似乎通过使用os.execvandpty.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 不被缓冲。

4

4 回答 4

12

首先,os.read确实阻塞,与您所说的相反。但是,它不会在select. 同样os.read在一个关闭的文件描述符上总是返回一个空字符串,你可能想要检查它。

然而,真正的问题是主设备描述符永远不会关闭,因此最终select会阻塞。在罕见的竞争条件下,子进程已经退出selectprocess.poll()并且您的程序很好地退出。但是大多数时候选择会永远阻塞。

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

权衡一下在任何地方抛出随机异常而不是轮询,我认为超时select听起来并不是那么坏的主意。无论如何,您的进程仍然很难成为系统上唯一(缓慢)的轮询进程。

于 2012-09-01T06:10:54.423 回答
9

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

所以,只有一行:

os.close(slave)

..在通话后立即放置subprocess.Popen,应该可以解决您的问题。

但是,可能会有更好的答案,具体取决于您的要求。正如其他人指出的那样,您不需要 pty 只是为了避免缓冲。你可以使用一个 bareos.pipe()代替pty.openpty()(并且对待返回值完全一样)。裸 OS 管道永远不会缓冲;如果子进程没有缓冲其输出,那么您的select()os.read()调用也不会看到缓冲。不过,你仍然需要这os.close(slave)条线。

但是您可能出于不同的原因确实需要一个 pty。如果您的一些子程序希望在大部分时间以交互方式运行,那么他们可能会检查他们的标准输入是否是 pty 并且根据答案表现不同(许多常见的实用程序都这样做)。如果你真的想让孩子认为它有一个分配给它的终端,那么pty模块就是要走的路。根据您的运行方式runner.py,您可能需要从 using切换subprocesspty.fork()子进程对象的 preexec_fn 可调用部分)。

于 2012-08-31T00:32:35.480 回答
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避免轮询循环。

于 2012-06-23T01:19:02.677 回答
0

当您的子进程退出时 - 您的父进程会收到SIGCHLD信号。默认情况下,此信号被忽略,但您可以拦截它:

import sys
import signal

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

signal.signal(signal.SIGCHLD, handler)

该信号还应该将阻塞系统调用中断为“选择”或“读取”(或您所在的任何位置),并让您在处理程序函数中执行您必须执行的任何操作(清理、退出等)。

于 2012-08-29T20:01:08.983 回答