python - Detectar cuando un proceso hijo está esperando entrada
linux subprocess (2)
¿Ha notado que raw_input escribe la cadena de solicitud en stderr si stdout es terminal (isatty); si stdout no es un terminal, entonces la solicitud también se escribe en stdout, pero stdout estará en modo totalmente en búfer.
Con stdout en un 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
Con stdout no en un 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.
De este modo, todas las escrituras se comprimen en stdout todas al mismo tiempo; Y lo que es peor, después de leer la entrada.
La verdadera solución es, pues, utilizar la pty. Sin embargo lo estás haciendo mal. Para que funcione pty, debe usar el comando pty.fork (), no el subproceso. (Esto será muy complicado). Tengo un código de trabajo que va así:
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)
Tenga en cuenta que, dependiendo del modo de terminal establecido por el programa hijo, pueden aparecer diferentes tipos de saltos de línea, etc.
Ahora sobre el problema de la "espera de entrada", eso no puede ser realmente ayudado ya que siempre se puede escribir en un pseudoterminal; Los personajes se pondrán a esperar en el búfer. Del mismo modo, una canalización siempre permite escribir hasta 4K o 32K o alguna otra cantidad definida de implementación, antes de bloquear. Una manera fea es forzar el programa y notificarlo cada vez que ingresa la llamada del sistema de lectura, con fd = 0; el otro sería hacer un módulo C con una llamada al sistema "read ()" de reemplazo y vincularlo antes de glibc para el vinculador dinámico (falla si el ejecutable está estáticamente vinculado o usa llamadas al sistema directamente con el ensamblador ...), y luego señalaría a python siempre que se ejecute la llamada del sistema de lectura (0, ...). Con todo, probablemente no valga la pena exactamente.
Estoy escribiendo un programa de Python para ejecutar código arbitrario subido por el usuario (y, por lo tanto, en el peor de los casos, inseguro, erróneo y fallido) en un servidor Linux. Dejando de lado las preguntas de seguridad, mi objetivo es determinar si el código (que podría estar en cualquier idioma, compilado o interpretado) escribe las cosas correctas en stdout
, stderr
y otros archivos en la entrada dada introducida en la stdin
del programa. Después de esto, necesito mostrar los resultados al usuario.
La solucion actual
Actualmente, mi solución es generar el proceso hijo utilizando subprocess.Popen(...)
con manejadores de archivos para stdout
, stderr
y stdin
. El archivo que se encuentra detrás del identificador stdin
contiene las entradas que el programa lee durante el funcionamiento y, una vez finalizado el programa, los archivos stdout
y stderr
se leen y se comprueban para verificar su corrección.
El problema
De lo contrario, este enfoque funciona perfectamente, pero cuando muestro los resultados, no puedo combinar las entradas y salidas dadas para que las entradas aparezcan en los mismos lugares que lo harían al ejecutar el programa desde un terminal. Es decir, para un programa como
print "Hello."
name = raw_input("Type your name: ")
print "Nice to meet you, %s!" % (name)
el contenido del archivo que contiene la salida estándar del programa sería, después de ejecutarse, ser:
Hello.
Type your name:
Nice to meet you, Anonymous!
Dado que el contenido del archivo que contiene el stdin
era Anonymous<LF>
. Entonces, en resumen, para el código de ejemplo dado (y, de manera equivalente, para cualquier otro código) quiero lograr un resultado como:
Hello.
Type your name: Anonymous
Nice to meet you, Anonymous!
Por lo tanto, el problema es detectar cuando el programa está esperando una entrada.
Métodos probados
He intentado los siguientes métodos para resolver el problema:
Popen.communicate(...)
Esto permite que el proceso principal envíe datos por separado a lo largo de una pipe , pero solo se puede llamar una vez, y por lo tanto no es adecuado para programas con múltiples salidas y entradas, como se puede deducir de la documentación.
Leyendo directamente desde Popen.stdout y Popen.stderr y escribiendo a Popen.stdin
La documentación advierte contra esto, y las Popen.stdout
s Popen.stdout
.read()
y .readline()
parecen bloquearse infinitamente cuando los programas comienzan a esperar la entrada.
Usando select.select(...)
para ver si los manejadores de archivos están listos para E / S
Esto no parece mejorar nada. Al parecer, las tuberías siempre están listas para leer o escribir, por lo que select.select(...)
no ayuda mucho aquí.
Usando un hilo diferente para la lectura sin bloqueo
Como se sugiere en esta respuesta , he intentado crear un Thread() separado que almacena los resultados de la lectura del stdout
en una Queue() . Las líneas de salida antes de una línea que requieren la entrada del usuario se muestran bien, pero la línea en la que el programa comienza a esperar la entrada del usuario ( "Type your name: "
en el ejemplo anterior) nunca se lee.
Usando un esclavo PTY como los manejadores de archivos del proceso hijo
Como se indica here , he intentado pty.openpty()
para crear un pseudo terminal con descriptores de archivo maestro y esclavo. Después de eso, le he dado al descriptor del archivo esclavo como un argumento para los parámetros stdout
, stderr
y stdin
subprocess.Popen(...)
call. La lectura a través del descriptor de archivo maestro abierto con os.fdopen(...)
produce el mismo resultado que al usar un hilo diferente: la entrada que exige la línea no se lee.
Edición: Usar el ejemplo de @Antti Haapala de pty.fork()
para la creación de procesos secundarios en lugar de subprocess.Popen(...)
también me permite leer la salida creada por raw_input(...)
.
Usando pexpect
También probé los métodos read()
, read_nonblocking()
y readline()
(documentados here ) de un proceso generado con pexpect, pero el mejor resultado, que obtuve con read_nonblocking()
, es el mismo que antes: la línea con salidas antes de querer que el usuario ingrese algo no se lee. es lo mismo que con un PTY creado con pty.fork()
: la línea que exige la entrada se lee.
Edición: al usar sys.stdout.write(...)
y sys.stdout.flush()
lugar de print
en mi programa maestro , que crea el hijo, parece que se corrige el mensaje que no se muestra, realmente se leyó En ambos casos, sin embargo.
Otros
También probé select.poll(...)
, pero parecía que los descriptores de archivo maestro PTY o PTY siempre están listos para escribir.
Notas
Otras soluciones
- Lo que también se me pasó por la mente es intentar alimentar la entrada cuando ha pasado algún tiempo sin que se haya generado una nueva salida. Esto, sin embargo, es arriesgado, porque no hay manera de saber si el programa está haciendo un cálculo pesado.
- Como @Antti Haapala mencionó en su respuesta, el contenedor de llamadas del sistema
read()
de glibc podría reemplazarse para comunicar las entradas al programa maestro. Sin embargo, esto no funciona con programas vinculados estáticamente o ensamblados. (Aunque, ahora que lo pienso, cualquier llamada de este tipo podría ser interceptada desde el código fuente y reemplazada por la versión parcheada deread()
- podría ser una tarea meticulosa de implementar). - La modificación del código del kernel de Linux para comunicar los syscalls
read()
al programa es probablemente una locura ...
PTYs
Creo que el PTY es el camino a seguir, ya que simula una terminal y se ejecutan programas interactivos en todas partes. La pregunta es, ¿cómo?
En lugar de intentar detectar cuándo el proceso secundario está esperando una entrada, puede usar el comando de script
Linux. De la página del manual para el script:
La utilidad de script hace un mecanografiado de todo lo impreso en su terminal.
Puedes usarlo así si lo estuvieras usando en una terminal:
$ script -q <outputfile> <command>
Entonces, en Python, puedes intentar darle este comando a la rutina de Popen
lugar de solo <command>
.
Edit: hice el siguiente programa:
#include <stdio.h>
int main() {
int i;
scanf("%d", &i);
printf("i + 1 = %d/n", i+1);
}
y luego corrió de la siguiente manera:
$ echo 9 > infile
$ script -q output ./a.out < infile
$ cat output
9
i + 1 = 10
Así que creo que se puede hacer en Python de esta manera en lugar de usar las banderas stdout
, stderr
y stdin
de Popen
.