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:
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()