Я пишу программу 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 - это путь, так как он подделывает терминальные и интерактивные программы, которые работают на терминалах повсюду. Вопрос в том, как?