From: Zbigniew Jędrzejewski-Szmek Date: Fri, 16 May 2025 13:30:02 +0000 (+0200) Subject: basic/terminal-util: query terminal name by DCS X-Git-Tag: v258-rc1~446^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5321b957b4027be66873eac730d4ce141021e368;p=thirdparty%2Fsystemd.git basic/terminal-util: query terminal name by DCS As requested in https://github.com/systemd/systemd/issues/36994, use DCS + q name ST. This works, but has limited terminal support: xterm, foot, kitty. --- diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 51b3a83d29d..4b1b0ecf6c8 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; @@ -2128,7 +2131,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 +2334,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 +2463,127 @@ 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 terminal_is_pty_fd(int fd) { int r; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 17dbdfdfd58..79683f5a857 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 { @@ -142,9 +145,10 @@ void termios_disable_echo(struct termios *termios); 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 terminal_is_pty_fd(int fd); int pty_open_peer(int fd, int mode); diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index f128ddca1f1..41bd4fbcecb 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -213,6 +213,21 @@ 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"); + + r = terminal_get_terminfo_by_dcs(fd, &name); + 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(terminal_is_pty_fd) { _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF; int r;