Подтвердить что ты не робот

Можете ли вы обмануть isatty AND log stdout и stderr отдельно?

Проблема

Итак, вы хотите зарегистрировать stdout и stderr (отдельно) процесса или подпроцесса, без вывода отличается от того, что вы увидите в терминале, если вы ничего не записывали.

Кажется, довольно просто нет? К сожалению, похоже, что может быть невозможно написать общее решение для этой проблемы, которое работает на любом заданном процессе...

Фон

Перенаправление труб является одним из способов разделения stdout и stderr, позволяя вам регистрировать их по отдельности. К сожалению, если вы измените stdout/err на канал, процесс может обнаружить, что канал не является tty (потому что он не имеет ширины/высоты, скорости передачи и т.д.) И может соответствующим образом изменять его поведение. Зачем менять поведение? Ну, некоторые разработчики используют функции терминала, которые не имеют смысла, если вы пишете файл. Например, загрузочные столбцы часто требуют, чтобы курсор терминала был перемещен назад в начало строки, а предыдущая загрузочная панель была перезаписана полосой новой длины. Также цвет и вес шрифта могут отображаться в терминале, но в плоском ASCII файле они не могут. Если вы хотите написать такую ​​программу stdout непосредственно в файл, этот вывод будет содержать все escape-коды терминала ANSI, а не правильно отформатированный вывод. Поэтому разработчик реализует какую-то проверку "isatty" перед записью чего-либо в stdout/err, поэтому он может давать более простой вывод для файлов, если эта проверка возвращает false.

Обычное решение состоит в том, чтобы обмануть такие программы, считая, что трубы фактически являются ttys, используя pty - двунаправленный канал, который также имеет ширину, высоту и т.д. Вы перенаправляете все входы/выходы процесса на этот pty и это заставляет процесс думать о своем разговоре с реальным терминалом (и вы можете его напрямую записывать в файл). Единственная проблема заключается в том, что, используя один pty для stdout и stderr, теперь мы больше не можем различать два.

Итак, вы можете попробовать другой pty для каждого канала - один для stdin, один для stdout и один для stderr. Хотя это будет работать в 50% случаев, многие процессы, к сожалению, выполняют дополнительные проверки перенаправления, которые гарантируют, что выходной путь stdout и stderr (/dev/tty000x) будут одинаковыми. Если это не так, должно быть перенаправление, поэтому они дают вам такое же поведение, как если бы вы передавали stderr и stdout без pty.

Вы можете подумать, что эта чрезмерная проверка перенаправления необычна, но, к сожалению, она довольно распространена, потому что многие программы повторно используют другой код для проверки, например этот бит кода, найденный в OSX:

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

Вызов

Я думаю, что лучший способ найти решение - это вызов. Если кто-то может запустить следующий script (в идеале через 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 - очень интересные способы преодолеть эту проблему, но они не решают проблему:)

4b9b3361

Ответ 1

Как это?

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

Ответ 2

Для более простого использования (например, для тестирования разработки) используйте strace (linux) или dtruss (OSX). Конечно, это не будет работать в привилегированном процессе.

Здесь образец, вы можете отличить 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 +++

В приведенном выше примере вы видите, что каждый standard xxx data удваивается, потому что вы не можете перенаправлять stdout/stderr. Однако вы можете запросить strace сохранить его вывод в файл.

С теоретической стороны, если stdout и stderr относятся к одному и тому же терминалу, вы можете различать только два, пока все еще в контексте вашего процесса, либо в пользовательском режиме (LD_PRELOAD), либо в пространстве ядра ( интерфейс ptrace, который использует инструмент strace). Когда данные попадают на фактическое устройство, реальное псевдо, различие теряется.

Ответ 3

Вы всегда можете выделить Pseudo-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, ваш "подчиненный" процесс увидит разные имена PYT:

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

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

Ответ 4

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

$ cat stderr
standard error data

Поскольку этот script импортирует модуль 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