Подтвердить что ты не робот

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

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

4b9b3361

Ответ 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.