From 5048d9aa2ab2f680029688068e3cd0850f388968 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 5 Mar 2026 10:48:07 +0800 Subject: [PATCH] fix(tui): call tcdrain() on stdout and stderr on exit (#38154) Problem: On FreeBSD, output written to TTY may be lost on exit. Example test failure: FAILED test/functional/terminal/tui_spec.lua @ 2521: TUI no assert failure on deadly signal #21896 test/functional/terminal/tui_spec.lua:2523: Row 1 did not match. Expected: |*Nvim: Caught deadly signal 'SIGTERM' | |* | |*[Process exited 1]^ | |* | |* | | | |{5:-- TERMINAL --} | Actual: |* | |*[Process exited 1]{100:^ }| |*{100:~ }| |*{100:~ }| |*{3:[No Name] }| | | |{5:-- TERMINAL --} | To print the expect() call that would assert the current screen state, use screen:snapshot_util(). In case of non-deterministic failures, use screen:redraw_debug() to show all intermediate screen states. Snapshot: screen:expect([[ | [Process exited 1]{100:^ }| {100:~ }|*2 {3:[No Name] }| | {5:-- TERMINAL --} | ]]) stack traceback: test/functional/ui/screen.lua:909: in function '_wait' test/functional/ui/screen.lua:537: in function 'expect' test/functional/terminal/tui_spec.lua:2523: in function Solution: Call tcdrain() on stdout and stderr on exit. This problem is only observed on FreeBSD, but it probably doesn't hurt to do this on all platforms with termios.h. In fact using tcdrain() on PTY slave is no-op on Linux according to Linux kernel source code. --- src/nvim/main.c | 16 ++++++++++++++-- test/functional/core/job_spec.lua | 16 ++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/nvim/main.c b/src/nvim/main.c index ab81d8544d..a6af8edc42 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -717,7 +717,19 @@ void os_exit(int r) if (!event_teardown() && r == 0) { r = 1; // Exit with error if main_loop did not teardown gracefully. } - if (!ui_client_channel_id) { + if (ui_client_channel_id) { +#ifdef HAVE_TERMIOS_H + // Sometimes the final output to TTY can be lost (at least on FreeBSD). + // Call tcdrain() to ensure all output has been transmitted to host terminal. + // Do this after event_teardown() as libuv events may write to stderr. + if (stdout_isatty) { + tcdrain(STDOUT_FILENO); + } + if (stderr_isatty) { + tcdrain(STDERR_FILENO); + } +#endif + } else { ml_close_all(true); // remove all memfiles } if (used_stdin) { @@ -901,7 +913,7 @@ void preserve_exit(const char *errmsg) ui_client_stop(); } if (errmsg != NULL && errmsg[0] != NUL) { - size_t has_eol = '\n' == errmsg[strlen(errmsg) - 1]; + bool has_eol = '\n' == errmsg[strlen(errmsg) - 1]; fprintf(stderr, has_eol ? "%s" : "%s\n", errmsg); } if (ui_client_channel_id) { diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index b5dc3c6121..da85155eb4 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -1369,16 +1369,12 @@ describe('jobs', function() ]]) feed(':q') - if is_os('freebsd') then - screen:expect { any = vim.pesc('[Process exited 0]') } - else - screen:expect([[ - | - [Process exited 0]^ | - |*4 - {5:-- TERMINAL --} | - ]]) - end + screen:expect([[ + | + [Process exited 0]^ | + |*4 + {5:-- TERMINAL --} | + ]]) end) it('uses real pipes for stdin/stdout #35984', function()