Kolorowanie danych wejściowych użytkownika jest trudne, ponieważ w połowie przypadków jest ono wysyłane przez sterownik terminalu (z lokalnym echem), więc w takim przypadku żadna aplikacja działająca w tym terminalu może nie wiedzieć, kiedy użytkownik zamierza wpisać tekst i odpowiednio zmienić kolor wyjściowy . Tylko pseudo-terminalowy sterownik (w jądrze) wie (emulator terminala (jak xterm) wysyła mu pewne znaki po pewnym naciśnięciu klawisza, a sterownik terminala może odsyłać niektóre znaki dla echa, ale xterm nie może wiedzieć, czy są one z lokalne echo lub dane wyjściowe aplikacji na stronie slave pseudo terminala).
A potem jest inny tryb, w którym sterownikowi terminalu mówi się, żeby nie echo, ale aplikacja tym razem coś wypisuje. Aplikacja (podobnie jak te używające readline, takie jak gdb, bash ...) może wysłać to na swoim standardzie lub stderr, co będzie trudne do odróżnienia od czegoś, co wyprowadza dla innych rzeczy niż echo danych wejściowych użytkownika.
Następnie, aby odróżnić stdout aplikacji od stderr, istnieje kilka podejść.
Wiele z nich polega na przekierowaniu poleceń stdout i stderr do potoków i potoków odczytanych przez aplikację w celu pokolorowania. Są z tym dwa problemy:
- Kiedy stdout nie jest już terminalem (jak potok zamiast), wiele aplikacji dostosowuje swoje zachowanie, aby rozpocząć buforowanie danych wyjściowych, co oznacza, że dane wyjściowe będą wyświetlane w dużych porcjach.
- Nawet jeśli jest to ten sam proces, który przetwarza dwie potoki, nie ma gwarancji, że kolejność tekstu napisanego przez aplikację na stdout i stderr zostanie zachowana, ponieważ proces odczytu nie może wiedzieć (czy jest coś do odczytania z obu) czy rozpocząć czytanie z rury „stdout” czy „stderr”.
Innym podejściem jest zmodyfikowanie aplikacji tak, aby kolorowała swoje standardowe i standardowe. Często nie jest to możliwe ani realistyczne.
Następnie sztuczką (w przypadku dynamicznie połączonych aplikacji) może być przejęcie (użycie $LD_PRELOADjak w odpowiedzi Illilla ) funkcji wyjściowych wywoływanych przez aplikację w celu wypisania czegoś i umieszczenia w nich kodu określającego kolor pierwszego planu na podstawie tego, czy mają one coś wypisać na stderr lub stdout. Oznacza to jednak przejęcie każdej możliwej funkcji z biblioteki C i dowolnej innej biblioteki wykonującej write(2)syscall bezpośrednio wywołanej przez aplikację, która może potencjalnie skończyć pisaniem czegoś na stdout lub stderr (printf, puts, perror ...), a nawet wtedy , co może zmienić jego zachowanie.
Innym podejściem może być użycie sztuczek PTRACE jako stracelub gdbzrobić, aby zaczepić się za każdym razem, gdy write(2)wywoływane jest wywołanie systemowe, i ustawić kolor wyjściowy na podstawie tego, czy write(2)jest on w deskryptorze pliku 1 czy 2.
Jest to jednak dość duża rzecz.
Sztuką, z którą właśnie bawiłem stracesię, jest przechwycenie samego siebie (co powoduje brudną robotę zaczepienia się przed każdym wywołaniem systemowym) za pomocą LD_PRELOAD, aby kazać mu zmienić kolor wyjściowy w zależności od tego, czy wykrył write(2)na fd 1 czy 2)
Patrząc na stracekod źródłowy, możemy zobaczyć, że wszystkie dane wyjściowe są wykonywane przez vfprintffunkcję. Wszystko, co musimy zrobić, to przejąć tę funkcję.
Opakowanie LD_PRELOAD wyglądałoby następująco:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
int vfprintf(FILE *outf, const char *fmt, va_list ap)
{
static int (*orig_vfprintf) (FILE*, const char *, va_list) = 0;
static int c = 0;
va_list ap_orig;
va_copy(ap_orig, ap);
if (!orig_vfprintf) {
orig_vfprintf = (int (*) (FILE*, const char *, va_list))
dlsym (RTLD_NEXT, "vfprintf");
}
if (strcmp(fmt, "%ld, ") == 0) {
int fd = va_arg(ap, long);
switch (fd) {
case 2:
write(2, "\e[31m", 5);
c = 1;
break;
case 1:
write(2, "\e[32m", 5);
c = 1;
break;
}
} else if (strcmp(fmt, ") ") == 0) {
if (c) write(2, "\e[m", 3);
c = 0;
}
return orig_vfprintf(outf, fmt, ap_orig);
}
Następnie kompilujemy to z:
cc -Wall -fpic -shared -o wrap.so wrap.c -ldl
I użyj go jako:
LD_PRELOAD=/path/to/wrap.so strace -qfo /dev/null -e write -s 0 env -u LD_PRELOAD some-cmd
Zauważysz, jak w przypadku wymiany some-cmdz bash, wierszu bash i co piszesz pojawia się na czerwono (stderr), natomiast z zshwydaje się na czarno (bo zsh dups stderr na nowy fd, aby wyświetlić jego szybka i echo).
Wygląda na to, że działa zaskakująco dobrze nawet w aplikacjach, których nie spodziewałbyś się (takich jak te, które używają kolorów).
Tryb kolorowania jest wyprowadzany na stracestderr, który jest uważany za terminal. Jeśli aplikacja przekieruje swoje standardowe wyjście lub standardowe polecenie, nasz przejęty ciąg będzie nadal zapisywał sekwencje specjalne koloru na terminalu.
To rozwiązanie ma swoje ograniczenia:
- Te związane z
strace: problemami z wydajnością, nie można uruchamiać innych poleceń PTRACE, takich jak stracelub gdbw nim, ani problemów z setuid / setgid
- Kolorystyka oparta na
writes na stdout / stderr każdego indywidualnego procesu. Tak na przykład, w sh -c 'echo error >&2', errorbyłby zielony, ponieważ echowyjścia na IT jej stdout (które sh przekierowany do stderr SH, ale wszystko strace widzi to write(1, "error\n", 6)). I in sh -c 'seq 1000000 | wc', seqrobi wiele lub więcej writedo swojego standardu , więc opakowanie ostatecznie wyśle wiele (niewidocznych) sekwencji ucieczki do terminala.