neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit ab8371a26cf47a40d26af637455ea71da0d5a59d
parent dea8430d5946c8e709e0196ae072f2394b34b5dc
Author: zeertzjq <zeertzjq@outlook.com>
Date:   Mon,  2 Mar 2026 20:39:05 +0800

fix(tui): server --listen error sometimes not visible (#38027)

Problem:  If Nvim server fails to --listen and prints error before the
          TUI enters alternate screen, the error isn't visible.
Solution: Forward server stderr using client side stderr handler instead
          of having the server inherit client stderr file descriptor.

This does mean that `stderr_isatty` will be `false` in the server, but
that value doesn't matter in embedded mode.

Always pass `stdin_fd` to embedded server to avoid a hang when reading
from stdin when it's a TTY (not sure why one wants to do that, perhaps
by mistake), because if `stdin_fd` isn't passed, the server will try to
use stderr as stdin.

Example test failure on CI:

FAILED   test/functional/terminal/tui_spec.lua @ 41: TUI exit status 1 and error message with server --listen error #34365
test/functional/terminal\tui_spec.lua:55: Failed to match any screen lines.
Expected (anywhere): "nvim%.exe: Failed to %-%-listen: address already in use:"
Actual:
  |{114:nvim.exe -h"}                                                |
  |                                                            |
  |[Process exited 1]^                                          |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |                                                            |
  |{5:-- TERMINAL --}                                              |

Snapshot:
screen:expect([[
  {114:nvim.exe -h"}                                                |
                                                              |
  [Process exited 1]^                                          |
                                                              |*13
  {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:55: in function <test/functional/terminal\tui_spec.lua:41>

In this case, it appears that the client entered alternate screen in the
middle of the server's print_mainerr().
Diffstat:
Mruntime/doc/api-ui-events.txt | 2+-
Msrc/nvim/channel.c | 21+++------------------
Msrc/nvim/event/defs.h | 2+-
Msrc/nvim/event/libuv_proc.c | 3---
Msrc/nvim/event/proc.c | 3---
Msrc/nvim/event/proc.h | 1-
Msrc/nvim/main.c | 2+-
Msrc/nvim/ui_client.c | 2+-
Mtest/functional/terminal/tui_spec.lua | 7+++++--
9 files changed, 12 insertions(+), 31 deletions(-)

diff --git a/runtime/doc/api-ui-events.txt b/runtime/doc/api-ui-events.txt @@ -55,7 +55,7 @@ with these (optional) keys: - `ext_termcolors` Use external default colors. - `term_name` Sets the name of the terminal 'term'. - `term_colors` Sets the number of supported colors 't_Co'. -- `stdin_fd` Read buffer 1 from this fd as if it were stdin |--|. +- `stdin_fd` Treat this fd as if it were stdin when using |--|. Only from |--embed| UI on startup. |ui-startup-stdin| - `stdin_tty` Tells if `stdin` is a `tty` or not. - `stdout_tty` Tells if `stdout` is a `tty` or not. diff --git a/src/nvim/channel.c b/src/nvim/channel.c @@ -405,16 +405,7 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s has_err = false; } else { has_out = rpc || callback_reader_set(chan->on_data); - has_err = callback_reader_set(chan->on_stderr); - proc->fwd_err = chan->on_stderr.fwd_err; -#ifdef MSWIN - // DETACHED_PROCESS can't inherit console handles (like ConPTY stderr). - // Use a pipe relay: libuv creates a pipe, on_channel_output writes to stderr. - if (!has_err && proc->fwd_err && proc->detach) { - has_err = true; - proc->fwd_err = false; - } -#endif + has_err = chan->on_stderr.fwd_err || callback_reader_set(chan->on_stderr); } bool has_in = stdin_mode == kChannelStdinPipe; @@ -686,16 +677,10 @@ static size_t on_channel_output(RStream *stream, Channel *chan, const char *buf, reader->eof = true; } -#ifdef MSWIN - // Pipe relay for fwd_err on Windows: relay server stderr to stdout. - // DETACHED_PROCESS prevents inheriting console handles, so channel_job_start - // creates a pipe instead. Write to stdout because ConPTY only captures stdout. - // On non-Windows, fwd_err uses UV_INHERIT_FD directly; this path is never reached. if (reader->fwd_err && count > 0) { - os_write(STDOUT_FILENO, buf, count, false); - return count; + ptrdiff_t wres = os_write(STDERR_FILENO, buf, count, false); + return (size_t)MAX(wres, 0); } -#endif if (callback_reader_set(*reader)) { ga_concat_len(&reader->buffer, buf, count); diff --git a/src/nvim/event/defs.h b/src/nvim/event/defs.h @@ -172,6 +172,6 @@ struct proc { proc_exit_cb cb; proc_state_cb state_cb; internal_proc_cb internal_exit_cb, internal_close_cb; - bool closed, detach, overlapped, fwd_err; + bool closed, detach, overlapped; MultiQueue *events; }; diff --git a/src/nvim/event/libuv_proc.c b/src/nvim/event/libuv_proc.c @@ -112,9 +112,6 @@ int libuv_proc_spawn(LibuvProc *uvproc) to_close[2] = pipe_pair[1]; uv_pipe_open(&proc->err.s.uv.pipe, pipe_pair[0]); - } else if (proc->fwd_err) { - uvproc->uvstdio[2].flags = UV_INHERIT_FD; - uvproc->uvstdio[2].data.fd = STDERR_FILENO; } int status; diff --git a/src/nvim/event/proc.c b/src/nvim/event/proc.c @@ -43,9 +43,6 @@ static int exit_need_delay = 0; int proc_spawn(Proc *proc, bool in, bool out, bool err) FUNC_ATTR_NONNULL_ALL { - // forwarding stderr contradicts with processing it internally - assert(!(err && proc->fwd_err)); - #ifdef MSWIN const bool out_use_poll = false; #else diff --git a/src/nvim/event/proc.h b/src/nvim/event/proc.h @@ -30,7 +30,6 @@ static inline Proc proc_init(Loop *loop, ProcType type, void *data) .internal_close_cb = NULL, .internal_exit_cb = NULL, .detach = false, - .fwd_err = false, }; } diff --git a/src/nvim/main.c b/src/nvim/main.c @@ -352,7 +352,7 @@ int main(int argc, char **argv) bool remote_ui = (ui_client_channel_id != 0); if (use_builtin_ui && !remote_ui) { - ui_client_forward_stdin = !stdin_isatty; + ui_client_forward_stdin = true; uint64_t rv = ui_client_start_server(get_vim_var_str(VV_PROGPATH), (size_t)params.argc, params.argv); if (!rv) { diff --git a/src/nvim/ui_client.c b/src/nvim/ui_client.c @@ -72,7 +72,7 @@ uint64_t ui_client_start_server(const char *exepath, size_t argc, char **argv) // If stdin is not a pty, it is forwarded to the client. // Replace stdin in the TUI process with the tty fd. - if (ui_client_forward_stdin) { + if (!stdin_isatty) { close(0); #ifdef MSWIN os_open_conin_fd(); diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua @@ -429,7 +429,7 @@ describe('TUI :restart', function() '--', 'Xtest-file1', 'Xtest-file2', - }) + }, { env = { NVIM_LOG_FILE = testlog } }) screen:expect([[ ^ | ~ |*3 @@ -437,6 +437,9 @@ describe('TUI :restart', function() | {5:-- TERMINAL --} | ]]) + -- This error happens as stdin (forwarded as fd 3) is not a pipe. + assert_log('Failed to get flags on descriptor 3: Bad file descriptor', testlog, 50) + server_session = n.connect(server_pipe) local expr = 'index(v:argv, "-") >= 0 || index(v:argv, "--") >= 0 ? v:true : v:false' local has_s = 'index(v:argv, "-s") >= 0 ? v:true : v:false' @@ -4334,7 +4337,7 @@ describe('TUI client', function() ffi.C.ui_call_chdir(to_api_string('README.md')) ]]) screen_client:expect_unchanged() - assert_log('Failed to chdir to README.md: not a directory', testlog) + assert_log('Failed to chdir to README%.md: not a directory', testlog) end) it('nvim_ui_send works with remote client #36317', function()