]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
basic/terminal-util: use non-blocking writes when sending ANSI sequences in terminal_...
authorDaan De Meyer <daan@amutable.com>
Thu, 2 Apr 2026 08:02:25 +0000 (08:02 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 2 Apr 2026 14:08:56 +0000 (16:08 +0200)
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.

src/basic/fd-util.c
src/basic/fd-util.h
src/basic/terminal-util.c

index 6fb4e78c2f6b69c85c67a4967f178946e5dafd6c..fbe3e68974d4428f7d0183e3ac3aa25beedaabe8 100644 (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;
 
index 60caa424b4e1da68fed796f906211ab7eabcd5f0..c15ce7fddde4a74c37cbdce50bd7ec298c73fb81 100644 (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);
 
index 7a240a4c7ab317d013ee36da26b09e4934f2d014..3fd5c4f8b8bd289677a613db27b5a4c270c014da 100644 (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;
@@ -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;