diff options
Diffstat (limited to 'src/shared/ptyfwd.c')
-rw-r--r-- | src/shared/ptyfwd.c | 516 |
1 files changed, 472 insertions, 44 deletions
diff --git a/src/shared/ptyfwd.c b/src/shared/ptyfwd.c index 195e603..998ce96 100644 --- a/src/shared/ptyfwd.c +++ b/src/shared/ptyfwd.c @@ -18,13 +18,28 @@ #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_OSC_SEQUENCE, + ANSI_COLOR_STATE_NEWLINE, + ANSI_COLOR_STATE_CARRIAGE_RETURN, + _ANSI_COLOR_STATE_MAX, + _ANSI_COLOR_STATE_INVALID = -EINVAL, +} AnsiColorState; + struct PTYForward { sd_event *event; @@ -65,7 +80,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; @@ -73,6 +89,14 @@ struct PTYForward { PTYForwardHandler handler; void *userdata; + + char *background_color; + AnsiColorState ansi_color_state; + char *csi_sequence; + char *osc_sequence; + + char *title; /* Window title to show by default */ + char *title_prefix; /* If terminal client overrides window title, prefix this string */ }; #define ESCAPE_USEC (1*USEC_PER_SEC) @@ -95,6 +119,14 @@ static void pty_forward_disconnect(PTYForward *f) { /* STDIN/STDOUT should not be non-blocking normally, so let's reset it */ (void) fd_nonblock(f->output_fd, false); + + if (colors_enabled()) { + (void) loop_write(f->output_fd, ANSI_NORMAL ANSI_ERASE_TO_END_OF_SCREEN, SIZE_MAX); + + if (f->title) + (void) loop_write(f->output_fd, ANSI_WINDOW_TITLE_POP, SIZE_MAX); + } + if (f->close_output_fd) f->output_fd = safe_close(f->output_fd); } @@ -109,6 +141,15 @@ 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->osc_sequence = mfree(f->osc_sequence); + f->ansi_color_state = _ANSI_COLOR_STATE_INVALID; } static int pty_forward_done(PTYForward *f, int rcode) { @@ -142,7 +183,7 @@ static bool look_for_escape(PTYForward *f, const char *buffer, size_t n) { if (*p == 0x1D) { usec_t nw = now(CLOCK_MONOTONIC); - if (f->escape_counter == 0 || nw > f->escape_timestamp + ESCAPE_USEC) { + if (f->escape_counter == 0 || nw > f->escape_timestamp + ESCAPE_USEC) { f->escape_timestamp = nw; f->escape_counter = 1; } else { @@ -196,11 +237,321 @@ static bool drained(PTYForward *f) { return true; } -static int shovel(PTYForward *f) { +static char *background_color_sequence(PTYForward *f) { + assert(f); + assert(f->background_color); + + return strjoin("\x1B[", f->background_color, "m"); +} + +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_background_color(PTYForward *f, size_t offset) { + _cleanup_free_ char *s = NULL; + + assert(f); + + if (!f->background_color) + return 0; + + 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); + + if (!f->background_color) + return 0; + + 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 insert_window_title_fix(PTYForward *f, size_t offset) { + assert(f); + + if (!f->title_prefix) + return 0; + + if (!f->osc_sequence) + return 0; + + const char *t = startswith(f->osc_sequence, "0;"); /* Set window title OSC sequence*/ + if (!t) + return 0; + + _cleanup_free_ char *joined = strjoin("\x1b]0;", f->title_prefix, t, "\a"); + if (!joined) + return -ENOMEM; + + return insert_string(f, offset, joined); +} + +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 && !f->title_prefix) + return 0; + + if (FLAGS_SET(f->flags, PTY_FORWARD_DUMB_TERMINAL)) + 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: + break; + + case ANSI_COLOR_STATE_NEWLINE: + case ANSI_COLOR_STATE_CARRIAGE_RETURN: + /* Immediately after a newline (ASCII 10) or carriage return (ASCII 13) insert an + * ANSI sequence set the background color back. */ + + r = insert_background_color(f, i); + if (r < 0) + return r; + + i += r; + break; + + case ANSI_COLOR_STATE_ESC: + + if (c == '[') { + f->ansi_color_state = ANSI_COLOR_STATE_CSI_SEQUENCE; + continue; + } else if (c == ']') { + f->ansi_color_state = ANSI_COLOR_STATE_OSC_SEQUENCE; + continue; + } + 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); + break; + } 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; + } + continue; + + case ANSI_COLOR_STATE_OSC_SEQUENCE: + + if ((uint8_t) c >= ' ') { + if (strlen_ptr(f->osc_sequence) >= 64) { + /* Safety check: lets not accept unbounded OSC sequences */ + f->osc_sequence = mfree(f->osc_sequence); + break; + } else if (!strextend(&f->osc_sequence, CHAR_TO_STR(c))) + return -ENOMEM; + } else { + /* Otherwise, the OSC sequence is over + * + * There are two allowed ways to end an OSC sequence: + * BEL '\x07' + * String Terminator (ST): <Esc>\ - "\x1b\x5c" + * since we cannot lookahead to see if the Esc is followed by a \ + * we cut a corner here and assume it will be \. */ + + if (IN_SET(c, '\x07', '\x1b')) { + r = insert_window_title_fix(f, i+1); + if (r < 0) + return r; + + i += r; + } + + f->osc_sequence = mfree(f->osc_sequence); + f->ansi_color_state = ANSI_COLOR_STATE_TEXT; + } + continue; + + default: + assert_not_reached(); + } + + if (c == '\n') + f->ansi_color_state = ANSI_COLOR_STATE_NEWLINE; + else if (c == '\r') + f->ansi_color_state = ANSI_COLOR_STATE_CARRIAGE_RETURN; + else if (c == 0x1B) /* ESC */ + f->ansi_color_state = ANSI_COLOR_STATE_ESC; + else + f->ansi_color_state = ANSI_COLOR_STATE_TEXT; + } + + return 0; +} + +static int do_shovel(PTYForward *f) { ssize_t k; + int r; assert(f); + if (f->out_buffer_size == 0 && !FLAGS_SET(f->flags, PTY_FORWARD_DUMB_TERMINAL)) { + /* If the output hasn't been allocated yet, we are at the beginning of the first + * shovelling. Hence, possibly send some initial ANSI sequences. But do so only if we are + * talking to an actual TTY. */ + + if (f->background_color) { + /* Erase the first line when we start */ + f->out_buffer = background_color_sequence(f); + if (!f->out_buffer) + return log_oom(); + + if (!strextend(&f->out_buffer, ANSI_ERASE_TO_END_OF_LINE)) + return log_oom(); + } + + if (f->title) { + if (!strextend(&f->out_buffer, + ANSI_WINDOW_TITLE_PUSH + "\x1b]2;", f->title, "\a")) + return log_oom(); + } + + if (f->out_buffer) { + 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 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) || (f->master_readable && f->out_buffer_full <= 0) || @@ -218,21 +569,19 @@ static int shovel(PTYForward *f) { f->stdin_hangup = true; f->stdin_event_source = sd_event_source_unref(f->stdin_event_source); - } else { - log_error_errno(errno, "read(): %m"); - return pty_forward_done(f, -errno); - } + } else + return log_error_errno(errno, "Failed to read from pty input fd: %m"); } else if (k == 0) { /* EOF on stdin */ f->stdin_readable = false; f->stdin_hangup = true; f->stdin_event_source = sd_event_source_unref(f->stdin_event_source); - } else { + } else { /* Check if ^] has been pressed three times within one second. If we get this we quite * immediately. */ if (look_for_escape(f, f->in_buffer + f->in_buffer_full, k)) - return pty_forward_done(f, -ECANCELED); + return -ECANCELED; f->in_buffer_full += (size_t) k; } @@ -250,10 +599,8 @@ static int shovel(PTYForward *f) { f->master_hangup = true; f->master_event_source = sd_event_source_unref(f->master_event_source); - } else { - log_error_errno(errno, "write(): %m"); - return pty_forward_done(f, -errno); - } + } else + return log_error_errno(errno, "write(): %m"); } else { assert(f->in_buffer_full >= (size_t) k); memmove(f->in_buffer, f->in_buffer + k, f->in_buffer_full - k); @@ -261,17 +608,14 @@ 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 - * temporary closing of everything on - * the other side, we treat it like - * EAGAIN here and try again, unless - * ignore_vhangup is off. */ + /* Note that EIO on the master device might be caused by vhangup() or + * temporary closing of everything on the other side, we treat it like EAGAIN + * here and try again, unless ignore_vhangup is off. */ if (errno == EAGAIN || (errno == EIO && ignore_vhangup(f))) f->master_readable = false; @@ -280,13 +624,16 @@ static int shovel(PTYForward *f) { f->master_hangup = true; f->master_event_source = sd_event_source_unref(f->master_event_source); - } else { - log_error_errno(errno, "read(): %m"); - return pty_forward_done(f, -errno); - } - } else { + } else + return log_error_errno(errno, "Failed to read from pty master fd: %m"); + } 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 log_error_errno(r, "Failed to scan for ANSI sequences: %m"); } } @@ -301,10 +648,8 @@ static int shovel(PTYForward *f) { f->stdout_writable = false; f->stdout_hangup = true; f->stdout_event_source = sd_event_source_unref(f->stdout_event_source); - } else { - log_error_errno(errno, "write(): %m"); - return pty_forward_done(f, -errno); - } + } else + return log_error_errno(errno, "Failed to write to pty output fd: %m"); } else { @@ -337,6 +682,18 @@ static int shovel(PTYForward *f) { return 0; } +static int shovel(PTYForward *f) { + int r; + + assert(f); + + r = do_shovel(f); + if (r < 0) + return pty_forward_done(f, r); + + return r; +} + static int on_master_event(sd_event_source *e, int fd, uint32_t revents, void *userdata) { PTYForward *f = ASSERT_PTR(userdata); @@ -406,11 +763,14 @@ int pty_forward_new( struct winsize ws; int r; + assert(master >= 0); + assert(ret); + f = new(PTYForward, 1); if (!f) return -ENOMEM; - *f = (struct PTYForward) { + *f = (PTYForward) { .flags = flags, .master = -EBADF, .input_fd = -EBADF, @@ -430,13 +790,17 @@ int pty_forward_new( else { /* If we shall be invoked in interactive mode, let's switch on non-blocking mode, so that we * never end up staving one direction while we block on the other. However, let's be careful - * here and not turn on O_NONBLOCK for stdin/stdout directly, but of re-opened copies of + * here and not turn on O_NONBLOCK for stdin/stdout directly, but of reopened copies of * them. This has two advantages: when we are killed abruptly the stdin/stdout fds won't be * left in O_NONBLOCK state for the next process using them. In addition, if some process * running in the background wants to continue writing to our stdout it can do so without - * being confused by O_NONBLOCK. */ + * being confused by O_NONBLOCK. + * We keep O_APPEND (if present) on the output FD and (try to) keep current file position on + * both input and output FD (principle of least surprise). + */ - f->input_fd = fd_reopen(STDIN_FILENO, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + f->input_fd = fd_reopen_propagate_append_and_position( + STDIN_FILENO, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); if (f->input_fd < 0) { /* Handle failures gracefully, after all certain fd types cannot be reopened * (sockets, …) */ @@ -450,7 +814,8 @@ int pty_forward_new( } else f->close_input_fd = true; - f->output_fd = fd_reopen(STDOUT_FILENO, O_WRONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + f->output_fd = fd_reopen_propagate_append_and_position( + STDOUT_FILENO, O_WRONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); if (f->output_fd < 0) { log_debug_errno(f->output_fd, "Failed to reopen stdout, using original fd: %m"); @@ -469,6 +834,10 @@ int pty_forward_new( f->master = master; + /* Disable color/window title setting unless we talk to a good TTY */ + if (!isatty_safe(f->output_fd) || get_color_mode() == COLOR_OFF) + f->flags |= PTY_FORWARD_DUMB_TERMINAL; + if (ioctl(f->output_fd, TIOCGWINSZ, &ws) < 0) /* If we can't get the resolution from the output fd, then use our internal, regular width/height, * i.e. something derived from $COLUMNS and $LINES if set. */ @@ -479,9 +848,16 @@ int pty_forward_new( (void) ioctl(master, TIOCSWINSZ, &ws); - if (!(flags & PTY_FORWARD_READ_ONLY)) { + if (!FLAGS_SET(flags, PTY_FORWARD_READ_ONLY)) { + bool same; + assert(f->input_fd >= 0); + r = fd_inode_same(f->input_fd, f->output_fd); + if (r < 0) + return r; + same = r > 0; + if (tcgetattr(f->input_fd, &f->saved_stdin_attr) >= 0) { struct termios raw_stdin_attr; @@ -489,11 +865,14 @@ int pty_forward_new( raw_stdin_attr = f->saved_stdin_attr; cfmakeraw(&raw_stdin_attr); - raw_stdin_attr.c_oflag = f->saved_stdin_attr.c_oflag; - tcsetattr(f->input_fd, TCSANOW, &raw_stdin_attr); + + if (!same) + raw_stdin_attr.c_oflag = f->saved_stdin_attr.c_oflag; + + (void) tcsetattr(f->input_fd, TCSANOW, &raw_stdin_attr); } - if (tcgetattr(f->output_fd, &f->saved_stdout_attr) >= 0) { + if (!same && tcgetattr(f->output_fd, &f->saved_stdout_attr) >= 0) { struct termios raw_stdout_attr; f->saved_stdout = true; @@ -502,7 +881,7 @@ int pty_forward_new( cfmakeraw(&raw_stdout_attr); raw_stdout_attr.c_iflag = f->saved_stdout_attr.c_iflag; raw_stdout_attr.c_lflag = f->saved_stdout_attr.c_lflag; - tcsetattr(f->output_fd, TCSANOW, &raw_stdout_attr); + (void) tcsetattr(f->output_fd, TCSANOW, &raw_stdout_attr); } r = sd_event_add_io(f->event, &f->stdin_event_source, f->input_fd, EPOLLIN|EPOLLET, on_stdin_event, f); @@ -540,7 +919,14 @@ int pty_forward_new( } PTYForward *pty_forward_free(PTYForward *f) { + if (!f) + return NULL; + pty_forward_disconnect(f); + free(f->background_color); + free(f->title); + free(f->title_prefix); + return mfree(f); } @@ -560,15 +946,14 @@ int pty_forward_set_ignore_vhangup(PTYForward *f, bool b) { assert(f); - if (!!(f->flags & PTY_FORWARD_IGNORE_VHANGUP) == b) + if (FLAGS_SET(f->flags, PTY_FORWARD_IGNORE_VHANGUP) == b) return 0; SET_FLAG(f->flags, PTY_FORWARD_IGNORE_VHANGUP, b); if (!ignore_vhangup(f)) { - /* We shall now react to vhangup()s? Let's check - * immediately if we might be in one */ + /* We shall now react to vhangup()s? Let's check immediately if we might be in one. */ f->master_readable = true; r = shovel(f); @@ -582,7 +967,7 @@ int pty_forward_set_ignore_vhangup(PTYForward *f, bool b) { bool pty_forward_get_ignore_vhangup(PTYForward *f) { assert(f); - return !!(f->flags & PTY_FORWARD_IGNORE_VHANGUP); + return FLAGS_SET(f->flags, PTY_FORWARD_IGNORE_VHANGUP); } bool pty_forward_is_done(PTYForward *f) { @@ -614,6 +999,7 @@ bool pty_forward_drain(PTYForward *f) { int pty_forward_set_priority(PTYForward *f, int64_t priority) { int r; + assert(f); if (f->stdin_event_source) { @@ -675,3 +1061,45 @@ 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); +} + +int pty_forward_set_title(PTYForward *f, const char *title) { + assert(f); + + /* Refuse accepting a title when we already started shoveling */ + if (f->out_buffer_size > 0) + return -EBUSY; + + return free_and_strdup(&f->title, title); +} + +int pty_forward_set_titlef(PTYForward *f, const char *format, ...) { + _cleanup_free_ char *title = NULL; + va_list ap; + int r; + + assert(f); + assert(format); + + if (f->out_buffer_size > 0) + return -EBUSY; + + va_start(ap, format); + r = vasprintf(&title, format, ap); + va_end(ap); + if (r < 0) + return -ENOMEM; + + return free_and_replace(f->title, title); +} + +int pty_forward_set_title_prefix(PTYForward *f, const char *title_prefix) { + assert(f); + + return free_and_strdup(&f->title_prefix, title_prefix); +} |