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 <test/functional/terminal/tui_spec.lua:2521>

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.
This commit is contained in:
zeertzjq
2026-03-05 10:48:07 +08:00
committed by GitHub
parent 8bfb91accc
commit 5048d9aa2a
2 changed files with 20 additions and 12 deletions

View File

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

View File

@@ -1369,16 +1369,12 @@ describe('jobs', function()
]])
feed(':q<CR>')
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()