检测子进程何时等待输入
我正在编写一个Python程序,用于在Linux服务器上运行用户上传的任意代码(因此,在最坏的情况下,就是不安全,错误和崩溃的代码)。除了安全性问题外,我的目标是确定代码(可能以任何语言编写,编译或解释的)是否将正确的内容写入stdout
,stderr
以及是否将给定输入的其他文件写入程序的stdin
。之后,我需要向用户显示结果。
当前解决方案
目前,我的解决办法是使用产卵子进程subprocess.Popen(...)
与文件句柄stdout
,stderr
和stdin
。后面的文件stdin
句柄包含了操作过程中的程序读取输入,并且该程序已终止后,将stdout
和stderr
文件的读取,并检查正确性。
问题
这种方法在其他方面可以完美地起作用,但是当我显示结果时,我无法组合给定的输入和输出,因此输入将出现在与从终端运行程序时相同的位置。即对于像这样的程序
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.stdout和Popen.stderr读取并写入Popen.stdin
文档对此提出警告,并且在程序开始等待输入时,Popen.stdout
s.read()
和.readline()
调用似乎无限阻塞。
使用select.select(...)
,看是否文件句柄准备好I
/ O
这似乎没有任何改善。显然,管道始终可以读取或写入,因此select.select(...)
在这里没有太大帮助。
使用其他线程进行非阻塞读取
如该答案所建议,我尝试创建一个单独的Thread()来存储从读取stdout
到Queue()的结果。要求用户输入的行之前的输出行显示得很好,但是程序开始等待用户输入的行("Type your name: "
在上面的示例中)从未被读取。
使用PTY从站作为子进程的文件句柄
按照这里的指示,我试图pty.openpty()
用主文件和从文件描述符创建一个伪终端。在那之后,我已经给奴隶的文件描述符作为参数subprocess.Popen(...)
调用的stdout
,stderr
和stdin
参数。读取以打开的主文件描述符os.fdopen(...)
产生的结果与使用不同线程的结果相同:要求输入的行不会被读取。
编辑: 使用@Antti
Haapala的示例来pty.fork()
创建子进程,而不是subprocess.Popen(...)
让我也阅读了创建的输出raw_input(...)
。
使用pexpect
我也试过了read()
,read_nonblocking()
和readline()
方法(记录在这里)与Pexpect的催生了一个过程的,但最好的结果,我用了read_nonblocking()
,
是和以前一样:与输出线希望用户输入的东西不前阅读。 相同与创建的PTY pty.fork()
:苛刻的输入行 并 得到读。
编辑: 利用sys.stdout.write(...)
和sys.stdout.flush()
替代的print
荷兰国际集团在我的 掌握
程序,该程序创建的孩子,似乎解决提示行没有得到展示-它实际上得到了在这两种情况下阅读,虽然。
其他
我也尝试过select.poll(...)
,但是似乎管道或PTY主文件描述符总是可以编写。
笔记
其他解决方案
- 我还想到的是,经过一段时间而没有生成新的输出时,尝试提供输入。但是,这是有风险的,因为无法知道程序是否正处于进行大量计算的过程中。
- 正如@Antti Haapala在回答中提到的那样,
read()
可以替换glibc中的系统调用包装程序,以将输入传递给主程序。但是,这不适用于静态链接程序或汇编程序。(尽管现在考虑到这一点,任何这样的调用都可以从源代码中截获,并用已修补的版本替换read()
-可能仍然难以实现。) - 修改Linux内核代码以将
read()
syscall传达给程序可能是疯狂的…
PTYs
我认为PTY是必经之路,因为它伪造了一个终端,并且交互式程序在各处的终端上运行。问题是,如何?
-
您是否已经注意到,如果stdout是terminal(isatty),则raw_input将提示字符串写入stderr;如果stdout不是终端,那么提示符也会写入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
使用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发送信号。总而言之,可能完全不值得麻烦。