mirror of
https://github.com/neovim/neovim.git
synced 2026-06-16 00:31:16 +00:00
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:
committed by
GitHub
parent
6583833ee2
commit
b3b5674ac7
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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([[
|
||||
|
||||
Reference in New Issue
Block a user