fix(:restart): --listen reusage on windows #39281

Problem:
On Windows, :restart cannot immediately reuse the canonical --listen
address because named pipe release is asynchronous.

Solution:
Start the new Nvim server on a temporary address; in the new Nvim,
retry serverstart() with the original ("canonical") address until it
succeeds.

(cherry picked from commit 5891f2f3dc)

Co-authored-by: Sanzhar Kuandyk <92693103+SanzharKuandyk@users.noreply.github.com>
This commit is contained in:
neovim-backports[bot]
2026-04-22 06:22:12 -04:00
committed by GitHub
parent 6583833ee2
commit b3b5674ac7
4 changed files with 122 additions and 17 deletions

View File

@@ -34,4 +34,57 @@ function M.serverlist(listed)
return found
end
-- (Windows only) Canonical --listen address persisted across restarts.
M.restart_canonical_addr = nil ---@type string?
--- (Windows only)
--- Called on the new server via nvim_exec_lua RPC from the old server (:restart).
--- Windows named pipes can't be rebound immediately, so the new server starts on a
--- temporary bootstrap address and polls until the canonical address is reclaimable.
--- @param canonical_addr string The original --listen address to reclaim.
--- @param bootstrap_addr string Temporary address the new server started on.
--- @param expected_uis integer Number of UIs expected to reattach (0 = don't wait).
function M.rebind_old_addr_after_restart(canonical_addr, bootstrap_addr, expected_uis)
M.restart_canonical_addr = canonical_addr
local poll_ms = 50
local max_wait_ms = 30000
local timer = assert(vim.uv.new_timer())
-- Poll until the canonical address can be reclaimed (or timeout).
local poll_elapsed = 0
timer:start(poll_ms, poll_ms, function()
vim.schedule(function()
poll_elapsed = poll_elapsed + poll_ms
if poll_elapsed >= max_wait_ms then
timer:stop()
timer:close()
return
end
if not vim.list_contains(vim.fn.serverlist(), canonical_addr) then
local ok = pcall(vim.fn.serverstart, canonical_addr)
if not ok then
return -- pipe still held by old server; retry next tick
end
end
-- Wait for UIs to reattach, then retire the bootstrap address.
local elapsed = 0
timer:stop()
timer:start(poll_ms, poll_ms, function()
vim.schedule(function()
elapsed = elapsed + poll_ms
local all_uis = expected_uis <= 0 or #vim.api.nvim_list_uis() >= expected_uis
if all_uis or elapsed >= max_wait_ms then
if canonical_addr ~= bootstrap_addr then
pcall(vim.fn.serverstop, bootstrap_addr)
end
timer:stop()
timer:close()
end
end)
end)
end)
end)
end
return M

View File

@@ -4977,8 +4977,10 @@ static void ex_restart(exarg_T *eap)
char **argv = xcalloc((size_t)argc + 3, sizeof(char *));
size_t i = 0;
const char *listen_arg = NULL;
#ifdef MSWIN // FIXME: --listen doesn't work on Windows and needs to be dropped
# define HANDLE_LISTEN_ADDR li = next_li; continue
#ifdef MSWIN
// On Windows, don't pass --listen to new server (named pipe can't be reused immediately).
// Instead pass the address via RPC, and new server will rebind to it after startup.
# define HANDLE_LISTEN_ADDR listen_arg = addr; li = next_li; continue
#else
# define HANDLE_LISTEN_ADDR listen_arg = addr
#endif
@@ -5020,11 +5022,27 @@ static void ex_restart(exarg_T *eap)
});
#undef HANDLE_LISTEN_ADDR
bool server_stopped = false;
if (listen_arg != NULL) {
// Stop listening on the --listen address so that the new server can listen.
server_stopped = server_stop(listen_arg, true);
#ifdef MSWIN
// On Windows, --listen is omitted from child argv because the named pipe can't be reused immediately.
// Recover the canonical address from the Lua module state (set by the previous rebind_old_addr_after_restart() call),
// and keep the current listener alive (new server reclaims it).
char *listen_arg_alloc = NULL;
if (listen_arg == NULL) {
Error lua_err = ERROR_INIT;
Object rv = NLUA_EXEC_STATIC("return require('vim._core.server').restart_canonical_addr",
(Array)ARRAY_DICT_INIT, kRetObject, NULL, &lua_err);
if (!ERROR_SET(&lua_err) && rv.type == kObjectTypeString && rv.data.string.size > 0) {
listen_arg_alloc = xstrdup(rv.data.string.data);
listen_arg = listen_arg_alloc;
}
api_free_object(rv);
api_clear_error(&lua_err);
}
bool server_stopped = false;
#else
// Stop listening on the --listen address so that the new server can listen.
bool server_stopped = listen_arg ? server_stop(listen_arg, true) : false;
#endif
#ifdef MSWIN
bool restart_alloc_console_env = false;
@@ -5086,7 +5104,8 @@ static void ex_restart(exarg_T *eap)
result_mem = NULL;
}
// Get new server's listen address.
// Get the new server's initial listen address. On Windows this is the
// temporary bootstrap address that UIs should reconnect to first.
MAXSIZE_TEMP_ARRAY(servername_args, 1);
ADD_C(servername_args, CSTR_AS_OBJ("servername"));
Object result = rpc_send_call(channel->id, "nvim_get_vvar", servername_args, &result_mem, &err);
@@ -5101,6 +5120,27 @@ static void ex_restart(exarg_T *eap)
arena_mem_free(result_mem);
result_mem = NULL;
#ifdef MSWIN
if (listen_arg != NULL) {
// Tell the new server to reclaim the canonical --listen address once the old listener exits,
// then retire the bootstrap address after all UIs have reattached (or timeout).
MAXSIZE_TEMP_ARRAY(lua_args, 2);
ADD_C(lua_args,
CSTR_AS_OBJ("return require('vim._core.server').rebind_old_addr_after_restart(...)"));
MAXSIZE_TEMP_ARRAY(handoff_params, 3);
ADD_C(handoff_params, CSTR_AS_OBJ(listen_arg));
ADD_C(handoff_params, CSTR_AS_OBJ(listen_addr));
ADD_C(handoff_params, INTEGER_OBJ((Integer)ui_active()));
ADD_C(lua_args, ARRAY_OBJ(handoff_params));
rpc_send_call(channel->id, "nvim_exec_lua", lua_args, &result_mem, &err);
if (ERROR_SET(&err)) {
goto fail_2;
}
arena_mem_free(result_mem);
result_mem = NULL;
}
#endif
// Send restart event with new listen address to all UIs.
ui_call_restart(cstr_as_string(listen_addr));
ui_flush();
@@ -5152,6 +5192,9 @@ fail_1:
if (server_stopped && server_start(listen_arg) != 0) {
semsg("couldn't resume listening on %s", listen_arg);
}
#ifdef MSWIN
xfree(listen_arg_alloc);
#endif
}
/// ":close": close current window, unless it is the last one

