你可以欺骗isatty并单独loginstdout和stderr吗?

问题

所以你要logging一个进程或subprocess的stdout和stderr(单独的),如果你没有logging任何东西,输出和你在terminal中看到的不一样。

似乎很简单没有? 不幸的是,似乎不可能为这个问题写出一个通用的解决scheme,这对任何给定的过程都有效。

背景

pipe道redirect是分离标准输出和标准错误的一种方法,允许您单独logging它们。 不幸的是,如果将stdout / err更改为pipe道,则进程可能会检测到pipe道不是tty(因为它没有宽度/高度,波特率等),并可能相应地改变其行为。 为什么改变行为? 那么,有些开发人员利用terminal的function,如果你正在写出一个文件,这是没有意义的。 例如,加载栏通常需要将terminal光标移回到行的开始位置,并使用新长度的栏来覆盖以前的加载栏。 颜色和字体重量也可以显示在terminal中,但是在平面ASCII文件中不能。 如果要将这样一个程序的标准输出直接写入文件,则该输出将包含所有terminal的ANSI转义码,而不是格式正确的输出。 因此,开发人员在向stdout / err写入任何内容之前都会执行某种“isatty”检查,因此如果该检查返回false,则可以为文件提供更简单的输出。

这里通常的解决scheme是欺骗这样的程序,通过使用一个pty(一个双向pipe道,也有宽度,高度等等)来认为pipe道实际上是tty。你将进程的所有input/输出redirect到这个pty,处理成思考它与真实的terminal交谈(你可以直接把它logging到文件中)。 唯一的问题是,通过使用stdout和stderr的单个pty,我们现在不能再区分这两者了。

所以你可能想为每个pipe道尝试一个不同的pty,一个是stdin,一个是stdout,另一个是stderr。 虽然这将在50%的时间内运行,但许多进程不幸的是会执行额外的redirect检查,以确保stdout和stderr(/ dev / tty000x)的输出path是相同的。 如果它们不是,那么必须有redirect,因此它们会给你一样的行为,就好像你已经用stdout和stdout没有pty一样。

你可能会认为这种redirect检查是不常见的,但不幸的是,这实际上是非常普遍的,因为很多程序都重复使用其他代码来检查,就像在OSX中find这样的代码:

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

挑战

我认为寻找解决scheme的最佳方式是以挑战的forms。 如果任何人都可以运行下面的脚本(理想情况下通过Python,但在这一点上我会采取任何东西),stdout和stderr分别logging,你设法愚弄它认为它是通过一个tty,你解决了这个问题:)

#!/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() 

请注意,解决scheme应该真的适用于任何进程,而不仅仅是这个代码。 覆盖sys / os模块,并使用LD_PRELOAD是非常有趣的方法来击败挑战,但他们不解决问题的核心:)

Solutions Collecting From Web of "你可以欺骗isatty并单独loginstdout和stderr吗?"

喜欢这个?

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

因为我欺骗了一点点 😉

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

像这样…

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

我现在觉得很肮脏,但是很有趣。 :d

 % 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; } 

对于更简单的用例(例如开发测试),使用strace (linux)或dtruss (OSX)。 当然这不会在特权过程中工作。

下面是一个示例,您可以从stderr fd2中区分stdout fd1:

 $ 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 +++ 

在上面的示例中,您会看到每个standard xxx data翻倍,因为您无法重定向stdout / stderr。 你可以,但是要求strace把它的输出保存到一个文件中。

从理论上讲,如果stdoutstderr指向同一个终端,那么在用户模式(LD_PRELOAD)或内核空间(strace工具使用的ptrace接口)中,只能在进程上下文中区分2, 。 一旦数据碰到实际的设备,真正的伪造,区分就失去了。

你可以随时分配伪TTY,这就是screen

在Python中,你可以使用pty.openpty()

这个“主”代码通过了你的测试:

 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() 

当然,如果你想区分stdin / out / err,你的“slave”进程将会看到不同的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 

因为这个脚本导入了os模块,所以我在/tmp/python创建了自己的os模块,并将/tmp/python sys.path到了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