mirror of
https://github.com/neovim/neovim.git
synced 2026-03-31 04:42:03 +00:00
fix(autocmd): deferred TermResponse lacks "data", may not fire (#37778)
Problem: TermResponse deferred due to blocked autocommands lacks "data" payload. Also, it may not fire if a new v:termresponse reuses the same string address. Solution: add it. Use the value of v:termresponse for "data.sequence". Replace pointer comparisons with a flag. The removal of "old_termresponse" comparisons is required to pass the test on the CI, or locally for me when compiled in RelWithDebInfo.
This commit is contained in:
@@ -67,10 +67,6 @@ void nvim_ui_term_event(uint64_t channel_id, String event, Object value, Error *
|
||||
|
||||
const String termresponse = value.data.string;
|
||||
set_vim_var_string(VV_TERMRESPONSE, termresponse.data, (ptrdiff_t)termresponse.size);
|
||||
|
||||
MAXSIZE_TEMP_DICT(data, 1);
|
||||
PUT_C(data, "sequence", value);
|
||||
apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, NULL,
|
||||
&DICT_OBJ(data));
|
||||
do_termresponse_autocmd(termresponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ static int autocmd_blocked = 0; // block all autocmds
|
||||
static bool autocmd_nested = false;
|
||||
static bool autocmd_include_groups = false;
|
||||
|
||||
static char *old_termresponse = NULL;
|
||||
static bool termresponse_changed = false;
|
||||
|
||||
// Map of autocmd group names and ids.
|
||||
// name -> ID
|
||||
@@ -2033,13 +2033,22 @@ BYPASS_AU:
|
||||
return retval;
|
||||
}
|
||||
|
||||
void do_termresponse_autocmd(const String sequence)
|
||||
{
|
||||
MAXSIZE_TEMP_DICT(data, 1);
|
||||
PUT_C(data, "sequence", STRING_OBJ(sequence));
|
||||
apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, true, AUGROUP_ALL, NULL, NULL,
|
||||
&DICT_OBJ(data));
|
||||
termresponse_changed = true;
|
||||
}
|
||||
|
||||
// Block triggering autocommands until unblock_autocmd() is called.
|
||||
// Can be used recursively, so long as it's symmetric.
|
||||
void block_autocmds(void)
|
||||
{
|
||||
// Remember the value of v:termresponse.
|
||||
// Detect if v:termresponse is set while blocked.
|
||||
if (!is_autocmd_blocked()) {
|
||||
old_termresponse = get_vim_var_str(VV_TERMRESPONSE);
|
||||
termresponse_changed = false;
|
||||
}
|
||||
autocmd_blocked++;
|
||||
}
|
||||
@@ -2051,8 +2060,11 @@ void unblock_autocmds(void)
|
||||
// When v:termresponse was set while autocommands were blocked, trigger
|
||||
// the autocommands now. Esp. useful when executing a shell command
|
||||
// during startup (nvim -d).
|
||||
if (!is_autocmd_blocked() && get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) {
|
||||
apply_autocmds(EVENT_TERMRESPONSE, NULL, NULL, false, curbuf);
|
||||
if (!is_autocmd_blocked() && termresponse_changed && has_event(EVENT_TERMRESPONSE)) {
|
||||
// Copied to a new allocation, as termresponse may be freed during the event.
|
||||
const String sequence = cstr_to_string(get_vim_var_str(VV_TERMRESPONSE));
|
||||
do_termresponse_autocmd(sequence);
|
||||
api_free_string(sequence);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2707,6 +2707,71 @@ describe('TUI', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
it('TermResponse from unblock_autocmds() sets "data"', function()
|
||||
if not child_exec_lua('return pcall(require, "ffi")') then
|
||||
pending('N/A: missing LuaJIT FFI')
|
||||
end
|
||||
child_exec_lua([[
|
||||
local ffi = require('ffi')
|
||||
ffi.cdef[=[
|
||||
void block_autocmds(void);
|
||||
void unblock_autocmds(void);
|
||||
]=]
|
||||
ffi.C.block_autocmds()
|
||||
vim.api.nvim_create_autocmd('TermResponse', {
|
||||
once = true,
|
||||
callback = function(ev)
|
||||
_G.data = ev.data
|
||||
end,
|
||||
})
|
||||
]])
|
||||
feed_data('\027P0$r\027\\')
|
||||
retry(nil, 4000, function()
|
||||
eq('\027P0$r', child_exec_lua('return vim.v.termresponse'))
|
||||
end)
|
||||
eq(vim.NIL, child_exec_lua('return _G.data'))
|
||||
child_exec_lua('require("ffi").C.unblock_autocmds()')
|
||||
eq({ sequence = '\027P0$r' }, child_exec_lua('return _G.data'))
|
||||
|
||||
-- If TermResponse during TermResponse changes v:termresponse, data.sequence contains the actual
|
||||
-- response that triggered the autocommand.
|
||||
-- The second autocommand below forces a use-after-free when v:termresponse's value changes
|
||||
-- during TermResponse if data.sequence didn't allocate its own copy.
|
||||
child_exec_lua([[
|
||||
require('ffi').C.block_autocmds()
|
||||
vim.api.nvim_create_autocmd('TermResponse', {
|
||||
once = true,
|
||||
callback = function(ev)
|
||||
_G.au1_termresponse1 = vim.v.termresponse
|
||||
_G.au1_sequence1 = ev.data.sequence
|
||||
local chan = vim.fn.sockconnect('pipe', vim.v.servername, { rpc = true })
|
||||
vim.rpcrequest(chan, 'nvim_ui_term_event', 'termresponse', 'baz')
|
||||
_G.au1_termresponse2 = vim.v.termresponse
|
||||
_G.au1_sequence2 = ev.data.sequence
|
||||
end,
|
||||
})
|
||||
_G.au2_sequences = {}
|
||||
vim.api.nvim_create_autocmd('TermResponse', {
|
||||
callback = function(ev)
|
||||
table.insert(_G.au2_sequences, ev.data.sequence)
|
||||
end,
|
||||
})
|
||||
]])
|
||||
child_session:request('nvim_ui_term_event', 'termresponse', 'foobar')
|
||||
eq('foobar', child_exec_lua('return vim.v.termresponse'))
|
||||
-- For good measure, check deferred TermResponse doesn't try to fire if autocmds are still
|
||||
-- blocked after unblock_autocmds.
|
||||
child_exec_lua('require("ffi").C.block_autocmds() require("ffi").C.unblock_autocmds()')
|
||||
eq(vim.NIL, child_exec_lua('return _G.au1_termresponse1'))
|
||||
child_exec_lua('require("ffi").C.unblock_autocmds()')
|
||||
eq('foobar', child_exec_lua('return _G.au1_termresponse1'))
|
||||
eq('foobar', child_exec_lua('return _G.au1_sequence1'))
|
||||
eq('baz', child_exec_lua('return _G.au1_termresponse2'))
|
||||
eq('foobar', child_exec_lua('return _G.au1_sequence2')) -- unchanged
|
||||
-- Second autocmd triggers due to "baz" (via the nested TermResponse), then from "foobar".
|
||||
eq({ 'baz', 'foobar' }, child_exec_lua('return _G.au2_sequences'))
|
||||
end)
|
||||
|
||||
it('nvim_ui_send works', function()
|
||||
child_session:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\')
|
||||
retry(nil, nil, function()
|
||||
|
||||
Reference in New Issue
Block a user