小编典典

检测子进程何时等待输入

python

我正在编写一个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!

鉴于包含的文件的内容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从站作为子进程的文件句柄

按照这里的指示,我试图pty.openpty()用主文件和从文件描述符创建一个伪终端。在那之后,我已经给奴隶的文件描述符作为参数subprocess.Popen(...)调用的stdoutstderrstdin参数。读取以打开的主文件描述符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是必经之路,因为它伪造了一个终端,并且交互式程序在各处的终端上运行。问题是,如何?


阅读 195

收藏
2021-01-20

共1个答案

小编典典

您是否已经注意到,如果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发送信号。总而言之,可能完全不值得麻烦。

2021-01-20