diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 51b3a83d29..c1989cf0b0 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -48,6 +48,9 @@ "\033?12l" /* reset cursor blinking */ \ "\033 1q" /* reset cursor style */ +/* How much to wait for a reply to a terminal sequence */ +#define CONSOLE_REPLY_WAIT_USEC (333 * USEC_PER_MSEC) + static volatile unsigned cached_columns = 0; static volatile unsigned cached_lines = 0; @@ -1243,10 +1246,6 @@ bool tty_is_vc_resolve(const char *tty) { return tty_is_vc(tty); } -const char* default_term_for_tty(const char *tty) { - return tty && tty_is_vc_resolve(tty) ? "linux" : "vt220"; -} - int fd_columns(int fd) { struct winsize ws = {}; @@ -2128,7 +2127,7 @@ int get_default_background_color(double *ret_red, double *ret_green, double *ret if (r < 0) goto finish; - usec_t end = usec_add(now(CLOCK_MONOTONIC), 333 * USEC_PER_MSEC); + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); char buf[STRLEN(ANSI_OSC "11;rgb:0/0/0" ANSI_ST)]; /* shortest possible reply */ size_t buf_full = 0; BackgroundColorContext context = {}; @@ -2331,7 +2330,7 @@ int terminal_get_size_by_dsr( if (r < 0) goto finish; - usec_t end = usec_add(now(CLOCK_MONOTONIC), 333 * USEC_PER_MSEC); + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); char buf[STRLEN("\x1B[1;1R")]; /* The shortest valid reply possible */ size_t buf_full = 0; CursorPositionContext context = {}; @@ -2460,6 +2459,178 @@ int terminal_fix_size(int input_fd, int output_fd) { return 1; } +#define MAX_TERMINFO_LENGTH 64 +/* python -c 'print("".join(hex(ord(i))[2:] for i in "name").upper())' */ +#define DCS_TERMINFO_Q ANSI_DCS "+q" "6E616D65" ANSI_ST +/* The answer is either 0+r… (invalid) or 1+r… (OK). */ +#define DCS_TERMINFO_R0 ANSI_DCS "0+r" ANSI_ST +#define DCS_TERMINFO_R1 ANSI_DCS "1+r" "6E616D65" "=" /* This is followed by Pt ST. */ +assert_cc(STRLEN(DCS_TERMINFO_R0) <= STRLEN(DCS_TERMINFO_R1 ANSI_ST)); + +static int scan_terminfo_response( + const char *buf, + size_t size, + char **ret_name) { + int r; + + assert(buf); + assert(ret_name); + + /* Check if we have enough space for the shortest possible answer. */ + if (size < STRLEN(DCS_TERMINFO_R0)) + return -EAGAIN; + + /* Check if the terminating sequence is present */ + if (memcmp(buf + size - STRLEN(ANSI_ST), ANSI_ST, STRLEN(ANSI_ST)) != 0) + return -EAGAIN; + + if (size <= STRLEN(DCS_TERMINFO_R1 ANSI_ST)) + return -EINVAL; /* The answer is invalid or empty */ + + if (memcmp(buf, DCS_TERMINFO_R1, STRLEN(DCS_TERMINFO_R1)) != 0) + return -EINVAL; /* The answer is not valid */ + + _cleanup_free_ void *dec = NULL; + size_t dec_size; + r = unhexmem_full(buf + STRLEN(DCS_TERMINFO_R1), size - STRLEN(DCS_TERMINFO_R1 ANSI_ST), + /* secure= */ false, + &dec, &dec_size); + if (r < 0) + return r; + + assert(((const char *) dec)[dec_size] == '\0'); /* unhexmem appends NUL for our convenience */ + if (memchr(dec, '\0', dec_size) || string_has_cc(dec, NULL) || !filename_is_valid(dec)) + return -EUCLEAN; + + *ret_name = TAKE_PTR(dec); + return 0; +} + +int terminal_get_terminfo_by_dcs(int fd, char **ret_name) { + int r; + + assert(fd >= 0); + assert(ret_name); + + /* Note: fd must be in non-blocking read-write mode! */ + + struct termios old_termios; + if (tcgetattr(fd, &old_termios) < 0) + return -errno; + + struct termios new_termios = old_termios; + termios_disable_echo(&new_termios); + + if (tcsetattr(fd, TCSADRAIN, &new_termios) < 0) + return -errno; + + r = loop_write(fd, DCS_TERMINFO_Q, SIZE_MAX); + if (r < 0) + goto finish; + + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); + char buf[STRLEN(DCS_TERMINFO_R1) + MAX_TERMINFO_LENGTH + STRLEN(ANSI_ST)]; + size_t bytes = 0; + + for (;;) { + usec_t n = now(CLOCK_MONOTONIC); + if (n >= end) { + r = -EOPNOTSUPP; + break; + } + + r = fd_wait_for_event(fd, POLLIN, usec_sub_unsigned(end, n)); + if (r < 0) + break; + if (r == 0) { + r = -EOPNOTSUPP; + break; + } + + /* On the first read, read multiple characters, i.e. the shortest valid reply. Afterwards + * read byte by byte, since we don't want to read too much and drop characters from the input + * queue. */ + ssize_t l = read(fd, buf + bytes, bytes == 0 ? STRLEN(DCS_TERMINFO_R0) : 1); + if (l < 0) { + if (errno == EAGAIN) + continue; + r = -errno; + break; + } + + assert((size_t) l <= sizeof(buf) - bytes); + bytes += l; + + r = scan_terminfo_response(buf, bytes, ret_name); + if (r != -EAGAIN) + break; + + if (bytes == sizeof(buf)) { + r = -EOPNOTSUPP; /* The response has the right prefix, but we didn't find a valid + * answer with a terminator in the alloted space. Something is + * wrong, possibly some unrelated bytes got injected into the + * answer. */ + break; + } + } + +finish: + /* We ignore failure here. We already got a reply and if cleanup fails, we can't help that. */ + (void) tcsetattr(fd, TCSADRAIN, &old_termios); + return r; +} + +int have_terminfo_file(const char *name) { + /* This is a heuristic check if we have the file, using the directory layout used on + * current Linux systems. Checks for other layouts can be added later if appropriate. */ + int r; + + assert(filename_is_valid(name)); + + _cleanup_free_ char *p = path_join("/usr/share/terminfo", CHAR_TO_STR(name[0]), name); + if (!p) + return log_oom_debug(); + + r = RET_NERRNO(access(p, F_OK)); + if (r == -ENOENT) + return false; + if (r < 0) + return r; + return true; +} + +int query_term_for_tty(const char *tty, char **ret_term) { + _cleanup_free_ char *dcs_term = NULL; + int r; + + assert(tty); + assert(ret_term); + + if (tty_is_vc_resolve(tty)) + return strdup_to(ret_term, "linux"); + + /* Try to query the terminal implementation that we're on. This will not work in all + * cases, which is fine, since this is intended to be used as a fallback. */ + + _cleanup_close_ int tty_fd = open_terminal(tty, O_RDWR|O_NOCTTY|O_CLOEXEC|O_NONBLOCK); + if (tty_fd < 0) + return log_debug_errno(tty_fd, "Failed to open %s to query terminfo: %m", tty); + + r = terminal_get_terminfo_by_dcs(tty_fd, &dcs_term); + if (r < 0) + return log_debug_errno(r, "Failed to query %s for terminfo: %m", tty); + + r = have_terminfo_file(dcs_term); + if (r < 0) + return log_debug_errno(r, "Failed to look for terminfo %s: %m", dcs_term); + if (r == 0) + return log_info_errno(SYNTHETIC_ERRNO(ENODATA), + "Terminfo %s not found for %s.", dcs_term, tty); + + *ret_term = TAKE_PTR(dcs_term); + return 0; +} + int terminal_is_pty_fd(int fd) { int r; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 17dbdfdfd5..2869469353 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -19,16 +19,19 @@ #define ANSI_WINDOW_TITLE_PUSH "\x1b[22;2t" #define ANSI_WINDOW_TITLE_POP "\x1b[23;2t" -/* ANSI "string terminator" character ("ST"). Terminal emulators typically allow three different ones: 0x07, - * 0x9c, and 0x1B 0x5C. We'll avoid 0x07 (BEL, aka ^G) since it might trigger unexpected TTY signal - * handling. And we'll avoid 0x9c since that's also valid regular codepoint in UTF-8 and elsewhere, and - * creates ambiguities. Because of that some terminal emulators explicitly choose not to support it. Hence we - * use 0x1B 0x5c */ -#define ANSI_ST "\e\\" +/* The "device control string" ("DCS") start sequence */ +#define ANSI_DCS "\eP" /* The "operating system command" ("OSC") start sequence */ #define ANSI_OSC "\e]" +/* ANSI "string terminator" character ("ST"). Terminal emulators typically allow three different ones: 0x07, + * 0x9c, and 0x1B 0x5C. We'll avoid 0x07 (BEL, aka ^G) since it might trigger unexpected TTY signal handling. + * And we'll avoid 0x9c since that's also valid regular codepoint in UTF-8 and elsewhere, and creates + * ambiguities. Because of that some terminal emulators explicitly choose not to support it. Hence we use + * 0x1B 0x5c. */ +#define ANSI_ST "\e\\" + bool isatty_safe(int fd); typedef enum TerminalResetFlags { @@ -97,7 +100,6 @@ bool tty_is_vc(const char *tty); bool tty_is_vc_resolve(const char *tty); bool tty_is_console(const char *tty) _pure_; int vtnr_from_tty(const char *tty); -const char* default_term_for_tty(const char *tty); void reset_dev_console_fd(int fd, bool switch_to_text); int lock_dev_console(void); @@ -140,11 +142,17 @@ assert_cc((TTY_MODE & 0711) == 0600); void termios_disable_echo(struct termios *termios); +/* The $TERM value we use for terminals other than the Linux console */ +#define FALLBACK_TERM "vt220" + int get_default_background_color(double *ret_red, double *ret_green, double *ret_blue); int terminal_get_size_by_dsr(int input_fd, int output_fd, unsigned *ret_rows, unsigned *ret_columns); - int terminal_fix_size(int input_fd, int output_fd); +int terminal_get_terminfo_by_dcs(int fd, char **ret_name); +int have_terminfo_file(const char *name); +int query_term_for_tty(const char *tty, char **ret_term); + int terminal_is_pty_fd(int fd); int pty_open_peer(int fd, int mode); diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 9e946c6cbb..593c04ae5c 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -2038,7 +2038,7 @@ static int build_environment( } if (exec_context_needs_term(c)) { - _cleanup_free_ char *cmdline = NULL; + _cleanup_free_ char *cmdline = NULL, *dcs_term = NULL; const char *tty_path, *term = NULL; tty_path = exec_context_tty_path(c); @@ -2063,8 +2063,16 @@ static int build_environment( term = cmdline; } + if (!term && tty_path) { + /* This handles real virtual terminals (returning "linux") and + * any terminals which support the DCS +q query sequence. */ + r = query_term_for_tty(tty_path, &dcs_term); + if (r >= 0) + term = dcs_term; + } + if (!term) { - /* If no precise $TERM is known and we pick a fallback default, then let's also set + /* If $TERM is not known and we pick a fallback default, then let's also set * $COLORTERM=truecolor. That's because our fallback default is vt220, which is * generally a safe bet (as it supports PageUp/PageDown unlike vt100, and is quite * universally available in terminfo/termcap), except for the fact that real DEC @@ -2083,7 +2091,7 @@ static int build_environment( our_env[n_env++] = x; - term = default_term_for_tty(tty_path); + term = FALLBACK_TERM; } x = strjoin("TERM=", term); diff --git a/src/core/main.c b/src/core/main.c index 2005d733c6..b1f7cc941b 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -1594,8 +1594,10 @@ static int fixup_environment(void) { return r; } - const char *t = term ?: default_term_for_tty("/dev/console"); - if (setenv("TERM", t, /* overwrite= */ true) < 0) + if (!term) + (void) query_term_for_tty("/dev/console", &term); + + if (setenv("TERM", term ?: FALLBACK_TERM, /* overwrite= */ true) < 0) return -errno; /* The kernels sets HOME=/ for init. Let's undo this. */ diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index f128ddca1f..358d578ccb 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -16,6 +16,7 @@ #include "strv.h" #include "terminal-util.h" #include "tests.h" +#include "time-util.h" #include "tmpfile-util.h" #define LOREM_IPSUM "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " \ @@ -28,21 +29,6 @@ TEST(colors_enabled) { log_info("colors_enabled: %s", yes_no(colors_enabled())); } -TEST(default_term_for_tty) { - puts(default_term_for_tty("/dev/tty23")); - puts(default_term_for_tty("/dev/ttyS23")); - puts(default_term_for_tty("/dev/tty0")); - puts(default_term_for_tty("/dev/pty0")); - puts(default_term_for_tty("/dev/pts/0")); - puts(default_term_for_tty("/dev/console")); - puts(default_term_for_tty("tty23")); - puts(default_term_for_tty("ttyS23")); - puts(default_term_for_tty("tty0")); - puts(default_term_for_tty("pty0")); - puts(default_term_for_tty("pts/0")); - puts(default_term_for_tty("console")); -} - TEST(read_one_char) { _cleanup_fclose_ FILE *file = NULL; char r; @@ -175,7 +161,10 @@ TEST(get_default_background_color) { double red, green, blue; int r; + usec_t n = now(CLOCK_MONOTONIC); r = get_default_background_color(&red, &green, &blue); + log_info("%s took %s", __func__+5, + FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), n), USEC_PER_MSEC)); if (r < 0) log_notice_errno(r, "Can't get terminal default background color: %m"); else @@ -186,25 +175,31 @@ TEST(terminal_get_size_by_dsr) { unsigned rows, columns; int r; + usec_t n = now(CLOCK_MONOTONIC); r = terminal_get_size_by_dsr(STDIN_FILENO, STDOUT_FILENO, &rows, &columns); + log_info("%s took %s", __func__+5, + FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), n), USEC_PER_MSEC)); if (r < 0) - log_notice_errno(r, "Can't get screen dimensions via DSR: %m"); - else { - log_notice("terminal size via DSR: rows=%u columns=%u", rows, columns); + return (void) log_notice_errno(r, "Can't get screen dimensions via DSR: %m"); - struct winsize ws = {}; + log_notice("terminal size via DSR: rows=%u columns=%u", rows, columns); - if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) - log_warning_errno(errno, "Can't get terminal size via ioctl, ignoring: %m"); - else - log_notice("terminal size via ioctl: rows=%u columns=%u", ws.ws_row, ws.ws_col); - } + struct winsize ws = {}; + + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) + log_warning_errno(errno, "Can't get terminal size via ioctl, ignoring: %m"); + else + log_notice("terminal size via ioctl: rows=%u columns=%u", ws.ws_row, ws.ws_col); } TEST(terminal_fix_size) { int r; + usec_t n = now(CLOCK_MONOTONIC); + r = terminal_fix_size(STDIN_FILENO, STDOUT_FILENO); + log_info("%s took %s", __func__+5, + FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), n), USEC_PER_MSEC)); if (r < 0) log_warning_errno(r, "Failed to fix terminal size: %m"); else if (r == 0) @@ -213,6 +208,54 @@ TEST(terminal_fix_size) { log_notice("Fixed terminal size."); } +TEST(terminal_get_terminfo_by_dcs) { + _cleanup_free_ char *name = NULL; + int r; + + /* We need a non-blocking read-write fd. */ + _cleanup_close_ int fd = fd_reopen(STDIN_FILENO, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) + return (void) log_info_errno(fd, "Cannot reopen stdin in read-write mode: %m"); + + usec_t n = now(CLOCK_MONOTONIC); + + r = terminal_get_terminfo_by_dcs(fd, &name); + log_info("%s took %s", __func__+5, + FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), n), USEC_PER_MSEC)); + if (r < 0) + return (void) log_info_errno(r, "Can't get terminal terminfo via DCS: %m"); + log_info("terminal terminfo via DCS: %s, $TERM: %s", name, strnull(getenv("TERM"))); +} + +TEST(have_terminfo_file) { + int r; + + FOREACH_STRING(s, + "linux", + "xterm", + "vt220", + "xterm-256color", + "nosuchfile") { + r = have_terminfo_file(s); + log_info("%s: %s → %s", __func__+5, s, r >= 0 ? yes_no(r) : STRERROR(r)); + ASSERT_OK(r); + } +} + +TEST(query_term_for_tty) { + int r; + + FOREACH_STRING(s, + "/dev/console", + "/dev/stdin", + "/dev/stdout") { + _cleanup_free_ char *term = NULL; + + r = query_term_for_tty(s, &term); + log_info("%s: %s → %s/%s", __func__+5, s, STRERROR(r), strnull(term)); + } +} + TEST(terminal_is_pty_fd) { _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF; int r;