Problem
So you want to log the stdout and stderr (separately) of a process or subprocess, without the output being different from what you'd see in the terminal if you weren't logging anything.
因此,您希望将进程或子进程的stdout和stderr(单独)记录下来,而不需要将输出与您在终端中看到的输出进行日志记录。
Seems pretty simple no? Well unfortunately, it appears that it may not be possible to write a general solution for this problem, that works on any given process...
似乎很简单不是吗?不幸的是,似乎不可能为这个问题写出一个通用的解决方案,它适用于任何给定的过程……
Background
Pipe redirection is one method to separate stdout and stderr, allowing you to log them individually. Unfortunately, if you change the stdout/err to a pipe, the process may detect the pipe is not a tty (because it has no width/height, baud rate, etc) and may change its behaviour accordingly. Why change the behaviour? Well, some developers make use of features of a terminal which don't make sense if you are writing out to a file. For example, loading bars often require the terminal cursor to be moved back to the beginning of the line and the previous loading bar to be overwritten with a bar of a new length. Also colour and font weight can be displayed in a terminal, but in a flat ASCII file they can not. If you were to write such a program's stdout directly to a file, that output would contain all the terminal ANSI escape codes, rather than properly formatted output. The developer therefore implements some sort of "isatty" check before writing anything to the stdout/err, so it can give a simpler output for files if that check returns false.
管道重定向是分离stdout和stderr的一种方法,允许您单独记录它们。不幸的是,如果您将stdout/err更改为管道,进程可能会检测到管道不是tty(因为它没有宽度/高度、波特率等),并可能相应地更改其行为。为什么改变行为?有些开发人员会使用终端的一些特性,如果你写文件的话,这些特性是没有意义的。例如,加载条通常要求将终端游标移回该行的开头,并使用新的长度的条覆盖先前的加载条。同样,颜色和字体的重量可以在终端中显示,但是在一个扁平的ASCII文件中不能显示。如果您要将这样一个程序的stdout直接写入文件,那么输出将包含所有终端ANSI转义代码,而不是正确格式化的输出。因此,开发人员在将任何东西写入stdout/err之前都实现了某种“isatty”检查,因此如果检查返回false,它可以为文件提供更简单的输出。
The usual solution here is to trick such programs into thinking the pipes are actually ttys by using a pty - a bidirectional pipe that also has width, height, etc. You redirect all inputs/outputs of the process to this pty, and that tricks the process into thinking its talking to a real terminal (and you can log it directly to a file). The only problem is, that by using a single pty for stdout and stderr, we can now no longer differentiate between the two.
通常的解决方案是哄骗这些项目思维管道实际上是tty(通过使用企业——双向管道也有宽度,高度,等等。你过程的所有输入/输出重定向到这个企业,这技巧过程认为其与真正的终端(也可以直接记录到一个文件)。唯一的问题是,通过对stdout和stderr使用一个pty,我们现在不能再区分这两个变量了。
So you might want to try a different pty for each pipe - one for the stdin, one for the stdout, and one for the stderr. While this will work 50% of the time, many processes unfortunately do additional redirection checks that make sure that the output path of the stdout and stderr (/dev/tty000x) are the same. If they are not, there must be redirection, thus they give you the same behaviour as if you had piped the stderr and stdout without a pty.
因此,您可能需要为每个管道尝试不同的pty—一个用于stdin,一个用于stdout,一个用于stderr。虽然这将在50%的时间内完成,但是很不幸的是,许多进程执行了额外的重定向检查,以确保stdout和stderr (/dev/tty000x)的输出路径是相同的。如果它们不是,就必须有重定向,因此它们给您的行为就像您在没有pty的情况下通过管道传输stderr和stdout一样。
You might think this over-the-top checking for redirection is uncommon, but unfortunately it is actually quite prevalent because a lot of programs re-use other code for checking, like this bit of code found in OSX:
您可能认为这种对重定向的过度检查是不常见的,但不幸的是,它实际上非常流行,因为许多程序重用其他代码进行检查,就像在OSX中发现的这段代码:
http://src.gnu-darwin.org/src/bin/stty/util.c
http://src.gnu-darwin.org/src/bin/stty/util.c
Challenge
I think the best way to find a solution is in the form of a challenge. If anyone can run the following script (ideally via Python, but at this point I'll take anything) in such a way that the stdout and stderr is logged separately, AND you managed to fool it into thinking it was executed via a tty, you solve the problem :)
我认为找到解决方案的最好方式是挑战。如果任何人都可以运行以下脚本(理想情况下是通过Python运行,但现在我将采用任何方法),以便将stdout和stderr分别记录下来,并且您设法欺骗它,使它认为它是通过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()
Note that a solution should really work for any process, not just this code specifically. Overwriting the sys/os module and using LD_PRELOAD is very interesting ways to beat the challenge, but they don't solve the heart of the problem :)
请注意,解决方案应该真正适用于任何进程,而不仅仅是这些代码。覆盖sys/os模块并使用LD_PRELOAD是克服挑战的非常有趣的方法,但它们不能解决问题的核心:)
4 个解决方案
#1
18
Like this?
像这样的吗?
% ./challenge.py >stdout 2>stderr
% cat stdout
This is a real tty :)
standard output data
% cat stderr
standard error data
Because I cheated a little bit. ;-)
因为我作弊了一点。:-)
% echo $LD_PRELOAD
/home/karol/preload.so
Like so...
像这样…
% gcc preload.c -shared -o preload.so -fPIC
I feel dirty now, but it was fun. :D
我现在觉得很脏,但很有趣。: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;
}
#2
5
For a simpler use-case (e.g. development testing), use strace
(linux) or dtruss
(OSX). Of course that won't work in privileged process.
对于更简单的用例(例如开发测试),可以使用strace (linux)或dtruss (OSX)。当然,这在特权程序中是行不通的。
Here's a sample, you can distinguish stdout
fd1 from stderr
fd2:
这里有一个样本,你可以区分stdout fd1和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 +++
In the sample above you see each standard xxx data
doubled, because you can't redirect stdout/stderr. You can, however ask strace
to save its output to a file.
在上面的示例中,您将看到每个标准xxx数据加倍,因为您无法重定向stdout/stderr。但是,您可以要求strace将其输出保存到一个文件中。
On a theoretical side, if stdout
and stderr
refer to the same terminal, you can only distinguish between the 2 while still in the context of your process, either in user mode (LD_PRELOAD), or kernel space (ptrace interface that strace tool uses). Once the data hits actual device, real of pseudo, the distinction is lost.
在理论上,如果stdout和stderr引用相同的终端,您只能在进程的上下文中区分2,或者在用户模式(LD_PRELOAD)中,或者在内核空间(strace工具使用的ptrace接口)中。一旦数据到达实际设备,即pseudo的真实设备,就失去了区别。
#3
1
You can always allocate Pseudo-TTY, that's what screen
does.
你总是可以分配伪tty,这就是屏幕的作用。
In Python you'd access it using pty.openpty()
在Python中,可以使用pty.openpty()访问它
This "master" code passes your test:
这个“master”代码通过了您的测试:
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()
Of course if you want to distinguish between stdin/out/err, your "slave" process will see different PYT names:
当然,如果您想区分stdin/out/err,您的“从属”过程将看到不同的PYT名称:
inp = pty.openpty()
oup = pty.openpty()
erp = pty.openpty()
subprocess.Popen([command, args], stdin=inp[1], stdout=uop[1], stderr=erp[1])
#4
0
$ PYTHONPATH=/tmp/python:$PYTHONPATH ./challenge.py
$ cat stdout
This is a real tty :)
standard output data
$ cat stderr
standard error data
Because this script imports the os
module, I've cheated by creating my own os
module in /tmp/python
and prepending /tmp/python
to sys.path
.
因为这个脚本导入了os模块,所以我在/tmp/python和prepending /tmp/python中创建了自己的os模块。
os.py
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
#1
18
Like this?
像这样的吗?
% ./challenge.py >stdout 2>stderr
% cat stdout
This is a real tty :)
standard output data
% cat stderr
standard error data
Because I cheated a little bit. ;-)
因为我作弊了一点。:-)
% echo $LD_PRELOAD
/home/karol/preload.so
Like so...
像这样…
% gcc preload.c -shared -o preload.so -fPIC
I feel dirty now, but it was fun. :D
我现在觉得很脏,但很有趣。: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;
}
#2
5
For a simpler use-case (e.g. development testing), use strace
(linux) or dtruss
(OSX). Of course that won't work in privileged process.
对于更简单的用例(例如开发测试),可以使用strace (linux)或dtruss (OSX)。当然,这在特权程序中是行不通的。
Here's a sample, you can distinguish stdout
fd1 from stderr
fd2:
这里有一个样本,你可以区分stdout fd1和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 +++
In the sample above you see each standard xxx data
doubled, because you can't redirect stdout/stderr. You can, however ask strace
to save its output to a file.
在上面的示例中,您将看到每个标准xxx数据加倍,因为您无法重定向stdout/stderr。但是,您可以要求strace将其输出保存到一个文件中。
On a theoretical side, if stdout
and stderr
refer to the same terminal, you can only distinguish between the 2 while still in the context of your process, either in user mode (LD_PRELOAD), or kernel space (ptrace interface that strace tool uses). Once the data hits actual device, real of pseudo, the distinction is lost.
在理论上,如果stdout和stderr引用相同的终端,您只能在进程的上下文中区分2,或者在用户模式(LD_PRELOAD)中,或者在内核空间(strace工具使用的ptrace接口)中。一旦数据到达实际设备,即pseudo的真实设备,就失去了区别。
#3
1
You can always allocate Pseudo-TTY, that's what screen
does.
你总是可以分配伪tty,这就是屏幕的作用。
In Python you'd access it using pty.openpty()
在Python中,可以使用pty.openpty()访问它
This "master" code passes your test:
这个“master”代码通过了您的测试:
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()
Of course if you want to distinguish between stdin/out/err, your "slave" process will see different PYT names:
当然,如果您想区分stdin/out/err,您的“从属”过程将看到不同的PYT名称:
inp = pty.openpty()
oup = pty.openpty()
erp = pty.openpty()
subprocess.Popen([command, args], stdin=inp[1], stdout=uop[1], stderr=erp[1])
#4
0
$ PYTHONPATH=/tmp/python:$PYTHONPATH ./challenge.py
$ cat stdout
This is a real tty :)
standard output data
$ cat stderr
standard error data
Because this script imports the os
module, I've cheated by creating my own os
module in /tmp/python
and prepending /tmp/python
to sys.path
.
因为这个脚本导入了os模块,所以我在/tmp/python和prepending /tmp/python中创建了自己的os模块。
os.py
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