检测子进程何时等待输入

Posted

技术标签:

【中文标题】检测子进程何时等待输入【英文标题】:Detecting when a child process is waiting for input 【发布时间】:2013-08-09 01:47:47 【问题描述】:

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

目前的解决方案

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

问题

这种方法效果很好,但是当我显示结果时,我无法组合给定的输入和输出,以便输入出现在与从终端运行程序时相同的位置。 IE。对于像

这样的程序
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>。因此,简而言之,对于给定的示例代码(以及等效地,对于 any 其他代码),我希望获得如下结果:

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

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

尝试过的方法

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

Popen.communicate(...)

这允许父进程通过pipe 单独发送数据,但只能调用一次,因此不适合具有多个输出和输入的程序 - 正如可以从文档中推断的那样。

直接从Popen.stdout和Popen.stderr读取并写入Popen.stdin

文档对此提出警告,当程序开始等待输入时,Popen.stdouts .read().readline() 调用似乎无限阻塞。

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

这似乎没有任何改善。显然管道总是准备好读取或写入,所以select.select(...) 在这里没有多大帮助。

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

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

使用PTY slave 作为子进程的文件句柄

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

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

使用pexpect

我还尝试了使用 pexpect 生成的进程的 read()read_nonblocking()readline() 方法(记录在 here),但最好的结果是我使用 read_nonblocking() 得到的,与以前相同:在希望用户输入内容之前具有输出的行不会被读取。 与使用 pty.fork() 创建的 PTY 相同:需要输入的行确实 em> 阅读。

编辑:通过在我的创建孩子的master程序中使用sys.stdout.write(...)sys.stdout.flush()而不是printing,似乎修复了提示行没有显示 - 但实际上在这两种情况下都被读取了。

其他

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

注意事项

其他解决方案

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

PTY

我认为 PTY 是要走的路,因为它伪造了一个终端,并且交互式程序在任何地方的终端上运行。问题是,怎么做?

【问题讨论】:

+1 很好的问题。我确实认为 PTY 是正确的方法,因为键入的用户输入既不会出现在标准输入也不会出现在标准输出上,它是 tty/pty 回显的函数。也就是说,我没有使用过pty 模块,所以不能给你一个正确的答案。 【参考方案1】:

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

在 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

标准输出不在 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.

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

因此,真正的解决方案是使用 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 或其他一些实现定义的数量。一种丑陋的方法是跟踪程序并注意它何时进入 read 系统调用,其中 fd = 0;另一种方法是使用替换“read()”系统调用创建一个 C 模块,并在 glibc 之前将其链接到动态链接器(如果可执行文件是静态链接或直接使用汇编器使用系统调用则失败......),并且then 会在执行 read(0, ...) 系统调用时向 python 发出信号。总而言之,可能不值得这么麻烦。

【讨论】:

您是否会知道这种“输入等待检测”是否可以在 Linux 以外的其他平台(例如某些 BSD)上本地实现?或者,如果现有的 Linux 内核补丁允许这种开箱即用的拦截? @miikkas:gdb may allow you to replace a syscall。 pexpect 也应该在 BSD 上工作。你可以use pty.spawn() on Linux in simple cases windows 有什么解决方案吗?【参考方案2】:

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

script 实用程序可将您终端上打印的所有内容制作成打字稿。

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

$ script -q <outputfile> <command>

因此,在 Python 中,您可以尝试将此命令提供给 Popen 例程,而不仅仅是 &lt;command&gt;

编辑: 我做了以下程序:

#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 中完成,而不是使用 Popenstdoutstderrstdin 标志。

【讨论】:

我认为您可以在 shell 脚本中执行此操作,我编辑了答案以显示这一点。但是我不确定你是否可以将它原样移植到 Python。 我无法重现示例中的结果;如果./a.out &lt; infile 部分用引号括起来(没有引号,它不包含任何内容),则输出仅包含脚本启动的日期。即使程序更复杂,即在scanf()s 和/或多个不同的scanf()s 之前有printf()s 和/或多个不同的scanf()s,您的示例是否有效? 我不确定是什么问题。如果将./a.out &lt; infile 括在引号中,则该部分不会被解释为命令,因此我不希望有任何输出。可以通过传递-q 选项来抑制日期部分。你能告诉我你用的是什么shell和系统吗?

以上是关于检测子进程何时等待输入的主要内容,如果未能解决你的问题,请参考以下文章

1-3 join方法

是否可以检测到哪个管道抛出了 SIGPIPE?

多线程《三》join方法

百万年薪python之路 -- 并发编程之 多进程二

Process join方法 以及其他属性

03process对象的其他方法属性