python linux unix tty pty

python - ¿Puedes engañar a isatty Y registrar stdout y stderr por separado?



linux unix (4)

¿Me gusta esto?

% ./challenge.py >stdout 2>stderr % cat stdout This is a real tty :) standard output data % cat stderr standard error data

Porque hice trampa un poco. ;-)

% echo $LD_PRELOAD /home/karol/preload.so

Al igual que...

% gcc preload.c -shared -o preload.so -fPIC

Me siento sucio ahora, pero fue divertido. :RE

% cat preload.c #include <stdlib.h> int isatty(int fd) { if(fd == 2 || fd == 1) { return 1; } return 0; } char* ttyname(int fd) { static char* fake_name = "/dev/fake"; if(fd == 2 || fd == 1) { return fake_name; } return NULL; }

Problema

Por lo tanto, desea registrar stdout y stderr (por separado) de un proceso o subproceso, sin que la salida sea diferente de la que vería en el terminal si no estuviera registrando nada.

Parece bastante simple, ¿no? Desafortunadamente, parece que no es posible escribir una solución general para este problema, que funcione en cualquier proceso dado ...

Fondo

La redirección de tuberías es un método para separar stdout y stderr, lo que le permite registrarlas individualmente. Desafortunadamente, si cambia el stdout / err a una tubería, el proceso puede detectar que la tubería no es una tty (porque no tiene ancho / alto, velocidad en baudios, etc.) y puede cambiar su comportamiento en consecuencia. ¿Por qué cambiar el comportamiento? Bueno, algunos desarrolladores hacen uso de las características de un terminal que no tienen sentido si está escribiendo en un archivo. Por ejemplo, las barras de carga a menudo requieren que el cursor de la terminal vuelva al principio de la línea y que la barra de carga anterior se sobrescriba con una barra de una nueva longitud. También se puede mostrar el peso del color y de la fuente en una terminal, pero en un archivo plano ASCII no pueden. Si tuviera que escribir el stdout de dicho programa directamente en un archivo, esa salida contendría todos los códigos de escape ANSI del terminal, en lugar de la salida formateada correctamente. Por lo tanto, el desarrollador implementa algún tipo de comprobación "isatty" antes de escribir algo en el stdout / err, por lo que puede dar un resultado más simple para los archivos si esa verificación devuelve falso.

La solución habitual aquí es engañar a tales programas para que piensen que las tuberías son en realidad ttys usando una pty, una tubería bidireccional que también tiene ancho, alto, etc. Usted redirige todas las entradas / salidas del proceso a esta pty, y eso engaña al proceso para pensar que está hablando con un terminal real (y puede registrarlo directamente en un archivo). El único problema es que al usar un solo pty para stdout y stderr, ahora ya no podemos diferenciar entre los dos.

Por lo tanto, es posible que desee probar con una pty diferente para cada tubería, una para el stdin, una para el stdout y otra para el stderr. Aunque esto funcionará el 50% del tiempo, lamentablemente, muchos procesos realizan comprobaciones de redireccionamiento adicionales que aseguran que la ruta de salida de stdout y stderr (/ dev / tty000x) sea la misma. Si no lo son, debe haber una redirección, por lo que te dan el mismo comportamiento que si hubieras canalizado el stderr y el stdout sin una pty.

Es posible que piense que este exceso de comprobación para la redirección es poco común, pero desafortunadamente es bastante frecuente porque muchos programas reutilizan otro código para verificar, como este código que se encuentra en OSX:

http://src.gnu-darwin.org/src/bin/stty/util.c

Reto

Creo que la mejor forma de encontrar una solución es en forma de desafío. Si alguien puede ejecutar el siguiente script (idealmente a través de Python, pero en este punto tomaré cualquier cosa) de manera que el stdout y el stderr se registren por separado, Y logre engañarlo y pensar que se ejecutó a través de un tty, tú resuelves el problema :)

#!/usr/bin/python import os import sys if sys.stdout.isatty() and sys.stderr.isatty() and os.ttyname(sys.stdout.fileno()) == os.ttyname(sys.stderr.fileno()): sys.stdout.write("This is a") sys.stderr.write("real tty :)") else: sys.stdout.write("You cant fool me!") sys.stdout.flush() sys.stderr.flush()

Tenga en cuenta que una solución realmente debería funcionar para cualquier proceso, no solo este código específicamente. Sobrescribir el módulo sys / os y usar LD_PRELOAD son formas muy interesantes de superar el desafío, pero no resuelven el problema central :)


Para un caso de uso más simple (por ejemplo, pruebas de desarrollo), use strace (linux) o dtruss (OSX). Por supuesto, eso no funcionará en un proceso privilegiado.

Aquí hay una muestra, puedes distinguir stdout fd1 de stderr fd2:

$ strace -ewrite python2 test.py [snip] write(1, "This is a real tty :)/n", 22This is a real tty :) ) = 22 write(2, "standard error data", 19standard error data) = 19 write(1, "standard output data", 20standard output data) = 20 +++ exited with 0 +++

En el ejemplo anterior, verá duplicados los standard xxx data , ya que no puede redirigir stdout / stderr. Sin embargo, puede pedirle que guarde su salida en un archivo.

Desde el punto de vista teórico, si stdout y stderr refieren al mismo terminal, solo puede distinguir entre los dos mientras se encuentra en el contexto de su proceso, ya sea en modo usuario (LD_PRELOAD) o kernel (interfaz ptrace que usa la herramienta strace) . Una vez que los datos llegan al dispositivo real, real de pseudo, la distinción se pierde.


Siempre puede asignar Pseudo-TTY, eso es lo que hace la screen .

En Python pty.openpty() utilizando pty.openpty()

Este código "maestro" pasa su prueba:

import subprocess, pty, os m, s = pty.openpty() fm = os.fdopen(m, "rw") p = subprocess.Popen(["python2", "test.py"], stdin=s, stdout=s, stderr=s) p.communicate() os.close(s) print fm.read()

Por supuesto, si desea distinguir entre stdin / out / err, su proceso "esclavo" verá diferentes nombres PYT:

inp = pty.openpty() oup = pty.openpty() erp = pty.openpty() subprocess.Popen([command, args], stdin=inp[1], stdout=uop[1], stderr=erp[1])


$ PYTHONPATH=/tmp/python:$PYTHONPATH ./challenge.py $ cat stdout This is a real tty :) standard output data $ cat stderr standard error data

Debido a que este script importa el módulo os , he hecho trampa al crear mi propio módulo os en /tmp/python y anteponer /tmp/python a sys.path .

os.py

import sys sys.path.remove(''/tmp/python'') this_module = sys.modules[''os''] del sys.modules[''os''] import os globals().update(vars(os)) class File(file): isatty = lambda self: True sys.stdout = File(''stdout'', ''w'') sys.stderr = File(''stderr'', ''w'') isatty = lambda fd: True ttyname = lambda fd: ''/dev/fake'' sys.modules[''os''] = this_module