python linux subprocess pty unbuffered-output

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 de read() - 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 .