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:
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