mirror of
https://github.com/neovim/neovim.git
synced 2026-08-30 19:11:52 +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;
|
const String termresponse = value.data.string;
|
||||||
set_vim_var_string(VV_TERMRESPONSE, termresponse.data, (ptrdiff_t)termresponse.size);
|
set_vim_var_string(VV_TERMRESPONSE, termresponse.data, (ptrdiff_t)termresponse.size);
|
||||||
|
do_termresponse_autocmd(termresponse);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ static int autocmd_blocked = 0; // block all autocmds
|
|||||||
static bool autocmd_nested = false;
|
static bool autocmd_nested = false;
|
||||||
static bool autocmd_include_groups = false;
|
static bool autocmd_include_groups = false;
|
||||||
|
|
||||||
static char *old_termresponse = NULL;
|
static bool termresponse_changed = false;
|
||||||
|
|
||||||
// Map of autocmd group names and ids.
|
// Map of autocmd group names and ids.
|
||||||
// name -> ID
|
// name -> ID
|
||||||
@@ -2033,13 +2033,22 @@ BYPASS_AU:
|
|||||||
return retval;
|
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.
|
// Block triggering autocommands until unblock_autocmd() is called.
|
||||||
// Can be used recursively, so long as it's symmetric.
|
// Can be used recursively, so long as it's symmetric.
|
||||||
void block_autocmds(void)
|
void block_autocmds(void)
|
||||||
{
|
{
|
||||||
// Remember the value of v:termresponse.
|
// Detect if v:termresponse is set while blocked.
|
||||||
if (!is_autocmd_blocked()) {
|
if (!is_autocmd_blocked()) {
|
||||||
old_termresponse = get_vim_var_str(VV_TERMRESPONSE);
|
termresponse_changed = false;
|
||||||
}
|
}
|
||||||
autocmd_blocked++;
|
autocmd_blocked++;
|
||||||
}
|
}
|
||||||
@@ -2051,8 +2060,11 @@ void unblock_autocmds(void)
|
|||||||
// When v:termresponse was set while autocommands were blocked, trigger
|
// When v:termresponse was set while autocommands were blocked, trigger
|
||||||
// the autocommands now. Esp. useful when executing a shell command
|
// the autocommands now. Esp. useful when executing a shell command
|
||||||
// during startup (nvim -d).
|
// during startup (nvim -d).
|
||||||
if (!is_autocmd_blocked() && get_vim_var_str(VV_TERMRESPONSE) != old_termresponse) {
|
if (!is_autocmd_blocked() && termresponse_changed && has_event(EVENT_TERMRESPONSE)) {
|
||||||
apply_autocmds(EVENT_TERMRESPONSE, NULL, NULL, false, curbuf);
|
// 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)
|
||||||
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()
|
it('nvim_ui_send works', function()
|
||||||
child_session:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\')
|
child_session:request('nvim_ui_send', '\027]2;TEST_TITLE\027\\')
|
||||||
retry(nil, nil, function()
|
retry(nil, nil, function()
|
||||||
|
|||||||
Reference in New Issue
Block a user