fix(terminal): free terminal if close_buffer() closes a closed terminal (#16264)

Use the (currently unused) 'destroy' field of the terminal struct as a
flag to indicate that the terminal's destruction is imminent (and
therefore it's close callback should not be called again).

Co-authored-by: Gregory Anders <greg@gpanders.com>
This commit is contained in:
zeertzjq 2021-11-11 06:28:55 +08:00 committed by GitHub
parent 2ecf0a4c61
commit 14def4d227
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -124,7 +124,9 @@ struct terminal {
// no way to know if the memory was reused. // no way to know if the memory was reused.
handle_T buf_handle; handle_T buf_handle;
// program exited // program exited
bool closed, destroy; bool closed;
// when true, the terminal's destruction is already enqueued.
bool destroy;
// some vterm properties // some vterm properties
bool forward_mouse; bool forward_mouse;
@ -261,40 +263,66 @@ Terminal *terminal_open(buf_T *buf, TerminalOptions opts)
void terminal_close(Terminal *term, int status) void terminal_close(Terminal *term, int status)
{ {
if (term->closed) { if (term->destroy) {
return; return;
} }
term->forward_mouse = false; #ifdef EXITFREE
if (entered_free_all_mem) {
// If called from close_buffer() inside free_all_mem(), the main loop has
// already been freed, so it is not safe to call the close callback here.
terminal_destroy(term);
return;
}
#endif
// flush any pending changes to the buffer bool only_destroy = false;
if (!exiting) {
block_autocmds(); if (term->closed) {
refresh_terminal(term); // If called from close_buffer() after the process has already exited, we
unblock_autocmds(); // only need to call the close callback to clean up the terminal object.
only_destroy = true;
} else {
term->forward_mouse = false;
// flush any pending changes to the buffer
if (!exiting) {
block_autocmds();
refresh_terminal(term);
unblock_autocmds();
}
term->closed = true;
} }
buf_T *buf = handle_get_buffer(term->buf_handle); buf_T *buf = handle_get_buffer(term->buf_handle);
term->closed = true;
if (status == -1 || exiting) { if (status == -1 || exiting) {
// If status is -1, this was called by close_buffer(buffer.c). Or if // If this was called by close_buffer() (status is -1), or if exiting, we
// exiting, we must inform the buffer the terminal no longer exists so that // must inform the buffer the terminal no longer exists so that
// close_buffer() doesn't call this again. // close_buffer() won't call this again.
// If inside Terminal mode K_EVENT handling, setting buf_handle to 0 also
// informs terminal_enter() to call the close callback before returning.
term->buf_handle = 0; term->buf_handle = 0;
if (buf) { if (buf) {
buf->terminal = NULL; buf->terminal = NULL;
} }
if (!term->refcount) { if (!term->refcount) {
// Not inside Terminal mode K_EVENT handling.
// We should not wait for the user to press a key. // We should not wait for the user to press a key.
term->destroy = true;
term->opts.close_cb(term->opts.data); term->opts.close_cb(term->opts.data);
} }
} else { } else if (!only_destroy) {
// This was called by channel_process_exit_cb() not in process_teardown().
// Do not call the close callback now. Wait for the user to press a key.
char msg[sizeof("\r\n[Process exited ]") + NUMBUFLEN]; char msg[sizeof("\r\n[Process exited ]") + NUMBUFLEN];
snprintf(msg, sizeof msg, "\r\n[Process exited %d]", status); snprintf(msg, sizeof msg, "\r\n[Process exited %d]", status);
terminal_receive(term, msg, strlen(msg)); terminal_receive(term, msg, strlen(msg));
} }
if (only_destroy) {
return;
}
if (buf && !is_autocmd_blocked()) { if (buf && !is_autocmd_blocked()) {
dict_T *dict = get_vim_var_dict(VV_EVENT); dict_T *dict = get_vim_var_dict(VV_EVENT);
tv_dict_add_nr(dict, S_LEN("status"), status); tv_dict_add_nr(dict, S_LEN("status"), status);
@ -417,6 +445,7 @@ void terminal_enter(void)
ui_busy_stop(); ui_busy_stop();
if (s->close) { if (s->close) {
bool wipe = s->term->buf_handle != 0; bool wipe = s->term->buf_handle != 0;
s->term->destroy = true;
s->term->opts.close_cb(s->term->opts.data); s->term->opts.close_cb(s->term->opts.data);
if (wipe) { if (wipe) {
do_cmdline_cmd("bwipeout!"); do_cmdline_cmd("bwipeout!");