你可以愚弄isatty并单独记录stdout和stderr吗?

时间:2015-12-09 18:12:46

标签: python linux unix tty pty

问题

因此,您希望记录进程或子进程的stdout和stderr(单独),如果您没有记录任何内容,则输出与您在终端中看到的不同。

看起来很简单没有?不幸的是,似乎可能无法为这个问题编写一个通用的解决方案,这个解决方案适用于任何给定的流程......

背景

管道重定向是一种分离stdout和stderr的方法,允许您单独记录它们。不幸的是,如果将stdout / err更改为管道,则进程可能会检测到管道不是tty(因为它没有宽度/高度,波特率等)并且可能会相应地更改其行为。为什么改变行为?好吧,一些开发人员利用终端的功能,如果你正在写一个文件,这些功能是没有意义的。例如,加载条通常要求将终端光标移回到行的开头,并使用新长度的条覆盖前一个加载条。颜色和字体粗细也可以显示在终端中,但是在平面ASCII文件中它们不能。如果您要将此类程序的stdout直接写入文件,则该输出将包含所有终端ANSI转义码,而不是正确格式化的输出。因此,开发人员实现某种类型的#34; isatty"在向stdout / err写入任何内容之前检查,因此如果该检查返回false,它可以为文件提供更简单的输出。

这里通常的解决方案是通过使用pty来欺骗这些程序,使管道实际上是tt - 一个也具有宽度,高度等的双向管道。您将进程的所有输入/输出重定向到此pty,并且这会诱使流程思考它与真实终端的对话(并且您可以将其直接记录到文件中)。唯一的问题是,通过对stdout和stderr使用单个pty,我们现在无法再区分这两者。

所以你可能想为每个管道尝试不同的pty - 一个用于stdin,一个用于stdout,一个用于stderr。虽然这将在50%的时间内工作,但不幸的是,许多进程会执行额外的重定向检查,以确保stdout和stderr(/ dev / tty000x)的输出路径相同。如果它们不是,则必须有重定向,因此它们会给你相同的行为,就好像你没有pty管道stderr和stdout一样。

您可能认为这种过度检查重定向并不常见,但遗憾的是它实际上非常普遍,因为许多程序重新使用其他代码进行检查,就像在OSX中找到的这些代码一样:

  

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

挑战

我认为找到解决方案的最佳方式是挑战。如果任何人都可以运行以下脚本(理想情况下通过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()

请注意,解决方案应该适用于任何进程,而不仅仅是此代码。覆盖sys / os模块并使用LD_PRELOAD是克服挑战的非常有趣的方法,但它们无法解决问题的核心:)

5 个答案:

答案 0 :(得分:21)

喜欢这个吗?

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

答案 1 :(得分:5)

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

以下是一个示例,您可以将stderr 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 +++ fd2区分开来:

standard xxx data

在上面的示例中,您会看到每个strace加倍,因为您无法重定向stdout / stderr。但是,您可以要求stdout将其输出保存到文件中。

从理论上讲,如果stderr#!/bin/bash if [ "$RACK_ENV" == "production" ]; then echo Using qgsocksify to wrap Puma bin/qgsocksify bundle exec puma -C config/puma.rb else echo Using straight Puma bundle exec puma -C config/puma.rb fi 引用同一个终端,则只能在进程的上下文中区分2,或者在用户模式下(LD_PRELOAD),或内核空间(strace工具使用的ptrace接口)。一旦数据到达实际设备,真实的伪,就会失去区别。

答案 2 :(得分:1)

您始终可以分配Pseudo-TTY,这是screen的作用。

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

访问它

这个&#34;主人&#34;代码通过了你的测试:

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,你的&#34;奴隶&#34;过程将看到不同的PYT名称:

inp = pty.openpty()
oup = pty.openpty()
erp = pty.openpty()

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

答案 3 :(得分:0)

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

$ cat stderr
standard error data

由于此脚本会导入os模块,因此我在os中创建了自己的/tmp/python模块,并将/tmp/python添加到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

答案 4 :(得分:0)

何时可以使用脚本命令:

$ script --return -c "[executable string]" >stdout 2>stderr