What
inspect_server() connects to every discovered nvim socket and issues a synchronous, un-timed-out vim.fn.rpcrequest() to read its label/cwd:
-- lua/config/server.lua:116-123
local connected, read, result = with_rpc_channel(path, function(chan)
return vim.fn.rpcrequest(
chan,
"nvim_exec_lua",
"local name = ...; return { vim.g[name] or false, vim.fn.getcwd() }",
{ SERVER_LABEL_VAR }
)
end)
with_rpc_channel() (lua/config/server.lua:87-95) wraps sockconnect+the callback in pcall, but a pcall only catches errors — it does not bound time. rpcrequest() blocks the entire main loop (all UI redraws, all keystrokes) until the far end replies or the channel errors out.
Where
lua/config/server.lua:87-95 — with_rpc_channel(), no timeout around the RPC call
lua/config/server.lua:111-133 — inspect_server(), calls it once per discovered socket
lua/config/server.lua:135-160 — list_nvim_servers(), loops inspect_server() synchronously over every nvim.* socket under run_root(), including every locally-forwarded remote (forward_remote(), line 486-520)
lua/config/server.lua:259-266 — rename_server() has the identical un-timed-out rpcrequest pattern for the non-local-server branch
- Callers that hit this on the main loop:
:ShowNvimServers (line 647-652), :NvimHop → open_hop_picker → hop_entries → list_nvim_servers (line 561-570), and the VimEnter autopick autocmd (line 689-706, runs on every plain nvim launch)
Why it matters
forward_remote() keeps an ssh -N -L tunnel alive as a local socket proxy for a remote nvim server. If the remote process hangs (suspended, OOM-thrashing, network partition on the far side of an already-established TCP connection), the local proxy socket still accepts the sockconnect — the hang only shows up once rpcrequest is waiting for a response that never comes. Since there's no timeout, Neovim's UI freezes completely (no redraws, no <C-c> cancel) until either the ssh ControlPersist connection notices the dead peer (can be minutes, depending on TCP keepalive/OS defaults) or something external kills the process.
This is user-triggered on every NvimHop/ShowNvimServers invocation, and silently on every fresh nvim launch via the VimEnter autopick path — so a single stale remote session can make even a brand-new local nvim appear to hang for no visible reason.
Recommended action
This needs a design decision (sync vs. async RPC, timeout duration, UX while waiting), so it's filed as an issue rather than a mechanical PR. Options:
- Bound it with
vim.wait() + a request id, e.g. issue the RPC as rpcrequest from a coroutine (the repo already has lib/async.lua) and race it against a vim.defer_fn timeout that force-closes the channel (vim.fn.chanclose(chan)) and treats the server as dead — mirrors how list_nvim_servers() already treats a failed connect (cleanup_forward).
- Move discovery off the synchronous path: use
vim.system/async channel APIs so list_nvim_servers() can be called with a callback instead of blocking, and have ShowNvimServers/NvimHop render a partial list that fills in as responses arrive.
- At minimum, apply the same timeout treatment to
rename_server()'s rpcrequest call (line 259-266), which has the identical unbounded-wait shape.
Either way, a socket that fails to answer within a couple hundred ms should be treated the same as a dead socket (cleaned up via the existing cleanup_forward/os.remove paths) rather than left to hang the caller indefinitely.
What
inspect_server()connects to every discovered nvim socket and issues a synchronous, un-timed-outvim.fn.rpcrequest()to read its label/cwd:with_rpc_channel()(lua/config/server.lua:87-95) wrapssockconnect+the callback inpcall, but apcallonly catches errors — it does not bound time.rpcrequest()blocks the entire main loop (all UI redraws, all keystrokes) until the far end replies or the channel errors out.Where
lua/config/server.lua:87-95—with_rpc_channel(), no timeout around the RPC calllua/config/server.lua:111-133—inspect_server(), calls it once per discovered socketlua/config/server.lua:135-160—list_nvim_servers(), loopsinspect_server()synchronously over everynvim.*socket underrun_root(), including every locally-forwarded remote (forward_remote(), line 486-520)lua/config/server.lua:259-266—rename_server()has the identical un-timed-outrpcrequestpattern for the non-local-server branch:ShowNvimServers(line 647-652),:NvimHop→open_hop_picker→hop_entries→list_nvim_servers(line 561-570), and theVimEnterautopick autocmd (line 689-706, runs on every plainnvimlaunch)Why it matters
forward_remote()keeps anssh -N -Ltunnel alive as a local socket proxy for a remote nvim server. If the remote process hangs (suspended, OOM-thrashing, network partition on the far side of an already-established TCP connection), the local proxy socket still accepts thesockconnect— the hang only shows up oncerpcrequestis waiting for a response that never comes. Since there's no timeout, Neovim's UI freezes completely (no redraws, no<C-c>cancel) until either the sshControlPersistconnection notices the dead peer (can be minutes, depending on TCP keepalive/OS defaults) or something external kills the process.This is user-triggered on every
NvimHop/ShowNvimServersinvocation, and silently on every freshnvimlaunch via theVimEnterautopick path — so a single stale remote session can make even a brand-new localnvimappear to hang for no visible reason.Recommended action
This needs a design decision (sync vs. async RPC, timeout duration, UX while waiting), so it's filed as an issue rather than a mechanical PR. Options:
vim.wait()+ a request id, e.g. issue the RPC asrpcrequestfrom a coroutine (the repo already haslib/async.lua) and race it against avim.defer_fntimeout that force-closes the channel (vim.fn.chanclose(chan)) and treats the server as dead — mirrors howlist_nvim_servers()already treats a failed connect (cleanup_forward).vim.system/async channel APIs solist_nvim_servers()can be called with a callback instead of blocking, and haveShowNvimServers/NvimHoprender a partial list that fills in as responses arrive.rename_server()'srpcrequestcall (line 259-266), which has the identical unbounded-wait shape.Either way, a socket that fails to answer within a couple hundred ms should be treated the same as a dead socket (cleaned up via the existing
cleanup_forward/os.removepaths) rather than left to hang the caller indefinitely.