From: Lennart Poettering Date: Wed, 10 Jul 2024 14:02:52 +0000 (+0200) Subject: terminal-util: add helper that queries terminal sizes via ANSI sequence X-Git-Tag: v257-rc1~873^2~36 X-Git-Url: http://git-history.diyao.me/?a=commitdiff_plain;h=3390be38d19c9d339bbc0e003743ce4278aa58b6;p=systemd%2F.git terminal-util: add helper that queries terminal sizes via ANSI sequence When we are talking to a serial terminal quite commonly the dimensions are not set properly, because the serial protocol has not handshake or similar to transfer this information. However, we can derive the dimensions via ANSI sequences too, which should get us the right information, since ANSI sequences are interpreted by the final terminal, rather than an intermediary local tty driver (which is where TIOCGWINSZ is interpreted). This adds a helper call that gets the dimensions this way. --- diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 8c3af0bcb3..1cfde3e3ad 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1842,3 +1842,233 @@ finish: RET_GATHER(r, RET_NERRNO(tcsetattr(STDIN_FILENO, TCSADRAIN, &old_termios))); return r; } + +typedef enum CursorPositionState { + CURSOR_TEXT, + CURSOR_ESCAPE, + CURSOR_ROW, + CURSOR_COLUMN, +} CursorPositionState; + +typedef struct CursorPositionContext { + CursorPositionState state; + unsigned row, column; +} CursorPositionContext; + +static int scan_cursor_position_response( + CursorPositionContext *context, + const char *buf, + size_t size, + size_t *ret_processed) { + + assert(context); + assert(buf || size == 0); + + for (size_t i = 0; i < size; i++) { + char c = buf[i]; + + switch (context->state) { + + case CURSOR_TEXT: + context->state = c == '\x1B' ? CURSOR_ESCAPE : CURSOR_TEXT; + break; + + case CURSOR_ESCAPE: + context->state = c == '[' ? CURSOR_ROW : CURSOR_TEXT; + break; + + case CURSOR_ROW: + if (c == ';') + context->state = context->row > 0 ? CURSOR_COLUMN : CURSOR_TEXT; + else { + int d = undecchar(c); + + /* We read a decimal character, let's suffix it to the number we so far read, + * but let's do an overflow check first. */ + if (d < 0 || context->row > (UINT_MAX-d)/10) + context->state = CURSOR_TEXT; + else + context->row = context->row * 10 + d; + } + break; + + case CURSOR_COLUMN: + if (c == 'R') { + if (context->column > 0) { + if (ret_processed) + *ret_processed = i + 1; + + return 1; /* success! */ + } + + context->state = CURSOR_TEXT; + } else { + int d = undecchar(c); + + /* As above, add the decimal charatcer to our column number */ + if (d < 0 || context->column > (UINT_MAX-d)/10) + context->state = CURSOR_TEXT; + else + context->column = context->column * 10 + d; + } + + break; + } + + /* Reset any positions we might have picked up */ + if (IN_SET(context->state, CURSOR_TEXT, CURSOR_ESCAPE)) + context->row = context->column = 0; + } + + if (ret_processed) + *ret_processed = size; + + return 0; /* all good, but not enough data yet */ +} + +int terminal_get_size_by_dsr( + int input_fd, + int output_fd, + unsigned *ret_rows, + unsigned *ret_columns) { + + assert(input_fd >= 0); + assert(output_fd >= 0); + + int r; + + /* Tries to determine the terminal dimension by means of ANSI sequences rather than TIOCGWINSZ + * ioctl(). Why bother with this? The ioctl() information is often incorrect on serial terminals + * (since there's no handshake or protocol to determine the right dimensions in RS232), but since the + * ANSI sequences are interpreted by the final terminal instead of an intermediary tty driver they + * should be more accurate. + * + * Unfortunately there's no direct ANSI sequence to query terminal dimensions. But we can hack around + * it: we position the cursor briefly at an absolute location very far down and very far to the + * right, and then read back where we actually ended up. Because cursor locations are capped at the + * terminal width/height we should then see the right values. In order to not risk integer overflows + * in terminal applications we'll use INT16_MAX-1 as location to jump to — hopefully a value that is + * large enough for any real-life terminals, but small enough to not overflow anything or be + * recognized as a "niche" value. (Note that the dimension fields in "struct winsize" are 16bit only, + * too). */ + + if (terminal_is_dumb()) + return -EOPNOTSUPP; + + r = terminal_verify_same(input_fd, output_fd); + if (r < 0) + return log_debug_errno(r, "Called with distinct input/output fds: %m"); + + struct termios old_termios; + if (tcgetattr(input_fd, &old_termios) < 0) + return log_debug_errno(errno, "Failed to to get terminal settings: %m"); + + struct termios new_termios = old_termios; + termios_disable_echo(&new_termios); + + if (tcsetattr(input_fd, TCSADRAIN, &new_termios) < 0) + return log_debug_errno(errno, "Failed to to set new terminal settings: %m"); + + unsigned saved_row = 0, saved_column = 0; + + r = loop_write(output_fd, + "\x1B[6n" /* Request cursor position (DSR/CPR) */ + "\x1B[32766;32766H" /* Position cursor really far to the right and to the bottom, but let's stay within the 16bit signed range */ + "\x1B[6n", /* Request cursor position again */ + SIZE_MAX); + if (r < 0) + goto finish; + + usec_t end = usec_add(now(CLOCK_MONOTONIC), 333 * USEC_PER_MSEC); + char buf[STRLEN("\x1B[1;1R")]; /* The shortest valid reply possible */ + size_t buf_full = 0; + CursorPositionContext context = {}; + + for (bool first = true;; first = false) { + if (buf_full == 0) { + usec_t n = now(CLOCK_MONOTONIC); + + if (n >= end) { + r = -EOPNOTSUPP; + goto finish; + } + + r = fd_wait_for_event(input_fd, POLLIN, usec_sub_unsigned(end, n)); + if (r < 0) + goto finish; + if (r == 0) { + r = -EOPNOTSUPP; + goto finish; + } + + /* On the first try, read multiple characters, i.e. the shortest valid + * reply. Afterwards read byte-wise, since we don't want to read too much, and + * unnecessarily drop too many characters from the input queue. */ + ssize_t l = read(input_fd, buf, first ? sizeof(buf) : 1); + if (l < 0) { + r = -errno; + goto finish; + } + + assert((size_t) l <= sizeof(buf)); + buf_full = l; + } + + size_t processed; + r = scan_cursor_position_response(&context, buf, buf_full, &processed); + if (r < 0) + goto finish; + + assert(processed <= buf_full); + buf_full -= processed; + memmove(buf, buf + processed, buf_full); + + if (r > 0) { + if (saved_row == 0) { + assert(saved_column == 0); + + /* First sequence, this is the cursor position before we set it somewhere + * into the void at the bottom right. Let's save where we are so that we can + * return later. */ + + /* Superficial validity checks */ + if (context.row <= 0 || context.column <= 0 || context.row >= 32766 || context.column >= 32766) { + r = -ENODATA; + goto finish; + } + + saved_row = context.row; + saved_column = context.column; + + /* Reset state */ + context = (CursorPositionContext) {}; + } else { + /* Second sequence, this is the cursor position after we set it somewhere + * into the void at the bottom right. */ + + /* Superficial validity checks (no particular reason to check for < 4, it's + * just a way to look for unreasonably small values) */ + if (context.row < 4 || context.column < 4 || context.row >= 32766 || context.column >= 32766) { + r = -ENODATA; + goto finish; + } + + if (ret_rows) + *ret_rows = context.row; + if (ret_columns) + *ret_columns = context.column; + + r = 0; + goto finish; + } + } + } + +finish: + /* Restore cursor position */ + if (saved_row > 0 && saved_column > 0) + RET_GATHER(r, terminal_set_cursor_position(output_fd, saved_row, saved_column)); + + RET_GATHER(r, RET_NERRNO(tcsetattr(input_fd, TCSADRAIN, &old_termios))); + return r; +} diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 51569a73f5..e2c21f7fb7 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -295,3 +295,4 @@ static inline const char* ansi_highlight_green_red(bool b) { 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); diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index 9f8ca15a65..3d15d45558 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include "alloc-util.h" @@ -176,6 +178,25 @@ TEST(get_default_background_color) { log_notice("R=%g G=%g B=%g", red, green, blue); } +TEST(terminal_get_size_by_dsr) { + unsigned rows, columns; + int r; + + r = terminal_get_size_by_dsr(STDIN_FILENO, STDOUT_FILENO, &rows, &columns); + 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); + + 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); + } +} + static void test_get_color_mode_with_env(const char *key, const char *val, ColorMode expected) { ASSERT_OK(setenv(key, val, true)); reset_terminal_feature_caches();