Обнаружение, когда дочерний процесс ожидает ввода

я пишу программу Python для запуска произвольного (и, в худшем случае, небезопасного, ошибочного и аварийного) кода на сервере Linux. Вопросы безопасности в сторону, моя цель-определить, если код (который может быть на любом языке, скомпилирован или интерпретирован) пишет правильные вещи в stdout, stderr и другие файлы на заданном входе, подаваемые вstdin. После этого мне нужно показать результаты пользователю.

текущий решение

в настоящее время мое решение-создать дочерний процесс с помощью subprocess.Popen(...) с дескрипторами файлов для stdout, stderr и stdin. Файл за stdin handle содержит входные данные, которые программа считывает во время работы, и после завершения программы 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.общаться.(..)

это позволяет родительскому процессу отдельно отправлять данные вдоль труба, но может быть вызван только один раз, и поэтому не подходит для программ с несколькими выходами и входами - так же, как и из документации.

непосредственно чтение из к popen.стандартный вывод и к popen.поток stderr и писать к popen.как stdin

документация предупреждает об этом, и Popen.stdouts .read() и .readline() вызовы, похоже, блокируются бесконечно, когда программы начинают ждать ввода.

используя select.select(...) чтобы увидеть, являются ли дескрипторы файлов готов к I / O

это, кажется, ничего не улучшает. По-видимому, трубы всегда готовы для чтения или письма, так что select.select(...) здесь не очень помогает.

использование другого потока для неблокирующего чтения

как полагают в ответ, я попытался создать отдельный Thread (), который хранит результаты чтения из stdout на Queue (). Выходные строки перед строкой, требующей пользователя входные данные отображаются красиво, но строка, на которой программа начинает ждать ввода пользователя ("Type your name: " в приведенном выше примере) никогда не читал.

С помощью PTY slave как дочерний процесс " файл обрабатывает

по указанию здесь, я пробовал pty.openpty() создать псевдо терминал с master и slave дескрипторов файлов. После этого я дал рабу дескриптор файла в качестве аргумента для subprocess.Popen(...) звонок stdout, stderr и stdin параметры. Чтение дескриптора главного файла, открытого с помощью os.fdopen(...) дает тот же результат, что и при использовании другого потока: строка, требующая ввода, не читается.

Edit: используя пример @Antti Haapala pty.fork() для создания дочернего процесса вместо subprocess.Popen(...) кажется, позволяет мне также читать вывод, созданный raw_input(...).

используя pexpect

я также пробовал the read(), read_nonblocking() и readline() методы (документально здесь) процесса, порожденного pexpect, но лучший результат, который я получил с read_nonblocking(), то же самое, что и раньше: строка с выходами перед желанием пользователя ввести что-то не читается. совпадает с ПТИ создан с pty.fork(): линия, требующая ввода тут читать.

Edit: с помощью sys.stdout.write(...) и sys.stdout.flush() вместо printing в моем мастер программа, которая создает ребенка, казалось, исправила строку подсказки, не отображаемую - она фактически была прочитана в обоих случаях.

другие

я тоже пробовал select.poll(...), но казалось, что дескрипторы Pipe или Pty master всегда готовы к записи.

Примечания

других решений

  • что также пришло мне в голову, это попробовать кормить ввод, когда прошло некоторое время без создания нового вывода. Это, однако, рискованно, потому что нет никакого способа узнать, находится ли программа только в середине выполнения тяжелых вычислений.
  • как @Antti Haapala упомянул в своем ответе,read() оболочка системного вызова из glibc может быть заменена для передачи входных данных в главную программу. Однако, это не работает со статически или сборки программ. (Хотя, теперь, когда я думаю об этом, любые такие звонки может быть перехвачен из исходного кода и заменен исправленной версией read() - может быть, кропотливого, чтобы все-таки реализовать.)
  • изменение кода ядра Linux для передачи read() syscalls для программы, вероятно, сумасшедший...

PTYs

я думаю, что PTY-это путь, так как он подделывает терминал, а интерактивные программы запускаются на терминалах повсюду. Вопрос в том, как?

2 ответов


вы заметили, что raw_input записывает строку приглашения в stderr, если stdout является terminal (isatty); если stdout не является терминалом, то приглашение тоже записывается в 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

С 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 или некоторую другую определенную сумму реализации перед блокировкой. Одним из уродливых способов является strace программу и уведомление, когда она входит в системный вызов read, с fd = 0; другим было бы сделать модуль C с заменой системного вызова "read ()" и связать его перед glibc для динамического компоновщика (не удается, если исполняемый файл статически связан или использует систему звонки непосредственно с ассемблера...), а затем будет сигнализировать python всякий раз, когда чтение (0,...) выполняется системный вызов. В общем, наверное, не стоит беспокоиться.


вместо того чтобы пытаться определить, когда дочерний процесс ждет ввода, вы можете использовать Linux . Из man-страницы для скрипта:

на скрипт утилита делает typescript всего, что напечатано на вашем терминале.

вы можете использовать его так, если вы использовали его на терминале:

$ 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 таким образом, а не с помощью stdout, stderr и stdin флаги Popen.