fix(process): avoid inherited stdio for Windows jobs #40074

Problem:
On Windows, channel jobs inherit Nvim's stdio, so a background job
writing to CON (e.g. gutentags) draws onto the TUI and stays until
redraw.

Solution:
Give Windows job stdin/stderr libuv-created pipes (UV_CREATE_PIPE)
instead of inherited fds, so libuv spawns the child with
CREATE_NO_WINDOW and CON writes no longer leak onto the TUI.

(cherry picked from commit 39e9c1754a)
This commit is contained in:
Sanzhar Kuandyk
2026-06-29 19:52:19 +05:00
committed by github-actions[bot]
parent ad5720eab9
commit 61765b97fe
5 changed files with 78 additions and 5 deletions

View File

@@ -406,6 +406,7 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s
proc->cwd = cwd;
proc->env = env;
proc->overlapped = overlapped;
proc->stdio_noinherit = true;
char *cmd = xstrdup(proc_get_exepath(proc));
bool has_out, has_err;

View File

@@ -170,5 +170,6 @@ struct proc {
proc_state_cb state_cb;
internal_proc_cb internal_exit_cb, internal_close_cb;
bool closed, detach, overlapped, fwd_err;
bool stdio_noinherit; ///< MSWIN: channel jobs don't inherit stdio, to prevent CON leaking to TUI
MultiQueue *events;
};

View File

@@ -63,23 +63,39 @@ int libuv_proc_spawn(LibuvProc *uvproc)
int to_close[3] = { -1, -1, -1 };
if (!proc->in.closed) {
uv_file pipe_pair[2];
int client_flags = 0;
#ifdef MSWIN
client_flags |= proc->overlapped ? UV_NONBLOCK_PIPE : 0;
#endif
if (proc->stdio_noinherit) {
// Let channel jobs use CREATE_NO_WINDOW so CON writes do not leak.
uvproc->uvstdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE;
uvproc->uvstdio[0].flags |= proc->overlapped ? UV_OVERLAPPED_PIPE : 0;
uvproc->uvstdio[0].data.stream = (uv_stream_t *)(&proc->in.uv.pipe);
} else {
uv_file pipe_pair[2];
int client_flags = proc->overlapped ? UV_NONBLOCK_PIPE : 0;
uv_pipe(pipe_pair, client_flags, UV_NONBLOCK_PIPE);
uvproc->uvstdio[0].flags = UV_INHERIT_FD;
uvproc->uvstdio[0].data.fd = pipe_pair[0];
to_close[0] = pipe_pair[0];
uv_pipe_open(&proc->in.uv.pipe, pipe_pair[1]);
}
#else
uv_file pipe_pair[2];
// As of libuv 1.51, UV_CREATE_PIPE can only create pipes
// using socketpair(), not pipe(). We want the latter on linux
// as socket pairs behave different in some confusing ways, like
// breaking /proc/0/fd/0 which is disowned by the linux socket maintainer.
uv_pipe(pipe_pair, client_flags, UV_NONBLOCK_PIPE);
uv_pipe(pipe_pair, 0, UV_NONBLOCK_PIPE);
uvproc->uvstdio[0].flags = UV_INHERIT_FD;
uvproc->uvstdio[0].data.fd = pipe_pair[0];
to_close[0] = pipe_pair[0];
uv_pipe_open(&proc->in.uv.pipe, pipe_pair[1]);
#endif
}
if (!proc->out.s.closed) {
@@ -104,6 +120,24 @@ int libuv_proc_spawn(LibuvProc *uvproc)
}
if (!proc->err.s.closed) {
#ifdef MSWIN
if (proc->stdio_noinherit) {
uvproc->uvstdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE;
// pipe must be readable for IOCP to work on Windows.
uvproc->uvstdio[2].flags |= proc->overlapped
? (UV_READABLE_PIPE | UV_OVERLAPPED_PIPE) : 0;
uvproc->uvstdio[2].data.stream = (uv_stream_t *)(&proc->err.s.uv.pipe);
} else {
uv_file pipe_pair[2];
uv_pipe(pipe_pair, UV_NONBLOCK_PIPE, 0);
uvproc->uvstdio[2].flags = UV_INHERIT_FD;
uvproc->uvstdio[2].data.fd = pipe_pair[1];
to_close[2] = pipe_pair[1];
uv_pipe_open(&proc->err.s.uv.pipe, pipe_pair[0]);
}
#else
uv_file pipe_pair[2];
uv_pipe(pipe_pair, UV_NONBLOCK_PIPE, 0);
@@ -112,6 +146,7 @@ int libuv_proc_spawn(LibuvProc *uvproc)
to_close[2] = pipe_pair[1];
uv_pipe_open(&proc->err.s.uv.pipe, pipe_pair[0]);
#endif
} else if (proc->fwd_err) {
uvproc->uvstdio[2].flags = UV_INHERIT_FD;
uvproc->uvstdio[2].data.fd = STDERR_FILENO;

View File

@@ -31,6 +31,7 @@ static inline Proc proc_init(Loop *loop, ProcType type, void *data)
.internal_exit_cb = NULL,
.detach = false,
.fwd_err = false,
.stdio_noinherit = false,
};
}

View File

@@ -111,6 +111,41 @@ describe('TUI', function()
feed_data(':')
screen:expect(s1)
end)
it('jobstart child writing to CON does not leak to screen', function()
-- A child job that writes to CON must not leak onto the TUI screen. The
-- embedded server AttachConsole()'s the parent terminal (so server io.stdout
-- e.g. SIXEL works), but jobs must get their own (windowless) console so
-- writes to CON go nowhere visible.
t.skip(not is_os('win'), 'N/A Windows only')
n.clear()
finally(function()
n.check_close()
end)
local screen = tt.setup_child_nvim({
'--clean',
'--cmd',
'colorscheme vim',
'--cmd',
nvim_set .. ' laststatus=2 background=dark',
}, { env = env_notermguicolors })
tt.override_screen_expect_for_conpty(screen)
feed_data('iZZZSENTINEL')
-- Leave insert mode and run a job that writes directly to CON, blocking
-- until it finishes. The CON output must not appear anywhere on screen.
feed_data("\027\027:call jobwait([jobstart(['cmd', '/c', 'echo LEAKEDXYZ > CON'])])\013")
screen:expect([[
ZZZSENTINE^L |
{100:~ }|*3
{3:[No Name] [+] }|
|
{5:-- TERMINAL --} |
]])
end)
end)
describe('TUI :detach', function()