mirror of
https://github.com/neovim/neovim.git
synced 2026-05-06 16:29:57 -04:00
c3f803c3b6
Problem: UI tools and orchestration engines need more context than just raw socket addresses from serverlist(). Without knowing if a server belongs to the current instance or knowing its PID, UIs cannot display meaningful options to users. Solution: - Added the `info=v:true` option to `serverlist()`. - When `info` is requested, it implies `peer=true` and returns a list of dictionaries (defined as `vim.ServerInfo`) with `addr`, `pid` and `own`. - Uses an RPC request to `getpid()` across the socket to fetch the peer's actual process ID. Signed-off-by: Szymon Wilczek <swilczek.lx@gmail.com>
148 lines
4.9 KiB
Lua
148 lines
4.9 KiB
Lua
-- For "--listen" and related functionality.
|
|
|
|
local M = {}
|
|
|
|
--- Called by builtin serverlist(). Returns the combined server list (own + peers).
|
|
---
|
|
---@class vim.ServerInfo
|
|
---@field addr string Server address (socket path, named pipe, or TCP host:port).
|
|
---@field pid integer? PID of the Nvim process owning this server (nil if unreachable).
|
|
---@field own boolean True if this server belongs to the current Nvim instance.
|
|
|
|
--- @param opts? table Options:
|
|
--- - opts.peer (boolean): If true, also discover peer servers.
|
|
--- - opts.info (boolean): If true, return a list of |vim.ServerInfo| dicts
|
|
--- instead of a list of addresses. Implies peer=true.
|
|
--- @param addrs string[] Internal ("own") addresses, from server_address_list.
|
|
--- @return string[]|vim.ServerInfo[]
|
|
function M.serverlist(opts, addrs)
|
|
if type(opts) ~= 'table' then
|
|
return addrs
|
|
end
|
|
|
|
local peer = opts.peer == true
|
|
local want_info = opts.info == true
|
|
local discover = peer or want_info
|
|
|
|
if not discover then
|
|
return addrs
|
|
end
|
|
|
|
local own_set = {} ---@type table<string, true>
|
|
for _, a in ipairs(addrs) do
|
|
own_set[a] = true
|
|
end
|
|
|
|
-- Discover peer servers in stdpath("run").
|
|
-- TODO: track TCP servers, somehow.
|
|
-- TODO: support Windows named pipes.
|
|
local root = vim.fs.normalize(vim.fn.stdpath('run') .. '/..')
|
|
local socket_paths = vim.fs.find(function(name, _)
|
|
return name:match('nvim.*')
|
|
end, { path = root, type = 'socket', limit = math.huge })
|
|
|
|
local peer_pids = {} ---@type table<string, integer>
|
|
|
|
for _, socket in ipairs(socket_paths) do
|
|
if not own_set[socket] and not vim.list_contains(addrs, socket) then
|
|
local ok, chan = pcall(vim.fn.sockconnect, 'pipe', socket, { rpc = true })
|
|
if ok and chan and chan > 0 then
|
|
-- Check that the server is responding
|
|
-- TODO: do we need a timeout or error handling here?
|
|
local ok_chan, chan_info = pcall(vim.fn.rpcrequest, chan, 'nvim_get_chan_info', 0)
|
|
if ok_chan and type(chan_info) == 'table' and chan_info.id then
|
|
table.insert(addrs, socket)
|
|
if want_info then
|
|
local ok_pid, pid = pcall(vim.fn.rpcrequest, chan, 'nvim_eval', 'getpid()')
|
|
if ok_pid and type(pid) == 'number' then
|
|
peer_pids[socket] = pid
|
|
end
|
|
end
|
|
end
|
|
pcall(vim.fn.chanclose, chan)
|
|
end
|
|
end
|
|
end
|
|
|
|
if not want_info then
|
|
return addrs
|
|
end
|
|
|
|
local self_pid = vim.fn.getpid()
|
|
local result = {} ---@type vim.ServerInfo[]
|
|
for _, addr in ipairs(addrs) do
|
|
local own = own_set[addr] == true
|
|
table.insert(result, {
|
|
addr = addr,
|
|
pid = own and self_pid or peer_pids[addr],
|
|
own = own,
|
|
})
|
|
end
|
|
return result
|
|
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 expected_uis integer Number of UIs expected to reattach (0 = don't wait).
|
|
function M.rebind_after_restart(canonical_addr, expected_uis)
|
|
M.restart_canonical_addr = canonical_addr
|
|
local bootstrap_addr = vim.v.servername -- Temporary autogenerated address.
|
|
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()
|
|
if timer:is_closing() then
|
|
return
|
|
end
|
|
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 = vim._with({ log_level = 5 }, function()
|
|
return pcall(vim.fn.serverstart, canonical_addr)
|
|
end)
|
|
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()
|
|
if timer:is_closing() then
|
|
return
|
|
end
|
|
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
|
|
vim._with({ log_level = 5 }, function()
|
|
pcall(vim.fn.serverstop, bootstrap_addr)
|
|
end)
|
|
end
|
|
timer:stop()
|
|
timer:close()
|
|
end
|
|
end)
|
|
end)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
return M
|