mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 19:38:20 +00:00
fix(tui): wait for embedded server's exit code
Uses the undocumented "error_exit" UI event for a different purpose: When :detach is used on the server, send an "error_exit" with 0 `status` to indicate that the server shouldn't wait for client exit.
This commit is contained in:
@@ -71,12 +71,26 @@ static void remote_ui_destroy(RemoteUI *ui)
|
|||||||
xfree(ui);
|
xfree(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
void remote_ui_disconnect(uint64_t channel_id)
|
/// Removes the client on the given channel from the list of UIs.
|
||||||
|
///
|
||||||
|
/// @param err if non-NULL and there is no UI on the channel, set an error
|
||||||
|
/// @param send_error_exit send an "error_exit" event with 0 status first
|
||||||
|
void remote_ui_disconnect(uint64_t channel_id, Error *err, bool send_error_exit)
|
||||||
{
|
{
|
||||||
RemoteUI *ui = pmap_get(uint64_t)(&connected_uis, channel_id);
|
RemoteUI *ui = pmap_get(uint64_t)(&connected_uis, channel_id);
|
||||||
if (!ui) {
|
if (!ui) {
|
||||||
|
if (err != NULL) {
|
||||||
|
api_set_error(err, kErrorTypeException,
|
||||||
|
"UI not attached to channel: %" PRId64, channel_id);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (send_error_exit) {
|
||||||
|
MAXSIZE_TEMP_ARRAY(args, 1);
|
||||||
|
ADD_C(args, INTEGER_OBJ(0));
|
||||||
|
push_call(ui, "error_exit", args);
|
||||||
|
ui_flush_buf(ui);
|
||||||
|
}
|
||||||
pmap_del(uint64_t)(&connected_uis, channel_id, NULL);
|
pmap_del(uint64_t)(&connected_uis, channel_id, NULL);
|
||||||
ui_detach_impl(ui, channel_id);
|
ui_detach_impl(ui, channel_id);
|
||||||
remote_ui_destroy(ui);
|
remote_ui_destroy(ui);
|
||||||
@@ -231,12 +245,7 @@ void nvim_ui_set_focus(uint64_t channel_id, Boolean gained, Error *error)
|
|||||||
void nvim_ui_detach(uint64_t channel_id, Error *err)
|
void nvim_ui_detach(uint64_t channel_id, Error *err)
|
||||||
FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY
|
FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY
|
||||||
{
|
{
|
||||||
if (!map_has(uint64_t, &connected_uis, channel_id)) {
|
remote_ui_disconnect(channel_id, err, false);
|
||||||
api_set_error(err, kErrorTypeException,
|
|
||||||
"UI not attached to channel: %" PRId64, channel_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
remote_ui_disconnect(channel_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(bfredl): use me to detach a specific ui from the server
|
// TODO(bfredl): use me to detach a specific ui from the server
|
||||||
|
@@ -173,5 +173,13 @@ void msg_history_show(Array entries)
|
|||||||
void msg_history_clear(void)
|
void msg_history_clear(void)
|
||||||
FUNC_API_SINCE(10) FUNC_API_REMOTE_ONLY;
|
FUNC_API_SINCE(10) FUNC_API_REMOTE_ONLY;
|
||||||
|
|
||||||
|
// This UI event is currently undocumented.
|
||||||
|
// - When the server needs to intentionally exit with an exit code, and there is no
|
||||||
|
// message in server stderr for the user, this event is sent with positive `status`
|
||||||
|
// argument, to indicate that the UI should exit normally with `status`.
|
||||||
|
// - When the server has crashed or there is a message in server stderr for the user,
|
||||||
|
// this event is not sent, and the UI should make server stderr visible.
|
||||||
|
// - When :detach is used on the server, this event is sent with a zero `status`
|
||||||
|
// argument, to indicate that the UI shouldn't wait for server exit.
|
||||||
void error_exit(Integer status)
|
void error_exit(Integer status)
|
||||||
FUNC_API_SINCE(12);
|
FUNC_API_SINCE(12) FUNC_API_CLIENT_IMPL;
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
#include <uv.h>
|
#include <uv.h>
|
||||||
|
|
||||||
#include "klib/kvec.h"
|
#include "klib/kvec.h"
|
||||||
|
#include "nvim/channel.h"
|
||||||
#include "nvim/event/libuv_proc.h"
|
#include "nvim/event/libuv_proc.h"
|
||||||
#include "nvim/event/loop.h"
|
#include "nvim/event/loop.h"
|
||||||
#include "nvim/event/multiqueue.h"
|
#include "nvim/event/multiqueue.h"
|
||||||
@@ -437,16 +438,20 @@ static void on_proc_exit(Proc *proc)
|
|||||||
Loop *loop = proc->loop;
|
Loop *loop = proc->loop;
|
||||||
ILOG("child exited: pid=%d status=%d" PRIu64, proc->pid, proc->status);
|
ILOG("child exited: pid=%d status=%d" PRIu64, proc->pid, proc->status);
|
||||||
|
|
||||||
#ifdef MSWIN
|
// TODO(justinmk): figure out why rpc_close sometimes(??) isn't called.
|
||||||
// XXX: This assumes the TUI never spawns any other processes...?
|
|
||||||
// TODO(justinmk): figure out why rpc_close sometimes(??) isn't called, then remove this jank.
|
|
||||||
// Theories:
|
// Theories:
|
||||||
// - EOF not received in receive_msgpack, then doesn't call chan_close_on_err().
|
// - EOF not received in receive_msgpack, then doesn't call chan_close_on_err().
|
||||||
// - proc_close_handles not tickled by ui_client.c's LOOP_PROCESS_EVENTS?
|
// - proc_close_handles not tickled by ui_client.c's LOOP_PROCESS_EVENTS?
|
||||||
if (ui_client_channel_id) {
|
if (ui_client_channel_id) {
|
||||||
|
uint64_t server_chan_id = ui_client_channel_id;
|
||||||
|
Channel *server_chan = find_channel(server_chan_id);
|
||||||
|
if (server_chan != NULL && server_chan->streamtype == kChannelStreamProc
|
||||||
|
&& proc == &server_chan->stream.proc) {
|
||||||
|
// If the current embedded server has exited,
|
||||||
|
// the client should exit with the same status.
|
||||||
exit_on_closed_chan(proc->status);
|
exit_on_closed_chan(proc->status);
|
||||||
}
|
}
|
||||||
#endif
|
}
|
||||||
|
|
||||||
// Process has terminated, but there could still be data to be read from the
|
// Process has terminated, but there could still be data to be read from the
|
||||||
// OS. We are still in the libuv loop, so we cannot call code that polls for
|
// OS. We are still in the libuv loop, so we cannot call code that polls for
|
||||||
|
@@ -5553,8 +5553,8 @@ static void ex_detach(exarg_T *eap)
|
|||||||
if (eap && eap->forceit) {
|
if (eap && eap->forceit) {
|
||||||
emsg("bang (!) not supported yet");
|
emsg("bang (!) not supported yet");
|
||||||
} else {
|
} else {
|
||||||
// 1. (TODO) Send "detach" UI-event (notification only).
|
// 1. Send "error_exit" UI-event (notification only).
|
||||||
// 2. Perform server-side `nvim_ui_detach`.
|
// 2. Perform server-side UI detach.
|
||||||
// 3. Close server-side channel without self-exit.
|
// 3. Close server-side channel without self-exit.
|
||||||
|
|
||||||
if (!current_ui) {
|
if (!current_ui) {
|
||||||
@@ -5571,7 +5571,7 @@ static void ex_detach(exarg_T *eap)
|
|||||||
|
|
||||||
// Server-side UI detach. Doesn't close the channel.
|
// Server-side UI detach. Doesn't close the channel.
|
||||||
Error err2 = ERROR_INIT;
|
Error err2 = ERROR_INIT;
|
||||||
nvim_ui_detach(chan->id, &err2);
|
remote_ui_disconnect(chan->id, &err2, true);
|
||||||
if (ERROR_SET(&err2)) {
|
if (ERROR_SET(&err2)) {
|
||||||
emsg(err2.msg); // UI disappeared already?
|
emsg(err2.msg); // UI disappeared already?
|
||||||
api_clear_error(&err2);
|
api_clear_error(&err2);
|
||||||
|
@@ -498,18 +498,22 @@ static void rpc_close_event(void **argv)
|
|||||||
if (is_ui_client || channel->streamtype == kChannelStreamStdio) {
|
if (is_ui_client || channel->streamtype == kChannelStreamStdio) {
|
||||||
if (!is_ui_client) {
|
if (!is_ui_client) {
|
||||||
// Avoid hanging when there are no other UIs and a prompt is triggered on exit.
|
// Avoid hanging when there are no other UIs and a prompt is triggered on exit.
|
||||||
remote_ui_disconnect(channel->id);
|
remote_ui_disconnect(channel->id, NULL, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!channel->detach) {
|
if (!channel->detach) {
|
||||||
exit_on_closed_chan(channel->exit_status == -1 ? 0 : channel->exit_status);
|
if (channel->streamtype == kChannelStreamProc && ui_client_error_exit < 0) {
|
||||||
|
// Wait for the embedded server to exit instead of exiting immediately,
|
||||||
|
// as it's necessary to get the server's exit code in on_proc_exit().
|
||||||
|
} else {
|
||||||
|
exit_on_closed_chan(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void rpc_free(Channel *channel)
|
void rpc_free(Channel *channel)
|
||||||
{
|
{
|
||||||
remote_ui_disconnect(channel->id);
|
remote_ui_disconnect(channel->id, NULL, false);
|
||||||
unpacker_teardown(channel->rpc.unpacker);
|
unpacker_teardown(channel->rpc.unpacker);
|
||||||
xfree(channel->rpc.unpacker);
|
xfree(channel->rpc.unpacker);
|
||||||
|
|
||||||
|
@@ -147,7 +147,6 @@ struct TUIData {
|
|||||||
char *space_buf;
|
char *space_buf;
|
||||||
size_t space_buf_len;
|
size_t space_buf_len;
|
||||||
bool stopped;
|
bool stopped;
|
||||||
int seen_error_exit;
|
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
bool rgb;
|
bool rgb;
|
||||||
@@ -170,7 +169,6 @@ void tui_start(TUIData **tui_p, int *width, int *height, char **term, bool *rgb)
|
|||||||
tui->is_starting = true;
|
tui->is_starting = true;
|
||||||
tui->screenshot = NULL;
|
tui->screenshot = NULL;
|
||||||
tui->stopped = false;
|
tui->stopped = false;
|
||||||
tui->seen_error_exit = 0;
|
|
||||||
tui->loop = &main_loop;
|
tui->loop = &main_loop;
|
||||||
tui->url = -1;
|
tui->url = -1;
|
||||||
|
|
||||||
@@ -558,14 +556,16 @@ static void terminfo_disable(TUIData *tui)
|
|||||||
/// Disable the alternate screen and prepare for the TUI to close.
|
/// Disable the alternate screen and prepare for the TUI to close.
|
||||||
static void terminfo_stop(TUIData *tui)
|
static void terminfo_stop(TUIData *tui)
|
||||||
{
|
{
|
||||||
if (ui_client_exit_status == 0) {
|
if (ui_client_exit_status == 0 && ui_client_error_exit > 0) {
|
||||||
ui_client_exit_status = tui->seen_error_exit;
|
ui_client_exit_status = ui_client_error_exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if nvim exited with nonzero status, without indicated this was an
|
// If Nvim exited with nonzero status, without indicating this was an
|
||||||
// intentional exit (like `:1cquit`), it likely was an internal failure.
|
// intentional exit (like `:1cquit`), it likely was an internal failure.
|
||||||
// Don't clobber the stderr error message in this case.
|
// Don't clobber the stderr error message in this case. #21608
|
||||||
if (ui_client_exit_status == tui->seen_error_exit) {
|
if (ui_client_exit_status == MAX(ui_client_error_exit, 0)) {
|
||||||
|
// Position the cursor on the last screen line, below all the text
|
||||||
|
cursor_goto(tui, tui->height - 1, 0);
|
||||||
// Exit alternate screen.
|
// Exit alternate screen.
|
||||||
unibi_out(tui, unibi_exit_ca_mode);
|
unibi_out(tui, unibi_exit_ca_mode);
|
||||||
}
|
}
|
||||||
@@ -604,12 +604,6 @@ static void tui_terminal_after_startup(TUIData *tui)
|
|||||||
flush_buf(tui);
|
flush_buf(tui);
|
||||||
}
|
}
|
||||||
|
|
||||||
void tui_error_exit(TUIData *tui, Integer status)
|
|
||||||
FUNC_ATTR_NONNULL_ALL
|
|
||||||
{
|
|
||||||
tui->seen_error_exit = (int)status;
|
|
||||||
}
|
|
||||||
|
|
||||||
void tui_stop(TUIData *tui)
|
void tui_stop(TUIData *tui)
|
||||||
FUNC_ATTR_NONNULL_ALL
|
FUNC_ATTR_NONNULL_ALL
|
||||||
{
|
{
|
||||||
@@ -650,8 +644,6 @@ static void tui_terminal_stop(TUIData *tui)
|
|||||||
FUNC_ATTR_NONNULL_ALL
|
FUNC_ATTR_NONNULL_ALL
|
||||||
{
|
{
|
||||||
tinput_stop(&tui->input);
|
tinput_stop(&tui->input);
|
||||||
// Position the cursor on the last screen line, below all the text
|
|
||||||
cursor_goto(tui, tui->height - 1, 0);
|
|
||||||
terminfo_stop(tui);
|
terminfo_stop(tui);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -284,6 +284,16 @@ void ui_client_event_raw_line(GridLineEvent *g)
|
|||||||
(const schar_T *)grid_line_buf_char, grid_line_buf_attr);
|
(const schar_T *)grid_line_buf_char, grid_line_buf_attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ui_client_event_error_exit(Array args)
|
||||||
|
{
|
||||||
|
if (args.size < 1
|
||||||
|
|| args.items[0].type != kObjectTypeInteger) {
|
||||||
|
ELOG("Error handling ui event 'error_exit'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui_client_error_exit = (int)args.items[0].data.integer;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef EXITFREE
|
#ifdef EXITFREE
|
||||||
void ui_client_free_all_mem(void)
|
void ui_client_free_all_mem(void)
|
||||||
{
|
{
|
||||||
|
@@ -17,11 +17,12 @@ EXTERN sattr_T *grid_line_buf_attr INIT( = NULL);
|
|||||||
// Client-side UI channel. Zero during early startup or if not a (--remote-ui) UI client.
|
// Client-side UI channel. Zero during early startup or if not a (--remote-ui) UI client.
|
||||||
EXTERN uint64_t ui_client_channel_id INIT( = 0);
|
EXTERN uint64_t ui_client_channel_id INIT( = 0);
|
||||||
|
|
||||||
// exit status from embedded nvim process
|
/// `status` argument of the last "error_exit" UI event, or -1 if none has been seen.
|
||||||
EXTERN int ui_client_exit_status INIT( = 0);
|
/// NOTE: This assumes "error_exit" never has a negative `status` argument.
|
||||||
|
EXTERN int ui_client_error_exit INIT( = -1);
|
||||||
|
|
||||||
// TODO(bfredl): the current structure for how tui and ui_client.c communicate is a bit awkward.
|
/// Server exit code.
|
||||||
// This will be restructured as part of The UI Devirtualization Project.
|
EXTERN int ui_client_exit_status INIT( = 0);
|
||||||
|
|
||||||
/// Whether ui client has sent nvim_ui_attach yet
|
/// Whether ui client has sent nvim_ui_attach yet
|
||||||
EXTERN bool ui_client_attached INIT( = false);
|
EXTERN bool ui_client_attached INIT( = false);
|
||||||
|
@@ -33,6 +33,28 @@ local assert_log = t.assert_log
|
|||||||
|
|
||||||
local testlog = 'Xtest-tui-log'
|
local testlog = 'Xtest-tui-log'
|
||||||
|
|
||||||
|
describe('TUI', function()
|
||||||
|
it('exit status 1 and error message with server --listen error #34365', function()
|
||||||
|
clear()
|
||||||
|
local addr_in_use = api.nvim_get_vvar('servername')
|
||||||
|
local screen = tt.setup_child_nvim(
|
||||||
|
{ '--listen', addr_in_use, '-u', 'NONE', '-i', 'NONE' },
|
||||||
|
{ extra_rows = 10, cols = 60 }
|
||||||
|
)
|
||||||
|
-- When the address is very long, the error message may be only partly visible.
|
||||||
|
if #addr_in_use <= 600 then
|
||||||
|
screen:expect({
|
||||||
|
any = vim.pesc(
|
||||||
|
('%s: Failed to --listen: address already in use:'):format(
|
||||||
|
is_os('win') and 'nvim.exe' or 'nvim'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
screen:expect({ any = vim.pesc('[Process exited 1]') })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
describe('TUI :detach', function()
|
describe('TUI :detach', function()
|
||||||
before_each(function()
|
before_each(function()
|
||||||
os.remove(testlog)
|
os.remove(testlog)
|
||||||
@@ -2119,15 +2141,20 @@ describe('TUI', function()
|
|||||||
|
|
||||||
it('no assert failure on deadly signal #21896', function()
|
it('no assert failure on deadly signal #21896', function()
|
||||||
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]])
|
exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]])
|
||||||
screen:expect {
|
screen:expect([[
|
||||||
grid = [[
|
|
||||||
Nvim: Caught deadly signal 'SIGTERM' |
|
Nvim: Caught deadly signal 'SIGTERM' |
|
||||||
|
|
|
|
||||||
[Process exited 1]^ |
|
[Process exited 1]^ |
|
||||||
|*3
|
|*3
|
||||||
{3:-- TERMINAL --} |
|
{3:-- TERMINAL --} |
|
||||||
]],
|
]])
|
||||||
}
|
end)
|
||||||
|
|
||||||
|
it('exit status 1 and error message with deadly signal sent to server', function()
|
||||||
|
local _, server_pid = child_session:request('nvim_call_function', 'getpid', {})
|
||||||
|
exec_lua([[vim.uv.kill(..., 'sigterm')]], server_pid)
|
||||||
|
screen:expect({ any = vim.pesc([[Nvim: Caught deadly signal 'SIGTERM']]) })
|
||||||
|
screen:expect({ any = vim.pesc('[Process exited 1]') })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('no stack-use-after-scope with cursor color #22432', function()
|
it('no stack-use-after-scope with cursor color #22432', function()
|
||||||
|
Reference in New Issue
Block a user