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.

(cherry picked from commit ba10d789a8)
This commit is contained in:
Daan De Meyer
2026-04-02 08:02:25 +00:00
committed by Zbigniew Jędrzejewski-Szmek
parent abca10b766
commit ade6e7a2a3
3 changed files with 43 additions and 27 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
@@ -853,7 +853,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) {
@@ -952,7 +952,8 @@ finish:
}
int terminal_reset_ansi_seq(int fd) {
int r, k;
_cleanup_(nonblock_resetp) int nonblock_reset = -EBADF;
int r;
assert(fd >= 0);
@@ -962,8 +963,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 */
@@ -971,17 +974,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");
CONSOLE_ANSI_SEQUENCE_TIMEOUT_USEC);
if (r < 0)
log_debug_errno(r, "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");
}
return k < 0 ? k : r;
return r;
}
void reset_dev_console_fd(int fd, bool switch_to_text) {
@@ -2016,7 +2013,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 = {};
@@ -2315,7 +2312,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 = {};
@@ -2386,16 +2383,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 = {};
@@ -2543,11 +2541,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;
@@ -2598,6 +2596,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);
@@ -2606,6 +2605,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);
@@ -2731,7 +2738,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;