feat(channel): add ChanClose event #40568

Problem:
Plugins using RPC sockets cannot detect when the peer closes a
`sockconnect()` channel, so reconnect logic has no reliable trigger.

Solution:
Add a `ChanClose` event with channel info before the channel is removed,
matching the existing `ChanOpen`/`ChanInfo` event model.
This commit is contained in:
Barrett Ruth
2026-07-04 14:21:25 -05:00
committed by GitHub
parent f0559997dd
commit d0a262db88
10 changed files with 63 additions and 5 deletions

View File

@@ -397,6 +397,13 @@ BufWriteCmd Before writing the whole buffer to a file.
BufWritePost After writing the whole buffer to a file
(should undo the commands for BufWritePre).
*ChanClose*
ChanClose After a channel was closed, before it is removed.
This is triggered even when inside an
autocommand defined without |autocmd-nested|.
Sets these |v:event| keys:
info as from |nvim_get_chan_info()|
*ChanInfo*
ChanInfo State of channel changed, for instance the
client of a RPC channel described itself.

View File

@@ -219,6 +219,7 @@ EVENTS
• |:delmarks| now triggers the |MarkSet| autocommand with line==col==0, same
as |nvim_buf_del_mark()|
• |ChanClose| is triggered after a channel is closed, before it is removed.
• |SessionWritePre| event emits just before |:mksession|.
• |TextPutPre| and |TextPutPost| are triggered before/after putting text.
• |TabMoved| is triggered when tabs are reordered.

View File

@@ -2544,6 +2544,7 @@ A jump table for the options with a short description can be found at |Q_op|.
Note: The following events are considered to happen outside of a
window context and thus cannot be ignored by 'eventignorewin':
|ChanClose|,
|ChanInfo|,
|ChanOpen|,
|CmdUndefined|,

View File

@@ -102,6 +102,7 @@ error('Cannot require a meta file')
--- |'BufWriteCmd'
--- |'BufWritePost'
--- |'BufWritePre'
--- |'ChanClose'
--- |'ChanInfo'
--- |'ChanOpen'
--- |'CmdUndefined'

View File

@@ -2191,6 +2191,7 @@ vim.go.ei = vim.go.eventignore
--- Note: The following events are considered to happen outside of a
--- window context and thus cannot be ignored by 'eventignorewin':
---
--- `ChanClose`,
--- `ChanInfo`,
--- `ChanOpen`,
--- `CmdUndefined`,

View File

@@ -22,6 +22,7 @@ return {
BufWriteCmd = true, -- write buffer using command
BufWritePost = true, -- after writing a buffer
BufWritePre = true, -- before writing a buffer
ChanClose = false,
ChanInfo = false, -- info was received about channel
ChanOpen = false, -- channel was opened
CmdUndefined = false, -- command undefined

View File

@@ -263,7 +263,7 @@ void channel_create_event(Channel *chan, const char *ext_source)
(void)ext_source;
#endif
channel_info_changed(chan, true);
channel_event(chan, EVENT_CHANOPEN);
}
void channel_incref(Channel *chan)
@@ -273,6 +273,11 @@ void channel_incref(Channel *chan)
void channel_decref(Channel *chan)
{
if (chan->refcount == 1 && !chan->did_close_event) {
chan->did_close_event = true;
channel_event(chan, EVENT_CHANCLOSE);
}
if (!(--chan->refcount)) {
// delay free, so that libuv is done with the handles
multiqueue_put(main_loop.events, free_channel_event, chan);
@@ -951,9 +956,8 @@ static void term_close(void *data)
multiqueue_put(chan->events, term_delayed_free, data);
}
void channel_info_changed(Channel *chan, bool new_chan)
void channel_event(Channel *chan, event_T event)
{
event_T event = new_chan ? EVENT_CHANOPEN : EVENT_CHANINFO;
if (has_event(event)) {
channel_incref(chan);
multiqueue_put(main_loop.events, set_info_event, chan, (void *)(intptr_t)event);

View File

@@ -4,6 +4,7 @@
#include <stdint.h>
#include <stdlib.h>
#include "nvim/autocmd_defs.h"
#include "nvim/channel_defs.h" // IWYU pragma: keep
#include "nvim/eval/typval_defs.h"
#include "nvim/event/defs.h"
@@ -44,6 +45,7 @@ struct Channel {
bool callback_busy;
bool callback_scheduled;
bool did_close_event;
};
#include "channel.h.generated.h"

View File

@@ -675,7 +675,7 @@ void rpc_set_client_info(uint64_t id, Dict info)
chan->rpc.client_type = kClientTypeUnknown;
}
channel_info_changed(chan, false);
channel_event(chan, EVENT_CHANINFO);
}
Dict rpc_client_info(Channel *chan)

View File

@@ -33,7 +33,7 @@ describe('channels', function()
source(init)
end)
pending('can connect to socket', function()
it('can connect to socket', function()
local server = n.new_session(true)
set_session(server)
local address = fn.serverlist()[1]
@@ -62,6 +62,46 @@ describe('channels', function()
eq({ 'notification', 'data', { id, { '' } } }, next_msg())
end)
it('emits ChanClose when RPC socket reaches EOF', function()
local client = get_session()
local server = n.new_session(true)
finally(function()
set_session(client)
if server then
server:close()
end
end)
set_session(server)
local address = fn.serverlist()[1]
set_session(client)
api.nvim_set_var('address', address)
exec_lua(function()
_G.closed_event = nil
vim.api.nvim_create_autocmd('ChanClose', {
callback = function()
_G.closed_event = vim.deepcopy(vim.v.event.info)
end,
})
end)
command("let g:id = sockconnect('pipe', address, {'rpc': v:true})")
local id = eval('g:id')
ok(id > 0)
eq(5, eval("rpcrequest(g:id, 'nvim_eval', '2+3')"))
server:close()
server = nil
set_session(client)
retry(nil, 3000, function()
local info = exec_lua(function()
return _G.closed_event
end)
eq(id, info.id)
eq('socket', info.stream)
eq('rpc', info.mode)
end)
end)
it('dont crash due to garbage in rpc #23781', function()
local client = get_session()
local server = n.new_session(true)