ptyfwd: optionally, change ANSI background color of forwarded terminals
As the bytes flow through our terminal forwarder we can color the background of the terminal with a color of our choices, if that's desired. This will later allow us to color the background of the uid0 tool when running as root with a slightly alarming red color. This does two things: 1. When an ANSI sequence is seen that resets the background color, it is extended to immediately set the color to our choice. 2. When a newline is seen it is immeidately extended to set the background color again and clear the current line till the end. Net effect: all lines written while we forward the ttys will be shown with the background color of choice.
This commit is contained in:
+274
-5
@@ -18,15 +18,26 @@
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "errno-util.h"
|
||||
#include "extract-word.h"
|
||||
#include "fd-util.h"
|
||||
#include "io-util.h"
|
||||
#include "log.h"
|
||||
#include "macro.h"
|
||||
#include "ptyfwd.h"
|
||||
#include "stat-util.h"
|
||||
#include "strv.h"
|
||||
#include "terminal-util.h"
|
||||
#include "time-util.h"
|
||||
|
||||
typedef enum AnsiColorState {
|
||||
ANSI_COLOR_STATE_TEXT,
|
||||
ANSI_COLOR_STATE_ESC,
|
||||
ANSI_COLOR_STATE_CSI_SEQUENCE,
|
||||
ANSI_COLOR_STATE_NEWLINE,
|
||||
_ANSI_COLOR_STATE_MAX,
|
||||
_ANSI_COLOR_STATE_INVALID = -EINVAL,
|
||||
} AnsiColorState;
|
||||
|
||||
struct PTYForward {
|
||||
sd_event *event;
|
||||
|
||||
@@ -67,7 +78,8 @@ struct PTYForward {
|
||||
bool last_char_set:1;
|
||||
char last_char;
|
||||
|
||||
char in_buffer[LINE_MAX], out_buffer[LINE_MAX];
|
||||
char in_buffer[LINE_MAX], *out_buffer;
|
||||
size_t out_buffer_size;
|
||||
size_t in_buffer_full, out_buffer_full;
|
||||
|
||||
usec_t escape_timestamp;
|
||||
@@ -75,6 +87,10 @@ struct PTYForward {
|
||||
|
||||
PTYForwardHandler handler;
|
||||
void *userdata;
|
||||
|
||||
char *background_color;
|
||||
AnsiColorState ansi_color_state;
|
||||
char *csi_sequence;
|
||||
};
|
||||
|
||||
#define ESCAPE_USEC (1*USEC_PER_SEC)
|
||||
@@ -115,6 +131,14 @@ static void pty_forward_disconnect(PTYForward *f) {
|
||||
}
|
||||
|
||||
f->saved_stdout = f->saved_stdin = false;
|
||||
|
||||
f->out_buffer = mfree(f->out_buffer);
|
||||
f->out_buffer_size = 0;
|
||||
f->out_buffer_full = 0;
|
||||
f->in_buffer_full = 0;
|
||||
|
||||
f->csi_sequence = mfree(f->csi_sequence);
|
||||
f->ansi_color_state = _ANSI_COLOR_STATE_INVALID;
|
||||
}
|
||||
|
||||
static int pty_forward_done(PTYForward *f, int rcode) {
|
||||
@@ -202,10 +226,243 @@ static bool drained(PTYForward *f) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static int shovel(PTYForward *f) {
|
||||
ssize_t k;
|
||||
static char *background_color_sequence(PTYForward *f) {
|
||||
assert(f);
|
||||
assert(f->background_color);
|
||||
|
||||
/* This sets the background color to the desired one, and erase the rest of the line with it */
|
||||
|
||||
return strjoin("\x1B[", f->background_color, "m", ANSI_ERASE_TO_END_OF_LINE);
|
||||
}
|
||||
|
||||
static int insert_string(PTYForward *f, size_t offset, const char *s) {
|
||||
assert(f);
|
||||
assert(offset <= f->out_buffer_full);
|
||||
assert(s);
|
||||
|
||||
size_t l = strlen(s);
|
||||
assert(l <= INT_MAX); /* Make sure we can still return this */
|
||||
|
||||
void *p = realloc(f->out_buffer, MAX(f->out_buffer_full + l, (size_t) LINE_MAX));
|
||||
if (!p)
|
||||
return -ENOMEM;
|
||||
|
||||
f->out_buffer = p;
|
||||
f->out_buffer_size = MALLOC_SIZEOF_SAFE(f->out_buffer);
|
||||
|
||||
memmove(f->out_buffer + offset + l, f->out_buffer + offset, f->out_buffer_full - offset);
|
||||
memcpy(f->out_buffer + offset, s, l);
|
||||
f->out_buffer_full += l;
|
||||
|
||||
return (int) l;
|
||||
}
|
||||
|
||||
static int insert_erase_newline(PTYForward *f, size_t offset) {
|
||||
_cleanup_free_ char *s = NULL;
|
||||
|
||||
assert(f);
|
||||
assert(f->background_color);
|
||||
|
||||
s = background_color_sequence(f);
|
||||
if (!s)
|
||||
return -ENOMEM;
|
||||
|
||||
return insert_string(f, offset, s);
|
||||
}
|
||||
|
||||
static int is_csi_background_reset_sequence(const char *seq) {
|
||||
enum {
|
||||
COLOR_TOKEN_NO,
|
||||
COLOR_TOKEN_START,
|
||||
COLOR_TOKEN_8BIT,
|
||||
COLOR_TOKEN_24BIT_R,
|
||||
COLOR_TOKEN_24BIT_G,
|
||||
COLOR_TOKEN_24BIT_B,
|
||||
} token_state = COLOR_TOKEN_NO;
|
||||
|
||||
bool b = false;
|
||||
int r;
|
||||
|
||||
assert(seq);
|
||||
|
||||
/* This parses CSI "m" sequences, and determines if they reset the background color. If so returns
|
||||
* 1. This can then be used to insert another sequence that sets the color to the desired
|
||||
* replacement. */
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *token = NULL;
|
||||
|
||||
r = extract_first_word(&seq, &token, ";", EXTRACT_RELAX|EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_RETAIN_ESCAPE);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
break;
|
||||
|
||||
switch (token_state) {
|
||||
|
||||
case COLOR_TOKEN_NO:
|
||||
|
||||
if (STR_IN_SET(token, "", "0", "00", "49"))
|
||||
b = true; /* These tokens set the background back to normal */
|
||||
else if (STR_IN_SET(token, "40", "41", "42", "43", "44", "45", "46", "47", "48"))
|
||||
b = false; /* And these tokens set them to something other than normal */
|
||||
|
||||
if (STR_IN_SET(token, "38", "48", "58"))
|
||||
token_state = COLOR_TOKEN_START; /* These tokens mean an 8bit or 24bit color will follow */
|
||||
break;
|
||||
|
||||
case COLOR_TOKEN_START:
|
||||
|
||||
if (STR_IN_SET(token, "5", "05"))
|
||||
token_state = COLOR_TOKEN_8BIT; /* 8bit color */
|
||||
else if (STR_IN_SET(token, "2", "02"))
|
||||
token_state = COLOR_TOKEN_24BIT_R; /* 24bit color */
|
||||
else
|
||||
token_state = COLOR_TOKEN_NO; /* something weird? */
|
||||
break;
|
||||
|
||||
case COLOR_TOKEN_24BIT_R:
|
||||
token_state = COLOR_TOKEN_24BIT_G;
|
||||
break;
|
||||
|
||||
case COLOR_TOKEN_24BIT_G:
|
||||
token_state = COLOR_TOKEN_24BIT_B;
|
||||
break;
|
||||
|
||||
case COLOR_TOKEN_8BIT:
|
||||
case COLOR_TOKEN_24BIT_B:
|
||||
token_state = COLOR_TOKEN_NO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
static int insert_background_fix(PTYForward *f, size_t offset) {
|
||||
assert(f);
|
||||
assert(f->background_color);
|
||||
|
||||
if (!is_csi_background_reset_sequence(strempty(f->csi_sequence)))
|
||||
return 0;
|
||||
|
||||
_cleanup_free_ char *s = NULL;
|
||||
s = strjoin(";", f->background_color);
|
||||
if (!s)
|
||||
return -ENOMEM;
|
||||
|
||||
return insert_string(f, offset, s);
|
||||
}
|
||||
|
||||
static int pty_forward_ansi_process(PTYForward *f, size_t offset) {
|
||||
int r;
|
||||
|
||||
assert(f);
|
||||
assert(offset <= f->out_buffer_full);
|
||||
|
||||
if (!f->background_color)
|
||||
return 0;
|
||||
|
||||
for (size_t i = offset; i < f->out_buffer_full; i++) {
|
||||
char c = f->out_buffer[i];
|
||||
|
||||
switch (f->ansi_color_state) {
|
||||
|
||||
case ANSI_COLOR_STATE_TEXT:
|
||||
if (c == '\n')
|
||||
f->ansi_color_state = ANSI_COLOR_STATE_NEWLINE;
|
||||
if (c == 0x1B) /* ESC */
|
||||
f->ansi_color_state = ANSI_COLOR_STATE_ESC;
|
||||
break;
|
||||
|
||||
case ANSI_COLOR_STATE_NEWLINE: {
|
||||
/* Immediately after a newline insert an ANSI sequence to erase the line with a background color */
|
||||
|
||||
r = insert_erase_newline(f, i);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
i += r;
|
||||
|
||||
f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
|
||||
break;
|
||||
}
|
||||
|
||||
case ANSI_COLOR_STATE_ESC: {
|
||||
|
||||
if (c == '[')
|
||||
f->ansi_color_state = ANSI_COLOR_STATE_CSI_SEQUENCE;
|
||||
else
|
||||
f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ANSI_COLOR_STATE_CSI_SEQUENCE: {
|
||||
|
||||
if (c >= 0x20 && c <= 0x3F) {
|
||||
/* If this is a "parameter" or "intermediary" byte (i.e. ranges 0x20…0x2F and
|
||||
* 0x30…0x3F) then we are still in the CSI sequence */
|
||||
|
||||
if (strlen_ptr(f->csi_sequence) >= 64) {
|
||||
/* Safety check: lets not accept unbounded CSI sequences */
|
||||
|
||||
f->csi_sequence = mfree(f->csi_sequence);
|
||||
f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
|
||||
} else if (!strextend(&f->csi_sequence, CHAR_TO_STR(c)))
|
||||
return -ENOMEM;
|
||||
} else {
|
||||
/* Otherwise, the CSI sequence is over */
|
||||
|
||||
if (c == 'm') {
|
||||
/* This is an "SGR" (Select Graphic Rendition) sequence. Patch in our background color. */
|
||||
r = insert_background_fix(f, i);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
i += r;
|
||||
}
|
||||
|
||||
f->csi_sequence = mfree(f->csi_sequence);
|
||||
f->ansi_color_state = ANSI_COLOR_STATE_TEXT;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int shovel(PTYForward *f) {
|
||||
ssize_t k;
|
||||
int r;
|
||||
|
||||
assert(f);
|
||||
|
||||
if (f->out_buffer_size == 0 && f->background_color) {
|
||||
/* Erase the first line when we start */
|
||||
f->out_buffer = background_color_sequence(f);
|
||||
if (!f->out_buffer)
|
||||
return pty_forward_done(f, log_oom());
|
||||
|
||||
f->out_buffer_full = strlen(f->out_buffer);
|
||||
f->out_buffer_size = MALLOC_SIZEOF_SAFE(f->out_buffer);
|
||||
}
|
||||
|
||||
if (f->out_buffer_size < LINE_MAX) {
|
||||
/* Make sure we always have room for at least one "line" */
|
||||
void *p = realloc(f->out_buffer, LINE_MAX);
|
||||
if (!p)
|
||||
return pty_forward_done(f, log_oom());
|
||||
|
||||
f->out_buffer = p;
|
||||
f->out_buffer_size = MALLOC_SIZEOF_SAFE(p);
|
||||
}
|
||||
|
||||
while ((f->stdin_readable && f->in_buffer_full <= 0) ||
|
||||
(f->master_writable && f->in_buffer_full > 0) ||
|
||||
@@ -267,9 +524,9 @@ static int shovel(PTYForward *f) {
|
||||
}
|
||||
}
|
||||
|
||||
if (f->master_readable && f->out_buffer_full < LINE_MAX) {
|
||||
if (f->master_readable && f->out_buffer_full < MIN(f->out_buffer_size, (size_t) LINE_MAX)) {
|
||||
|
||||
k = read(f->master, f->out_buffer + f->out_buffer_full, LINE_MAX - f->out_buffer_full);
|
||||
k = read(f->master, f->out_buffer + f->out_buffer_full, f->out_buffer_size - f->out_buffer_full);
|
||||
if (k < 0) {
|
||||
|
||||
/* Note that EIO on the master device might be caused by vhangup() or
|
||||
@@ -289,7 +546,12 @@ static int shovel(PTYForward *f) {
|
||||
}
|
||||
} else {
|
||||
f->read_from_master = true;
|
||||
size_t scan_index = f->out_buffer_full;
|
||||
f->out_buffer_full += (size_t) k;
|
||||
|
||||
r = pty_forward_ansi_process(f, scan_index);
|
||||
if (r < 0)
|
||||
return pty_forward_done(f, log_error_errno(r, "Failed to scan for ANSI sequences: %m"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +817,7 @@ PTYForward *pty_forward_free(PTYForward *f) {
|
||||
if (!f)
|
||||
return NULL;
|
||||
pty_forward_disconnect(f);
|
||||
free(f->background_color);
|
||||
return mfree(f);
|
||||
}
|
||||
|
||||
@@ -689,3 +952,9 @@ int pty_forward_set_width_height(PTYForward *f, unsigned width, unsigned height)
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pty_forward_set_background_color(PTYForward *f, const char *color) {
|
||||
assert(f);
|
||||
|
||||
return free_and_strdup(&f->background_color, color);
|
||||
}
|
||||
|
||||
@@ -39,4 +39,6 @@ int pty_forward_set_priority(PTYForward *f, int64_t priority);
|
||||
|
||||
int pty_forward_set_width_height(PTYForward *f, unsigned width, unsigned height);
|
||||
|
||||
int pty_forward_set_background_color(PTYForward *f, const char *color);
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(PTYForward*, pty_forward_free);
|
||||
|
||||
Reference in New Issue
Block a user