View File

@@ -216,7 +216,10 @@ describe('vim._core', function()
t.matches("^module 'vim%.hl' not found:", t.pcall_err(n.exec_lua, [[require('vim.hl')]]))
-- All `vim._core.*` modules are builtin.
t.eq({ 'serverlist' }, n.exec_lua([[return vim.tbl_keys(require('vim._core.server'))]]))
t.eq(
{ 'rebind_old_addr_after_restart', 'serverlist' },
n.exec_lua([[local k = vim.tbl_keys(require('vim._core.server')); table.sort(k); return k]])
)
local expected = {
'vim.F',
'vim._core.defaults',

View File

@@ -317,11 +317,15 @@ describe('TUI :restart', function()
local server_session = n.connect(server_pipe)
local _, server_pid = server_session:request('nvim_call_function', 'getpid', {})
local function assert_new_pid()
if is_os('win') then
return -- FIXME
end
server_session:close()
server_session = n.connect(server_pipe)
-- On Windows, --listen address is restored async (after old server exits).
if is_os('win') then
retry(nil, 5000, function()
server_session = n.connect(server_pipe)
end)
else
server_session = n.connect(server_pipe)
end
local _, new_pid = server_session:request('nvim_call_function', 'getpid', {})
t.neq(server_pid, new_pid)
server_pid = new_pid
@@ -431,11 +435,13 @@ describe('TUI :restart', function()
assert_new_pid()
assert_termguicolors_and_no_gui_running()
-- No --listen conflict when server exit is delayed.
feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd.restart()\n')
screen:expect(s0)
assert_new_pid()
assert_termguicolors_and_no_gui_running()
if not is_os('win') then
-- No --listen conflict when server exit is delayed.
feed_data(':lua vim.schedule(function() vim.wait(100) end); vim.cmd.restart()\n')
screen:expect(s0)
assert_new_pid()
assert_termguicolors_and_no_gui_running()
end
screen:try_resize(60, 6)
screen:expect([[