1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
|
-- For "--listen", ":restart", and related remote/server functionality.
local util = require('vim._core.util')
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.
---@field own boolean True if this server belongs to the current Nvim instance.
---@field active integer Timestamp of last user activity.
--- @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 want_info = opts.info == true
if opts.peer ~= true and not want_info then
return addrs
end
local seen = {} ---@type table<string, true>
for _, a in ipairs(addrs) do
seen[a] = true
end
local peers ---@type string[]|vim.ServerInfo[]
if want_info then
local self_pid = vim.fn.getpid()
local self_active = vim.v.useractive
peers = vim.tbl_map(function(addr)
return {
addr = addr,
pid = self_pid,
own = true,
active = self_active,
}
end, addrs)
else
peers = addrs
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 })
for _, socket in ipairs(socket_paths) do
if not seen[socket] then
local ok, chan = pcall(vim.fn.sockconnect, 'pipe', socket, { rpc = true })
if ok and chan and chan > 0 then
--- @cast chan integer
-- Check that the server is responding
-- TODO: do we need a timeout or error handling here?
local ok_rpc, peer_info = pcall(
vim.fn.rpcrequest,
chan,
'nvim_exec_lua',
[[return { pid = vim.fn.getpid(), active = vim.v.useractive, }]],
{}
)
if ok_rpc and type(peer_info) == 'table' then
---@cast peer_info vim.ServerInfo
seen[socket] = true
if want_info then
table.insert(peers, {
addr = socket,
pid = peer_info.pid,
own = false,
active = peer_info.active,
})
else
table.insert(peers, socket)
end
end
pcall(vim.fn.chanclose, chan)
end
end
end
return peers
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
-- Called by ex_restart(). Saves the current session and calls ex_restart() (again) with the
-- updated arguments.
--
-- TODO: https://github.com/neovim/neovim/issues/34204
--
--- @param eap vim._core.ExCmdArgs
--- @param extra { quit_cmd: string }
function M.ex_session_restart(eap, extra)
local after_cmd = eap.args -- User-provided [command].
assert(not after_cmd:find(']==]'))
-- Use custom +cmd if given.
local quit_cmd = extra.quit_cmd == '' and 'qall' or extra.quit_cmd
-- Preserve v:this_session in the restarted Nvim.
local this_session = vim.v.this_session
assert(not this_session:find(']==]'))
-- Nvim temp "root dir": "/tmp/…/nvim.<user>/"
local tmproot = vim.fs.abspath(vim.fs.dirname(vim.fs.dirname(vim.fn.tempname())))
assert(not tmproot:find(']==]'))
local fd, session = vim.uv.fs_mkstemp(vim.fs.joinpath(tmproot, 'restart_session_XXXXXX'))
if not fd then
error('Failed to get temporary filename for restart session')
end
vim.uv.fs_close(fd)
-- Write session
local session_arg = vim.fn.fnameescape(session)
vim.cmd('%argdelete')
vim.cmd.mksession { session_arg, bang = true }
-- Lua commands to restore the session and remove the session file
local after_list = {}
table.insert(after_list, ('vim.cmd("source %s")'):format(session_arg))
table.insert(after_list, ('pcall(vim.fs.rm, [==[%s]==])'):format(session))
table.insert(after_list, ('vim.v.this_session = [==[%s]==]'):format(this_session))
if after_cmd ~= '' then
table.insert(after_list, ('vim.cmd([==[%s]==])'):format(after_cmd))
end
local after = 'lua ' .. table.concat(after_list, ';')
-- Restart Neovim and run our Lua commands
local success, msg = pcall(function()
-- "+:::" dummy prefix tells the C handler that this is actually a non-bang restart.
-- Then v:startreason (if any) can be preserved.
vim.cmd.restart { '+:::', quit_cmd, after, bang = true }
end)
if not success then
--- @cast msg string
vim.fs.rm(session, { force = true })
-- Trim error message to be equivalent to `:restart!`
if msg:find('Vim:') then
util.echo_err(util.cmd_errmsg(msg))
else
error(msg)
end
end
end
return M
|