26

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

目前的解决方案

目前,我的解决方案是subprocess.Popen(...)使用stdout,stderrstdin. 句柄后面的文件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!

鉴于包含 的文件的内容stdinAnonymous<LF>. 因此,简而言之,对于给定的示例代码(以及等效地,对于任何其他代码),我希望获得如下结果:

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

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

尝试过的方法

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

Popen.communicate(...)

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

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

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

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

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

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

正如这个答案中所建议的,我尝试创建一个单独的Thread()来存储从读取stdoutQueue()的结果。要求用户输入的行之前的输出行显示得很好,但程序开始等待用户输入的行("Type your name: "在上面的示例中)永远不会被读取。

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

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

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

使用pexpect

我还尝试了使用 pexpect 生成的进程的read(),read_nonblocking()readline()方法(在此处read_nonblocking()记录),但是我得到的最佳结果与以前相同:在希望用户输入之前带有输出的行没有阅读。与使用创建的 PTY 相同:确实会读取pty.fork()要求输入的行。

编辑:通过在我的创建子程序的主程序中使用sys.stdout.write(...)andsys.stdout.flush()而不是printing ,似乎修复了提示行未显示的问题——尽管它实际上在两种情况下都被读取了。

其他

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

笔记

其他解决方案

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

PTY

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

4

2 回答 2

6

你有没有注意到,如果 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 发出信号。总而言之,可能不值得麻烦。

于 2013-08-07T16:01:20.483 回答
0

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

脚本实用程序为您终端上打印的所有内容制作打字稿。

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

$ 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 中完成,stdoutstderr不是stdin使用Popen.

于 2013-08-08T18:45:50.043 回答