Я пишу программу 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.communicate(...)
Это позволяет родительскому процессу отдельно отправлять данные по pipe, но может быть вызван только один раз и поэтому не подходит для программ с несколькими выходами и входами - так же, как это можно сделать из документации.
Непосредственное чтение из Popen.stdout и Popen.stderr и запись в Popen.stdin
Документация предупреждает об этом, а 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(...)
.
Использование pexpect
Я также пробовал методы 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 - это путь, так как он подделывает терминальные и интерактивные программы, которые работают на терминалах повсюду. Вопрос в том, как?