Обнаружение, когда дочерний процесс ожидает ввода
Я пишу программу 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!
Таким образом, проблема заключается в обнаружении, когда программа ожидает ввода.
Пробные методы
Я попытался использовать следующие методы для решения проблемы:
Это позволяет родительскому процессу отдельно отправлять данные по pipe, но может быть вызван только один раз и поэтому не подходит для программ с несколькими выходами и входами - так же, как это можно сделать из документации.
Документация предупреждает об этом, а Popen.stdout
.read()
и .readline()
, кажется, блокируется бесконечно, когда программы начинают ждать ввода.
Используя select.select(...)
, чтобы посмотреть, готовы ли файлы для ввода/вывода
Это ничего не улучшает. По-видимому, трубы всегда готовы к чтению или записи, поэтому select.select(...)
здесь не помогает.
Использование другого потока для неблокирующего чтения
Как было предложено в этом ответе, я попытался создать отдельный Thread() хранит результаты чтения из stdout
в Queue(). Выходные строки перед строкой, требующей ввода пользователя, отображаются хорошо, но строка, по которой программа начинает ждать ввода пользователем ("Type your name: "
в приведенном выше примере), никогда не читается.
Использование PTY ведомый, поскольку файл дочернего процесса обрабатывает
Как указано здесь, я пробовал pty.openpty()
, чтобы создать псевдотерминал с дескрипторами ведущего и подчиненного файлов. После этого я дал описатель подчиненного файла в качестве аргумента для параметров subprocess.Popen(...)
call stdout
, stderr
и stdin
. Чтение через дескриптор главного файла, открытый с помощью os.fdopen(...)
, дает тот же результат, что и при использовании другого потока: строка, требующая ввода, не считывается.
Изменить: Использование примера @Antti Haapala pty.fork()
для создания дочернего процесса вместо subprocess.Popen(...)
, похоже, позволяет мне также читать результат, созданный raw_input(...)
.
Я также пробовал методы read()
, read_nonblocking()
и readline()
(зарегистрированный здесь) процесса, порожденного pexpect, но лучший результат, который я получил с read_nonblocking()
, , такой же, как и раньше: строка с выводами перед тем, как пользователь вводит что-то, не читается. совпадает с PTY, созданный с помощью pty.fork()
: ввод строки, требующей ввода, читается.
Edit:Используя sys.stdout.write(...)
и sys.stdout.flush()
вместо print
ing в моей основной программе, которая создает дочерний элемент, казалось, что исправление строки приглашения не отображается, но в обоих случаях оно действительно прочитано.
Другие
Я также пробовал select.poll(...)
, но казалось, что дескрипторы файла pipe или PTY всегда готовы для записи.
Примечания
Другие решения
- То, что также перешло мне в голову, - это попробовать подавать входные данные, когда какое-то время прошло без создания нового выхода. Это, однако, рискованно, потому что нет способа узнать, находится ли программа только в середине тяжелого расчета.
- Как отметил в своем ответе @Antti Haapala, обертка системного вызова
read()
от glibc могла быть заменена, чтобы сообщать входы основной программе. Однако это не работает со статически связанными или сборочными программами. (Хотя теперь, когда я думаю об этом, любые такие вызовы могут быть перехвачены из исходного кода и заменены исправленной версией read()
- могут быть кропотливыми, чтобы реализовать все еще.)
- Изменение кода ядра Linux для передачи системных вызовов
read()
в программу, вероятно, является безумным...
Ptys
Я думаю, что PTY - это путь, так как он подделывает терминальные и интерактивные программы, которые работают на терминалах повсюду. Вопрос в том, как?
Ответы
Ответ 1
Вы заметили, что raw_input записывает строку приглашения в stderr, если stdout является терминалом (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 или некоторую другую определенную величину реализации перед блокировкой. Один уродливый способ заключается в том, чтобы трассировать программу и уведомлять, когда она входит в системный вызов, с fd = 0; другой - сделать модуль C с заменяющим системным вызовом "read()" и связать его перед glibc для динамического компоновщика (сбой, если исполняемый файл статически связан или использует системные вызовы непосредственно с ассемблером...) и затем будет сигнализировать python всякий раз, когда выполняется системный вызов read (0,...). В целом, вероятно, не стоит того, чтобы точно.
Ответ 2
Вместо того, чтобы пытаться обнаружить, когда дочерний процесс ожидает ввода, вы можете использовать команду linux script
. На странице man для script:
Утилита script создает 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
.