From 5891f2f3dc41eda44c0072d726cf95e54aba85ad Mon Sep 17 00:00:00 2001 From: Sanzhar Kuandyk <92693103+SanzharKuandyk@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:49:16 +0500 Subject: [PATCH] fix(:restart): reuse --listen addr on Windows #38539 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. --- runtime/lua/vim/_core/server.lua | 53 +++++++++++++++++++++++++ src/nvim/ex_docmd.c | 57 +++++++++++++++++++++++---- test/functional/core/main_spec.lua | 5 ++- test/functional/terminal/tui_spec.lua | 24 ++++++----- 4 files changed, 122 insertions(+), 17 deletions(-) diff --git a/runtime/lua/vim/_core/server.lua b/runtime/lua/vim/_core/server.lua index 54d8d104f0..a79f871cd6 100644 --- a/runtime/lua/vim/_core/server.lua +++ b/runtime/lua/vim/_core/server.lua @@ -38,4 +38,57 @@ function M.serverlist(opts, addrs) return addrs 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 diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 96a27bdf4b..79c53b2900 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -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 diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 1e548901be..e95fe80343 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -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', diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 5ada3ebd0a..196531513e 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -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([[