mirror of
https://github.com/neovim/neovim.git
synced 2026-04-28 18:24:13 +00:00
fix(channel): support :detach, :restart on Windows #37977
fix: allocate hidden console for detached server Starting the server with UV_PROCESS_DETACHED results in DETACHED_PROCESS, leaving the child without a console. Without a console: CONIN$ / CONOUT$ cannot resolve, causing channel_from_stdio to fail. ConPTY cannot attach, breaking :terminal. This patch allocates a hidden console via AllocConsole() when the server has none, restoring working stdio and enabling ConPTY. Also updates os_set_cloexec to clear HANDLE_FLAG_INHERIT on the RPC pipe handles, matching the Unix F_DUPFD_CLOEXEC behavior.
This commit is contained in:
@@ -407,6 +407,14 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s
|
|||||||
has_out = rpc || callback_reader_set(chan->on_data);
|
has_out = rpc || callback_reader_set(chan->on_data);
|
||||||
has_err = callback_reader_set(chan->on_stderr);
|
has_err = callback_reader_set(chan->on_stderr);
|
||||||
proc->fwd_err = chan->on_stderr.fwd_err;
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_in = stdin_mode == kChannelStdinPipe;
|
bool has_in = stdin_mode == kChannelStdinPipe;
|
||||||
@@ -536,8 +544,18 @@ uint64_t channel_from_stdio(bool rpc, CallbackReader on_output, const char **err
|
|||||||
// stdin and stdout with CONIN$ and CONOUT$, respectively.
|
// stdin and stdout with CONIN$ and CONOUT$, respectively.
|
||||||
if (embedded_mode && os_has_conpty_working()) {
|
if (embedded_mode && os_has_conpty_working()) {
|
||||||
stdin_dup_fd = os_dup(STDIN_FILENO);
|
stdin_dup_fd = os_dup(STDIN_FILENO);
|
||||||
os_replace_stdin_to_conin();
|
os_set_cloexec(stdin_dup_fd);
|
||||||
stdout_dup_fd = os_dup(STDOUT_FILENO);
|
stdout_dup_fd = os_dup(STDOUT_FILENO);
|
||||||
|
os_set_cloexec(stdout_dup_fd);
|
||||||
|
|
||||||
|
// The server may have no console (spawned with UV_PROCESS_DETACHED for
|
||||||
|
// :detach support). Allocate a hidden one so CONIN$/CONOUT$ and ConPTY
|
||||||
|
// (:terminal) work.
|
||||||
|
if (!GetConsoleWindow()) {
|
||||||
|
AllocConsole();
|
||||||
|
ShowWindow(GetConsoleWindow(), SW_HIDE);
|
||||||
|
}
|
||||||
|
os_replace_stdin_to_conin();
|
||||||
os_replace_stdout_and_stderr_to_conout();
|
os_replace_stdout_and_stderr_to_conout();
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
@@ -668,6 +686,17 @@ static size_t on_channel_output(RStream *stream, Channel *chan, const char *buf,
|
|||||||
reader->eof = true;
|
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;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (callback_reader_set(*reader)) {
|
if (callback_reader_set(*reader)) {
|
||||||
ga_concat_len(&reader->buffer, buf, count);
|
ga_concat_len(&reader->buffer, buf, count);
|
||||||
schedule_channel_event(chan);
|
schedule_channel_event(chan);
|
||||||
|
|||||||
@@ -306,6 +306,19 @@ int main(int argc, char **argv)
|
|||||||
nlua_init(argv, argc, params.lua_arg0);
|
nlua_init(argv, argc, params.lua_arg0);
|
||||||
TIME_MSG("init lua interpreter");
|
TIME_MSG("init lua interpreter");
|
||||||
|
|
||||||
|
// On Windows, channel_from_stdio() replaces fd 2 with CONOUT$ (for ConPTY
|
||||||
|
// support). Save a dup of the original stderr first so that if server_init()
|
||||||
|
// fails, print_mainerr() can write through the pipe to the TUI client's relay.
|
||||||
|
#ifdef MSWIN
|
||||||
|
int startup_stderr_fd = -1;
|
||||||
|
if (embedded_mode) {
|
||||||
|
startup_stderr_fd = os_dup(STDERR_FILENO);
|
||||||
|
if (startup_stderr_fd >= 0) {
|
||||||
|
os_set_cloexec(startup_stderr_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (embedded_mode) {
|
if (embedded_mode) {
|
||||||
const char *err;
|
const char *err;
|
||||||
if (!channel_from_stdio(true, CALLBACK_READER_INIT, &err)) {
|
if (!channel_from_stdio(true, CALLBACK_READER_INIT, &err)) {
|
||||||
@@ -357,9 +370,27 @@ int main(int argc, char **argv)
|
|||||||
// Nvim server...
|
// Nvim server...
|
||||||
|
|
||||||
if (!server_init(params.listen_addr)) {
|
if (!server_init(params.listen_addr)) {
|
||||||
|
#ifdef MSWIN
|
||||||
|
// Restore the original stderr (pipe to TUI client) so print_mainerr()
|
||||||
|
// output is visible in the TUI terminal via the relay in on_channel_output.
|
||||||
|
if (startup_stderr_fd >= 0) {
|
||||||
|
dup2(startup_stderr_fd, STDERR_FILENO);
|
||||||
|
close(startup_stderr_fd);
|
||||||
|
startup_stderr_fd = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
mainerr(IObuff, NULL, NULL);
|
mainerr(IObuff, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef MSWIN
|
||||||
|
// Server started successfully. Close the saved fd so the pipe write end is
|
||||||
|
// fully released — child processes inherit CONOUT$ (fd 2), not the pipe.
|
||||||
|
if (startup_stderr_fd >= 0) {
|
||||||
|
close(startup_stderr_fd);
|
||||||
|
startup_stderr_fd = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
TIME_MSG("expanding arguments");
|
TIME_MSG("expanding arguments");
|
||||||
|
|
||||||
if (params.diff_mode && params.window_count == -1) {
|
if (params.diff_mode && params.window_count == -1) {
|
||||||
@@ -957,6 +988,11 @@ static void remote_request(mparm_T *params, int remote_args, char *server_addr,
|
|||||||
|
|
||||||
if (is_ui) {
|
if (is_ui) {
|
||||||
if (!chan) {
|
if (!chan) {
|
||||||
|
#ifdef MSWIN
|
||||||
|
// The TUI client is spawned in a ConPTY which only captures stdout.
|
||||||
|
// Redirect stderr to stdout so this error appears in the terminal.
|
||||||
|
dup2(STDOUT_FILENO, STDERR_FILENO);
|
||||||
|
#endif
|
||||||
fprintf(stderr, "Remote ui failed to start: %s\n", connect_error);
|
fprintf(stderr, "Remote ui failed to start: %s\n", connect_error);
|
||||||
os_exit(1);
|
os_exit(1);
|
||||||
} else if (strequal(server_addr, os_getenv_noalloc("NVIM"))) {
|
} else if (strequal(server_addr, os_getenv_noalloc("NVIM"))) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <uv.h>
|
#include <uv.h>
|
||||||
|
|
||||||
#ifdef MSWIN
|
#ifdef MSWIN
|
||||||
|
# include <io.h>
|
||||||
# include <shlobj.h>
|
# include <shlobj.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -483,9 +484,9 @@ FILE *os_fopen(const char *path, const char *flags)
|
|||||||
return fdopen(fd, flags);
|
return fdopen(fd, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets file descriptor `fd` to close-on-exec.
|
/// Sets file descriptor `fd` to close-on-exec (Unix) or non-inheritable (Windows).
|
||||||
//
|
///
|
||||||
// @return -1 if failed to set, 0 otherwise.
|
/// @return -1 if failed to set, 0 otherwise.
|
||||||
int os_set_cloexec(const int fd)
|
int os_set_cloexec(const int fd)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_FD_CLOEXEC
|
#ifdef HAVE_FD_CLOEXEC
|
||||||
@@ -505,11 +506,16 @@ int os_set_cloexec(const int fd)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
#endif
|
#elif defined(MSWIN)
|
||||||
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
||||||
// No FD_CLOEXEC flag. On Windows, the file should have been opened with
|
if (h == INVALID_HANDLE_VALUE
|
||||||
// O_NOINHERIT anyway.
|
|| !SetHandleInformation(h, HANDLE_FLAG_INHERIT, 0)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
return -1;
|
return -1;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close a file
|
/// Close a file
|
||||||
|
|||||||
@@ -60,12 +60,7 @@ uint64_t ui_client_start_server(const char *exepath, size_t argc, char **argv)
|
|||||||
CallbackReader on_err = CALLBACK_READER_INIT;
|
CallbackReader on_err = CALLBACK_READER_INIT;
|
||||||
on_err.fwd_err = true;
|
on_err.fwd_err = true;
|
||||||
|
|
||||||
#ifdef MSWIN
|
|
||||||
// TODO(justinmk): detach breaks `tt.setup_child_nvim` tests on Windows?
|
|
||||||
bool detach = os_env_exists("__NVIM_DETACH", true);
|
|
||||||
#else
|
|
||||||
bool detach = true;
|
bool detach = true;
|
||||||
#endif
|
|
||||||
varnumber_T exit_status;
|
varnumber_T exit_status;
|
||||||
Channel *channel = channel_job_start(args, exepath,
|
Channel *channel = channel_job_start(args, exepath,
|
||||||
CALLBACK_READER_INIT, on_err, CALLBACK_NONE,
|
CALLBACK_READER_INIT, on_err, CALLBACK_NONE,
|
||||||
|
|||||||
@@ -50,18 +50,20 @@ describe('TUI', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
screen:expect({ any = vim.pesc('[Process exited 1]') })
|
screen:expect({ any = vim.pesc('[Process exited 1]') })
|
||||||
|
|
||||||
-- When the address is very long, the error message may be only partly visible.
|
-- When the address is very long, the error message may be only partly visible.
|
||||||
if #addr_in_use <= 600 then
|
if #addr_in_use <= 600 then
|
||||||
screen:expect({
|
screen:expect({
|
||||||
any = vim.pesc(
|
any = vim.pesc(
|
||||||
('%s: Failed to --listen: address already in use:'):format(
|
('%s: Failed to --listen: address already in use:'):format(
|
||||||
is_os('win') and 'nvim.exe' or 'nvim'
|
fn.fnamemodify(nvim_prog, ':t')
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
unchanged = true,
|
unchanged = true,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Always assert the log for the error message.
|
||||||
assert_log(
|
assert_log(
|
||||||
vim.pesc('Failed to start server: address already in use: ' .. addr_in_use),
|
vim.pesc('Failed to start server: address already in use: ' .. addr_in_use),
|
||||||
testlog,
|
testlog,
|
||||||
@@ -116,41 +118,6 @@ describe('TUI :detach', function()
|
|||||||
it('does not stop server', function()
|
it('does not stop server', function()
|
||||||
local job_opts = { env = t.shallowcopy(env_notermguicolors) }
|
local job_opts = { env = t.shallowcopy(env_notermguicolors) }
|
||||||
|
|
||||||
if is_os('win') then
|
|
||||||
-- TODO(justinmk): on Windows,
|
|
||||||
-- - tt.setup_child_nvim() is broken.
|
|
||||||
-- - session.lua is broken after the pipe closes.
|
|
||||||
-- So this test currently just exercises __NVIM_DETACH + :detach, without asserting anything.
|
|
||||||
|
|
||||||
-- TODO(justinmk): temporary hack for Windows.
|
|
||||||
job_opts.env['__NVIM_DETACH'] = '1'
|
|
||||||
n.clear(job_opts)
|
|
||||||
|
|
||||||
local screen = Screen.new(50, 10)
|
|
||||||
n.feed('iHello, World')
|
|
||||||
screen:expect([[
|
|
||||||
Hello, World^ |
|
|
||||||
{1:~ }|*8
|
|
||||||
{5:-- INSERT --} |
|
|
||||||
]])
|
|
||||||
|
|
||||||
-- local addr = api.nvim_get_vvar('servername')
|
|
||||||
eq(1, #n.api.nvim_list_uis())
|
|
||||||
|
|
||||||
-- TODO(justinmk): test util should not freak out when the pipe closes.
|
|
||||||
n.expect_exit(n.command, 'detach')
|
|
||||||
|
|
||||||
-- n.get_session():close() -- XXX: hangs
|
|
||||||
-- n.set_session(n.connect(addr)) -- XXX: hangs
|
|
||||||
-- eq(0, #n.api.nvim_list_uis()) -- XXX: hangs
|
|
||||||
|
|
||||||
-- Avoid a dangling process.
|
|
||||||
n.get_session():close('kill')
|
|
||||||
-- n.expect_exit(n.command, 'qall!')
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
n.clear()
|
n.clear()
|
||||||
finally(function()
|
finally(function()
|
||||||
n.check_close()
|
n.check_close()
|
||||||
@@ -171,13 +138,16 @@ describe('TUI :detach', function()
|
|||||||
}, job_opts)
|
}, job_opts)
|
||||||
|
|
||||||
tt.feed_data('iHello, World')
|
tt.feed_data('iHello, World')
|
||||||
screen:expect([[
|
tt.screen_expect(
|
||||||
|
screen,
|
||||||
|
[[
|
||||||
Hello, World^ |
|
Hello, World^ |
|
||||||
{100:~ }|*3
|
{100:~ }|*3
|
||||||
{3:[No Name] [+] }|
|
{3:[No Name] [+] }|
|
||||||
{5:-- INSERT --} |
|
{5:-- INSERT --} |
|
||||||
{5:-- TERMINAL --} |
|
{5:-- TERMINAL --} |
|
||||||
]])
|
]]
|
||||||
|
)
|
||||||
|
|
||||||
local child_session = n.connect(child_server)
|
local child_session = n.connect(child_server)
|
||||||
finally(function()
|
finally(function()
|
||||||
@@ -226,13 +196,16 @@ describe('TUI :detach', function()
|
|||||||
child_server,
|
child_server,
|
||||||
}, job_opts)
|
}, job_opts)
|
||||||
|
|
||||||
screen_reattached:expect([[
|
tt.screen_expect(
|
||||||
|
screen_reattached,
|
||||||
|
[[
|
||||||
We did it, pooky^. |
|
We did it, pooky^. |
|
||||||
{100:~ }|*3
|
{100:~ }|*3
|
||||||
{3:[No Name] [+] }|
|
{3:[No Name] [+] }|
|
||||||
|
|
|
|
||||||
{5:-- TERMINAL --} |
|
{5:-- TERMINAL --} |
|
||||||
]])
|
]]
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -264,16 +237,8 @@ describe('TUI :restart', function()
|
|||||||
'echo getpid()',
|
'echo getpid()',
|
||||||
}, { env = env_notermguicolors })
|
}, { env = env_notermguicolors })
|
||||||
|
|
||||||
--- FIXME: On Windows spaces at the end of a screen line may have wrong attrs.
|
|
||||||
--- Remove this function when that's fixed.
|
|
||||||
---
|
|
||||||
--- @param s string
|
|
||||||
local function screen_expect(s)
|
local function screen_expect(s)
|
||||||
if is_os('win') then
|
tt.screen_expect(screen, s)
|
||||||
s = s:gsub(' *%} +%|\n', '{MATCH: *}}{MATCH: *}|\n')
|
|
||||||
s = s:gsub('%}%^ +%|\n', '{MATCH:[ ^]*}}{MATCH:[ ^]*}|\n')
|
|
||||||
end
|
|
||||||
screen:expect(s)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The value of has("gui_running") should be 0 before and after :restart.
|
-- The value of has("gui_running") should be 0 before and after :restart.
|
||||||
@@ -499,10 +464,6 @@ describe('TUI :restart', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
describe('TUI :connect', function()
|
describe('TUI :connect', function()
|
||||||
if t.skip(is_os('win'), "relies on :detach which currently doesn't work on windows") then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local screen_empty = [[
|
local screen_empty = [[
|
||||||
^ |
|
^ |
|
||||||
{100:~ }|*5
|
{100:~ }|*5
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
-- - NOTE: Only use this if your test actually needs the full lifecycle/capabilities of the
|
-- - NOTE: Only use this if your test actually needs the full lifecycle/capabilities of the
|
||||||
-- builtin Nvim TUI. Most tests should just use `Screen.new()` directly, or plain old API calls.
|
-- builtin Nvim TUI. Most tests should just use `Screen.new()` directly, or plain old API calls.
|
||||||
|
|
||||||
|
local t = require('test.testutil')
|
||||||
local n = require('test.functional.testnvim')()
|
local n = require('test.functional.testnvim')()
|
||||||
local Screen = require('test.functional.ui.screen')
|
local Screen = require('test.functional.ui.screen')
|
||||||
|
|
||||||
@@ -207,4 +208,17 @@ function M.setup_child_nvim(args, opts)
|
|||||||
return M.setup_screen(opts.extra_rows, argv, opts.cols, env)
|
return M.setup_screen(opts.extra_rows, argv, opts.cols, env)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- FIXME: On Windows spaces at the end of a screen line may have wrong attrs.
|
||||||
|
--- Remove this function when that's fixed.
|
||||||
|
---
|
||||||
|
--- @param screen test.functional.ui.screen
|
||||||
|
--- @param s string
|
||||||
|
function M.screen_expect(screen, s)
|
||||||
|
if t.is_os('win') then
|
||||||
|
s = s:gsub(' *%} +%|\n', '{MATCH: *}}{MATCH: *}|\n')
|
||||||
|
s = s:gsub('%}%^ +%|\n', '{MATCH:[ ^]*}}{MATCH:[ ^]*}|\n')
|
||||||
|
end
|
||||||
|
screen:expect(s)
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
Reference in New Issue
Block a user