检测subprocess何时等待input

我正在编写一个Python程序,用于在Linux服务器上运行用户上传的任意代码(最坏的情况下是不安全的,错误的和崩溃的)。 抛开安全性问题,我的目标是要确定,如果代码(可能是用任何语言编译或解释的)将正确的东西写入stdoutstderr和其他文件,并将其input到程序的stdin 。 之后,我需要将结果显示给用户。

目前的解决scheme

目前,我的解决scheme是使用subprocess.Popen(...)具有stdoutstderrstdin文件句柄的subprocess。 stdin句柄后面的文件包含程序在操作期间读取的input,在程序结束后,会读取stdoutstderr文件并检查其是否正确。

问题

这种方法在其他方面是完美的,但是当我显示结果时,我不能将给定的input和输出组合起来,这样input将会出现在与从terminal运行程序时相同的地方。 就像一个程序

 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! 

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

试过的方法

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

Popen.communicate(…)

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

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

文档警告这个,当程序开始等待input时, Popen.stdout.read().readline()调用似乎无限地阻塞。

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

这似乎并没有改善任何东西。 显然,pipe道总是准备好阅读或写作,所以select.select(...)在这里没有什么帮助。

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

正如在这个答案中所build议的,我已经尝试创build一个单独的Thread() ,它将从stdout读取的结果存储到Queue()中 。 在要求用户input的行之前的输出行很好地显示,但是程序开始等待用户input的行(上面例中的"Type your name: "input"Type your name: " )永远不会被读取。

使用PTY slave作为subprocess的文件句柄

按照这里的指示,我尝试过使用pty.openpty()来创build一个具有主从文件描述符的伪terminal。 之后,我将slave文件描述符作为subprocess.Popen(...)调用的stdoutstderrstdin参数的参数。 通过使用os.fdopen(...)打开的主文件描述符的读取产生与使用不同的线程相同的结果:要求input的行不会被读取。

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

使用pexpect

我也尝试了使用pexpect生成的进程的read()read_nonblocking()readline()方法( 这里logging ),但read_nonblocking()得到的最好结果与之前一样:在用户input内容之前的输出不会被读取。 与使用pty.fork()创build的PTY相同:要求input的行不会被读取。

编辑:通过使用sys.stdout.write(...)sys.stdout.flush()而不是print在我的主程序,创build孩子,似乎修复提示行不显示 – 它实际上已阅读在这两种情况下,虽然。

其他

我也试过select.poll(...) ,但似乎pipe或PTY主文件描述符总是准备写入。

笔记

其他解决scheme

  • 我还想到的是,在一段时间过去之后尝试给input提供input,而没有产生新的输出。 然而,这样做风险很大,因为无法知道程序是否正在进行繁重的计算。
  • 正如@Antti Haapala在他的回答中提到的那样,glibc中的read()系统调用包装可以被replace,以将input传递给主程序。 但是,这不适用于静态链接或汇编程序。 (虽然现在我想起来了,但是任何这样的调用都可以从源代码中被截获,并且replace为read()的补丁版本 – 可能仍然很难实现。)
  • 修改Linux内核代码以传递read()系统调用程序可能是疯狂的…

的pty

我认为PTY是要走的路,因为它伪造了terminal,互动程序在terminal上运行。 问题是,如何?

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

用tty的stdout

 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.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信号。 总而言之,可能不值得麻烦。

而不是试图检测子进程何时等待输入,您可以使用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来完成,而不是使用stdoutstderrstdin标志。