From: Daan De Meyer Date: Thu, 2 Apr 2026 08:02:25 +0000 (+0000) Subject: basic/terminal-util: use non-blocking writes when sending ANSI sequences in terminal_... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ba10d789a899680dc7c3cdc5cd29b66d761eb81e;p=thirdparty%2Fsystemd.git basic/terminal-util: use non-blocking writes when sending ANSI sequences in terminal_get_size() terminal_get_size() writes ANSI escape sequences (CSI 18 and DSR queries) to the output fd to determine terminal dimensions. This is called during early boot via reset_dev_console_fd() and from service execution contexts via exec_context_apply_tty_size(). Previously, these writes used loop_write() on a blocking fd, which could block indefinitely if the terminal is not consuming data — for example on a serial console with flow control asserted, or a disconnected terminal. This is the same problem that was solved for terminal_reset_ansi_seq() in systemd/systemd#32369 by temporarily setting the fd to non-blocking mode with a write timeout. Apply the same pattern here: set the output fd to non-blocking in terminal_get_size() before issuing the queries, and restore blocking mode afterward. Change the loop_write() calls in terminal_query_size_by_dsr() and terminal_query_size_by_csi18() to loop_write_full() with a 100ms timeout so writes fail gracefully instead of hanging. Also introduce the CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC constant for all timeouts used across all ANSI sequence writes and reads (vt_disallocate(), terminal_reset_ansi_seq(), and the two size query functions). 333ms is now used for all timeouts in terminal-util.c. Also introduce a cleanup function for resetting a fd back to blocking mode after it was made non-blocking. --- diff --git a/src/basic/fd-util.c b/src/basic/fd-util.c index 6fb4e78c2f6..fbe3e68974d 100644 --- a/src/basic/fd-util.c +++ b/src/basic/fd-util.c @@ -166,6 +166,13 @@ int fd_nonblock(int fd, bool nonblock) { return 1; } +void nonblock_resetp(int *fd) { + PROTECT_ERRNO; + + if (*fd >= 0) + (void) fd_nonblock(*fd, false); +} + int stdio_disable_nonblock(void) { int ret = 0; diff --git a/src/basic/fd-util.h b/src/basic/fd-util.h index 60caa424b4e..c15ce7fddde 100644 --- a/src/basic/fd-util.h +++ b/src/basic/fd-util.h @@ -112,6 +112,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(DIR*, closedir, NULL); int fd_nonblock(int fd, bool nonblock); int stdio_disable_nonblock(void); +void nonblock_resetp(int *fd); + int fd_cloexec(int fd, bool cloexec); int fd_cloexec_many(const int fds[], size_t n_fds, bool cloexec); diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 7a240a4c7ab..3fd5c4f8b8b 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -44,8 +44,8 @@ #include "time-util.h" #include "utf8.h" -/* How much to wait for a reply to a terminal sequence */ -#define CONSOLE_REPLY_WAIT_USEC (333 * USEC_PER_MSEC) +/* How much to wait when reading/writing ANSI sequences from/to the console */ +#define CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC (333 * USEC_PER_MSEC) static volatile unsigned cached_columns = 0; static volatile unsigned cached_lines = 0; @@ -848,7 +848,7 @@ int vt_disallocate(const char *tty_path) { "\033[3J" /* clear screen including scrollback, requires Linux 2.6.40 */ "\033c", /* reset to initial state */ SIZE_MAX, - 100 * USEC_PER_MSEC); + CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); } static int vt_default_utf8(void) { @@ -947,7 +947,8 @@ finish: } int terminal_reset_ansi_seq(int fd) { - int r, k; + _cleanup_(nonblock_resetp) int nonblock_reset = -EBADF; + int r; assert(fd >= 0); @@ -957,8 +958,10 @@ int terminal_reset_ansi_seq(int fd) { r = fd_nonblock(fd, true); if (r < 0) return log_debug_errno(r, "Failed to set terminal to non-blocking mode: %m"); + if (r > 0) + nonblock_reset = fd; - k = loop_write_full(fd, + r = loop_write_full(fd, "\033[!p" /* soft terminal reset */ ANSI_OSC "104" ANSI_ST /* reset color palette via OSC 104 */ ANSI_NORMAL /* reset colors */ @@ -966,17 +969,11 @@ int terminal_reset_ansi_seq(int fd) { "\033[1G" /* place cursor at beginning of current line */ "\033[0J", /* erase till end of screen */ SIZE_MAX, - 100 * USEC_PER_MSEC); - if (k < 0) - log_debug_errno(k, "Failed to reset terminal through ANSI sequences: %m"); - - if (r > 0) { - r = fd_nonblock(fd, false); - if (r < 0) - log_debug_errno(r, "Failed to set terminal back to blocking mode: %m"); - } + CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); + if (r < 0) + log_debug_errno(r, "Failed to reset terminal through ANSI sequences: %m"); - return k < 0 ? k : r; + return r; } void reset_dev_console_fd(int fd, bool switch_to_text) { @@ -2009,7 +2006,7 @@ int terminal_get_cursor_position( if (r < 0) return r; - usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); char buf[STRLEN("\x1B[1;1R")]; /* The shortest valid reply possible */ size_t buf_full = 0; CursorPositionContext context = {}; @@ -2308,7 +2305,7 @@ int get_default_background_color(double *ret_red, double *ret_green, double *ret if (r < 0) return r; - usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); char buf[STRLEN(ANSI_OSC "11;rgb:0/0/0" ANSI_ST)]; /* shortest possible reply */ size_t buf_full = 0; BackgroundColorContext context = {}; @@ -2379,16 +2376,17 @@ static int terminal_query_size_by_dsr( /* Use DECSC/DECRC to save/restore cursor instead of querying position via DSR. This way the cursor * is always restored — even on timeout — and we only need one DSR response instead of two. */ - r = loop_write(output_fd, - "\x1B" "7" /* DECSC: save cursor position */ - "\x1B[32766;32766H" /* CUP: position cursor far to the right and to the bottom, staying within 16bit signed range */ - "\x1B[6n" /* DSR: request cursor position (CPR) */ - "\x1B" "8", /* DECRC: restore cursor position */ - SIZE_MAX); + r = loop_write_full(output_fd, + "\x1B" "7" /* DECSC: save cursor position */ + "\x1B[32766;32766H" /* CUP: position cursor far to the right and to the bottom, staying within 16bit signed range */ + "\x1B[6n" /* DSR: request cursor position (CPR) */ + "\x1B" "8", /* DECRC: restore cursor position */ + SIZE_MAX, + CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); if (r < 0) return r; - usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); char buf[STRLEN("\x1B[1;1R")]; /* The shortest valid reply possible */ size_t buf_full = 0; CursorPositionContext context = {}; @@ -2536,11 +2534,11 @@ static int terminal_query_size_by_csi18( assert(nonblock_input_fd >= 0); assert(output_fd >= 0); - r = loop_write(output_fd, CSI18_Q, SIZE_MAX); + r = loop_write_full(output_fd, CSI18_Q, SIZE_MAX, CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); if (r < 0) return r; - usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); char buf[STRLEN(CSI18_R1)]; size_t bytes = 0; @@ -2591,6 +2589,7 @@ int terminal_get_size( _cleanup_close_ int nonblock_input_fd = -EBADF; struct termios old_termios = TERMIOS_NULL; CLEANUP_TERMIOS_RESET(nonblock_input_fd, old_termios); + _cleanup_(nonblock_resetp) int nonblock_reset = -EBADF; int r; assert(try_dsr || try_csi18); @@ -2599,6 +2598,14 @@ int terminal_get_size( if (r < 0) return r; + /* Put the output fd in non-blocking mode with a write timeout, to avoid blocking indefinitely on + * write if the terminal is not consuming data (e.g. serial console with flow control). */ + r = fd_nonblock(output_fd, true); + if (r < 0) + return log_debug_errno(r, "Failed to set terminal to non-blocking mode: %m"); + if (r > 0) + nonblock_reset = output_fd; + /* Flush any stale input that might confuse the response parsers. */ (void) tcflush(nonblock_input_fd, TCIFLUSH); @@ -2732,7 +2739,7 @@ int terminal_get_terminfo_by_dcs(int fd, char **ret_name) { if (r < 0) return r; - usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_REPLY_WAIT_USEC); + usec_t end = usec_add(now(CLOCK_MONOTONIC), CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC); char buf[STRLEN(DCS_TERMINFO_R1) + MAX_TERMINFO_LENGTH + STRLEN(ANSI_ST)]; size_t bytes = 0;