fix(rpc): trigger UILeave earlier on channel close (#38846)

Problem:
On exit, rpc_free() is called when processing main_loop.events after
libuv calls close callbacks of the channel's stream. However, when there
are no child processes, these libuv callbacks are called in loop_close()
instead of proc_teardown(), and main_loop.events isn't processed after
loop_close(). As a result, calling remote_ui_disconnect() in rpc_free()
causes UILeave to depend on the presence of child processes.

Solution:
Always call remote_ui_disconnect() in rpc_close_event(), and remove the
call in rpc_free().

(cherry picked from commit 5d66ef188f)
This commit is contained in:
zeertzjq
2026-04-08 05:29:08 +08:00
committed by github-actions[bot]
parent a358b9be64
commit eee2d10fd2
3 changed files with 71 additions and 28 deletions

View File

@@ -162,16 +162,65 @@ it('autocmds UIEnter/UILeave', function()
autocmd UIEnter * call add(g:evs, "UIEnter") | let g:uienter_ev = deepcopy(v:event)
autocmd UILeave * call add(g:evs, "UILeave") | let g:uileave_ev = deepcopy(v:event)
autocmd VimEnter * call add(g:evs, "VimEnter")
autocmd VimLeave * call add(g:evs, "VimLeave")
]])
local screen = Screen.new()
eq({ chan = 1 }, eval('g:uienter_ev'))
eq({ 'VimEnter', 'UIEnter' }, eval('g:evs'))
screen:detach()
eq({ chan = 1 }, eval('g:uileave_ev'))
eq({
'VimEnter',
'UIEnter',
'UILeave',
}, eval('g:evs'))
eq({ 'VimEnter', 'UIEnter', 'UILeave' }, eval('g:evs'))
local servername = api.nvim_get_vvar('servername')
local session2 = n.connect(servername)
local status2, chan2 = session2:request('nvim_get_chan_info', 0)
t.ok(status2)
local session3 = n.connect(servername)
local status3, chan3 = session3:request('nvim_get_chan_info', 0)
t.ok(status3)
local screen2 = Screen.new(nil, nil, nil, session2)
eq({ chan = chan2.id }, eval('g:uienter_ev'))
eq({ 'VimEnter', 'UIEnter', 'UILeave', 'UIEnter' }, eval('g:evs'))
screen2:detach()
eq({ chan = chan2.id }, eval('g:uileave_ev'))
eq({ 'VimEnter', 'UIEnter', 'UILeave', 'UIEnter', 'UILeave' }, eval('g:evs'))
command('let g:evs = ["…"]')
screen2:attach(session2)
eq({ chan = chan2.id }, eval('g:uienter_ev'))
eq({ '', 'UIEnter' }, eval('g:evs'))
Screen.new(nil, nil, nil, session3)
eq({ chan = chan3.id }, eval('g:uienter_ev'))
eq({ '', 'UIEnter', 'UIEnter' }, eval('g:evs'))
screen:attach(n.get_session())
eq({ chan = 1 }, eval('g:uienter_ev'))
eq({ '', 'UIEnter', 'UIEnter', 'UIEnter' }, eval('g:evs'))
session3:close()
t.retry(nil, 1000, function()
eq({}, api.nvim_get_chan_info(chan3.id))
end)
eq({ chan = chan3.id }, eval('g:uileave_ev'))
eq({ '', 'UIEnter', 'UIEnter', 'UIEnter', 'UILeave' }, eval('g:evs'))
command('let g:evs = ["…"]')
command('autocmd UILeave * call writefile(g:evs, "Xevents.log")')
finally(function()
os.remove('Xevents.log')
end)
n.expect_exit(command, 'qall!')
n.check_close() -- Wait for process exit.
-- UILeave should have been triggered for both remaining UIs.
eq('\nVimLeave\nUILeave\nUILeave\n', t.read_file('Xevents.log'))
end)
it('autocmds VimSuspend/VimResume #22041', function()