检测子进程何时等待输入

时间:2013-08-07 15:30:51

标签: python linux subprocess pty unbuffered-output

我正在编写一个Python程序,用于在Linux服务器上运行用户上传的任意(因此,在最坏的情况下,不安全,错误和崩溃)代码。除了安全问题之外,我的目标是确定代码(可能是任何语言,编译或解释)是否将正确的内容写入stdoutstderr和其他给定输入的文件该计划的stdin。在此之后,我需要向用户显示结果。

当前的解决方案

目前,我的解决方案是使用subprocess.Popen(...)生成子进程,并为stdoutstderrstdin提供文件句柄。 stdin句柄后面的文件包含程序在操作期间读取的输入,在程序终止后,将读取stdoutstderr文件并检查其是否正确。

问题

这种方法非常完美,但是当我显示结果时,我无法组合给定的输入和输出,因此输入将出现在与从终端运行程序时相同的位置。即对于像

这样的程序
print "Hello."
name = raw_input("Type your name: ")
print "Nice to meet you, %s!" % (name)

包含程序stdout的文件的内容在运行后将是:

Hello.
Type your name: 
Nice to meet you, Anonymous!

鉴于包含stdin的文件的内容为Anonymous<LF>。所以,简而言之,对于给定的示例代码(以及等效的任何其他代码),我希望得到如下结果:

Hello.
Type your name: Anonymous
Nice to meet you, Anonymous!

因此,问题是检测程序何时等待输入。

尝试过的方法

我尝试了以下方法来解决问题:

Popen.communicate(...)

这允许父进程分别沿pipe发送数据,但只能调用一次,因此不适用于具有多个输出和输入的程序 - 正如可以从文档中推断出来的那样。 / p>

直接从Popen.stdoutPopen.stderr阅读并写信至Popen.stdin

文档警告这一点,当程序开始等待输入时,Popen.stdout s .read().readline()调用似乎无限制地阻塞。

使用select.select(...)查看文件句柄是否已准备好进行I / O

这似乎没有改善任何事情。显然,管道随时可以阅读或写作,因此select.select(...)在这里没有多大帮助。

使用不同的线程进行非阻塞读取

根据this answer的建议,我尝试创建一个单独的Thread(),用于存储从stdout读取到Queue()的结果。在要求用户输入的行之前的输出行很好地显示,但程序开始等待用户输入的行(上例中的"Type your name: ")永远不会被读取。

使用PTY从属作为子进程'文件句柄

按照指示here,我尝试pty.openpty()创建一个包含主文件和从文件描述符的伪终端。之后,我将slave文件描述符作为subprocess.Popen(...)调用的stdoutstderrstdin参数的参数。读取使用os.fdopen(...)打开的主文件描述符会产生与使用不同线程相同的结果:线要求输入无法读取。

编辑:使用@Antti Haapala的pty.fork()示例创建子进程而不是subprocess.Popen(...)似乎还允许我读取raw_input(...)创建的输出。

使用pexpect

我还尝试了使用pexpect生成的进程的read()read_nonblocking()readline()方法(记录为here),但最好的结果是我得到的使用read_nonblocking()与之前相同:在想要用户输入内容之前输出的行不会被读取。与使用{{1}创建的PTY相同}:要求输入的行被读取。

修改:在创建孩子的计划中使用pty.fork()sys.stdout.write(...)代替sys.stdout.flush(),似乎修复了提示行没有显示 - 但实际上它在两种情况下都被读取了。

其他

我也尝试了select.poll(...),但似乎管道或PTY主文件描述符总是可以写入。

注释

其他解决方案

  • 我想到的是,在经过一段时间而没有生成新输出的情况下尝试输入输入。然而,这是有风险的,因为没有办法知道该程序是否只是在进行繁重的计算。
  • 正如@Antti Haapala在他的回答中提到的,可以替换来自glibc的print系统调用包装器以将输入传递给主程序。但是,这不适用于静态链接或汇编程序。 (虽然,现在我想起来了,任何这样的调用都可以从源代码中截获并替换为read()的修补版本 - 可能仍然需要付出艰苦的努力。)
  • 修改Linux内核代码以将read()系统调用传递给程序可能是疯了......

的pty

我认为PTY是可行的方式,因为它假装终端并且交互式程序在各处的终端上运行。问题是,如何?

2 个答案:

答案 0 :(得分:5)

你是否注意到如果stdout是terminal(isatty),raw_input会将提示字符串写入stderr;如果stdout不是终端,那么提示也会被写入stdout,但是stdout将处于完全缓冲模式。

使用stdout on tty

write(1, "Hello.\n", 7)                  = 7
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(2, "Type your name: ", 16)         = 16
fstat(0, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb114059000
read(0, "abc\n", 1024)                   = 4
write(1, "Nice to meet you, abc!\n", 23) = 23

stdout不在tty

ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff8d9d3410) = -1 ENOTTY (Inappropriate ioctl for device)
# oops, python noticed that stdout is NOTTY.
fstat(0, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f29895f0000
read(0, "abc\n", 1024)                     = 4
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f29891c4bd0}, {0x451f62, [], SA_RESTORER, 0x7f29891c4bd0}, 8) = 0
write(1, "Hello.\nType your name: Nice to m"..., 46) = 46
# squeeze all output at the same time into stdout... pfft.

因此,所有写入都同时被压缩到stdout中;更糟糕的是,读取输入后。

因此,真正的解决方案是使用pty。但是你做错了。要使pty起作用,必须使用pty.fork()命令,而不是子进程。 (这将非常棘手)。我有一些像这样的工作代码:

import os
import tty
import pty

program = "python"

# command name in argv[0]
argv = [ "python", "foo.py" ]

pid, master_fd = pty.fork()

# we are in the child process
if pid == pty.CHILD:
    # execute the program
    os.execlp(program, *argv)

# else we are still in the parent, and pty.fork returned the pid of 
# the child. Now you can read, write in master_fd, or use select:
# rfds, wfds, xfds = select.select([master_fd], [], [], timeout)

请注意,根据子程序设置的终端模式,可能会出现不同种类的换行等。

现在关于“等待输入”的问题,这不能真正帮助,因为人们总能写入伪终端;字符将被放入缓冲区中等待。同样,管道总是允许在阻塞之前写入4K或32K或其他一些实现定义的数量。一种丑陋的方法是在程序进入读取系统调用时对程序进行检查并注意,fd = 0;另一种方法是创建一个带有替换“read()”系统调用的C模块,并在glibc之前将其链接到动态链接器(如果可执行文件是静态链接或直接使用系统调用汇编程序,则会失败),以及然后每当执行read(0,...)系统调用时都会发出python信号。总而言之,可能完全不值得麻烦。

答案 1 :(得分:0)

您可以使用linux script命令,而不是尝试检测子进程何时等待输入。从脚本的手册页:

  

脚本实用程序会在终端上打印所有内容的打字稿。

如果您在终端上使用它,可以像这样使用它:

$ script -q <outputfile> <command>

因此,在Python中,您可以尝试将此命令发送到Popen例程,而不仅仅是<command>

编辑: 我做了以下计划:

#include <stdio.h>
int main() {
    int i;
    scanf("%d", &i);
    printf("i + 1 = %d\n", i+1);
}

然后运行如下:

$ echo 9 > infile
$ script -q output ./a.out < infile
$ cat output
9
i + 1 = 10

所以我认为可以用Python这种方式完成,而不是使用stdout的{​​{1}},stderrstdin标记。