mirror of
https://github.com/morgan9e/systemd
synced 2026-04-15 00:47:10 +09:00
Use DCS sequence to query terminal name and set $TERM automatically (#37538)
This code seems to work quickly and nicely for a bunch of modern terminals. Setting $TERM automatically removes an common annoyance for users. This code will not work for all terminal emulators, but by adding it in systemd we'll entice maintainers of those terminals to add support for the sequences. For the terminals that don't support the sequence, we get a bit of a slowdown of `< 1 ms`, which seems hardly noticeable. The user can always set TERM explicitly to avoid this if upgrading to a newer terminal emulator is not possible. Closes https://github.com/systemd/systemd/issues/36994.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user