diff options
Diffstat (limited to 'src/nvim/os/shell.c')
-rw-r--r-- | src/nvim/os/shell.c | 284 |
1 files changed, 253 insertions, 31 deletions
diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index f5a1637c94..9514936ad0 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -14,18 +14,20 @@ #include "nvim/os/shell.h" #include "nvim/os/signal.h" #include "nvim/types.h" +#include "nvim/main.h" #include "nvim/vim.h" #include "nvim/message.h" #include "nvim/memory.h" #include "nvim/ui.h" -#include "nvim/misc2.h" #include "nvim/screen.h" #include "nvim/memline.h" #include "nvim/option_defs.h" #include "nvim/charset.h" #include "nvim/strings.h" -#define DYNAMIC_BUFFER_INIT {NULL, 0, 0} +#define DYNAMIC_BUFFER_INIT { NULL, 0, 0 } +#define NS_1_SECOND 1000000000U // 1 second, in nanoseconds +#define OUT_DATA_THRESHOLD 1024 * 10U // 10KB, "a few screenfuls" of data. typedef struct { char *data; @@ -53,12 +55,12 @@ char **shell_build_argv(const char *cmd, const char *extra_args) size_t i = tokenize(p_sh, rv); if (extra_args) { - rv[i++] = xstrdup(extra_args); // Push a copy of `extra_args` + rv[i++] = xstrdup(extra_args); // Push a copy of `extra_args` } if (cmd) { - i += tokenize(p_shcf, rv + i); // Split 'shellcmdflag' - rv[i++] = xstrdup(cmd); // Push a copy of the command. + i += tokenize(p_shcf, rv + i); // Split 'shellcmdflag' + rv[i++] = shell_xescape_xquote(cmd); // Copy (and escape) `cmd`. } rv[i] = NULL; @@ -162,7 +164,7 @@ int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_args) /// @param input The input to the shell (NULL for no input), passed to the /// stdin of the resulting process. /// @param len The length of the input buffer (not used if `input` == NULL) -/// @param[out] output A pointer to to a location where the output will be +/// @param[out] output Pointer to a location where the output will be /// allocated and stored. Will point to NULL if the shell /// command did not output anything. If NULL is passed, /// the shell output will be ignored. @@ -187,6 +189,9 @@ static int do_os_system(char **argv, bool silent, bool forward_output) { + out_data_decide_throttle(0); // Initialize throttle decider. + out_data_ring(NULL, 0); // Initialize output ring-buffer. + // the output buffer DynamicBuffer buf = DYNAMIC_BUFFER_INIT; stream_read_cb data_cb = system_data_cb; @@ -205,29 +210,29 @@ static int do_os_system(char **argv, xstrlcpy(prog, argv[0], MAXPATHL); Stream in, out, err; - LibuvProcess uvproc = libuv_process_init(&loop, &buf); + LibuvProcess uvproc = libuv_process_init(&main_loop, &buf); Process *proc = &uvproc.process; - Queue *events = queue_new_child(loop.events); + MultiQueue *events = multiqueue_new_child(main_loop.events); proc->events = events; proc->argv = argv; proc->in = input != NULL ? &in : NULL; proc->out = &out; proc->err = &err; if (!process_spawn(proc)) { - loop_poll_events(&loop, 0); - // Failed, probably due to `sh` not being executable + loop_poll_events(&main_loop, 0); + // Failed, probably due to 'sh' not being executable if (!silent) { MSG_PUTS(_("\nCannot execute ")); msg_outtrans((char_u *)prog); msg_putchar('\n'); } - queue_free(events); + multiqueue_free(events); return -1; } // We want to deal with stream events as fast a possible while queueing // process events, so reset everything to NULL. It prevents closing the - // streams while there's still data in the OS buffer(due to the process + // streams while there's still data in the OS buffer (due to the process // exiting before all data is read). if (input != NULL) { proc->in->events = NULL; @@ -235,10 +240,10 @@ static int do_os_system(char **argv, } proc->out->events = NULL; rstream_init(proc->out, 0); - rstream_start(proc->out, data_cb); + rstream_start(proc->out, data_cb, &buf); proc->err->events = NULL; rstream_init(proc->err, 0); - rstream_start(proc->err, data_cb); + rstream_start(proc->err, data_cb, &buf); // write the input, if any if (input) { @@ -250,14 +255,18 @@ static int do_os_system(char **argv, return -1; } // close the input stream after everything is written - wstream_set_write_cb(&in, shell_write_cb); + wstream_set_write_cb(&in, shell_write_cb, NULL); } - // invoke busy_start here so event_poll_until wont change the busy state for - // the UI + // Invoke busy_start here so LOOP_PROCESS_EVENTS_UNTIL will not change the + // busy state. ui_busy_start(); ui_flush(); int status = process_wait(proc, -1, NULL); + if (!got_int && out_data_decide_throttle(0)) { + // Last chunk of output was skipped; display it now. + out_data_ring(NULL, SIZE_MAX); + } ui_busy_stop(); // prepare the out parameters if requested @@ -277,8 +286,8 @@ static int do_os_system(char **argv, } } - assert(queue_empty(events)); - queue_free(events); + assert(multiqueue_empty(events)); + multiqueue_free(events); return status; } @@ -309,25 +318,192 @@ static void system_data_cb(Stream *stream, RBuffer *buf, size_t count, dbuf->len += nread; } +/// Tracks output received for the current executing shell command, and displays +/// a pulsing "..." when output should be skipped. Tracking depends on the +/// synchronous/blocking nature of ":!". +// +/// Purpose: +/// 1. CTRL-C is more responsive. #1234 #5396 +/// 2. Improves performance of :! (UI, esp. TUI, is the bottleneck). +/// 3. Avoids OOM during long-running, spammy :!. +/// +/// Vim does not need this hack because: +/// 1. :! in terminal-Vim runs in cooked mode, so CTRL-C is caught by the +/// terminal and raises SIGINT out-of-band. +/// 2. :! in terminal-Vim uses a tty (Nvim uses pipes), so commands +/// (e.g. `git grep`) may page themselves. +/// +/// @param size Length of data, used with internal state to decide whether +/// output should be skipped. size=0 resets the internal state and +/// returns the previous decision. +/// +/// @returns true if output should be skipped and pulse was displayed. +/// Returns the previous decision if size=0. +static bool out_data_decide_throttle(size_t size) +{ + static uint64_t started = 0; // Start time of the current throttle. + static size_t received = 0; // Bytes observed since last throttle. + static size_t visit = 0; // "Pulse" count of the current throttle. + static char pulse_msg[] = { ' ', ' ', ' ', '\0' }; + + if (!size) { + bool previous_decision = (visit > 0); + started = received = visit = 0; + return previous_decision; + } + + received += size; + if (received < OUT_DATA_THRESHOLD + // Display at least the first chunk of output even if it is big. + || (!started && received < size + 1000)) { + return false; + } else if (!visit) { + started = os_hrtime(); + } else if (visit % 20 == 0) { + uint64_t since = os_hrtime() - started; + if (since > (3 * NS_1_SECOND)) { + received = visit = 0; + return false; + } + } + + visit++; + // Pulse "..." at the bottom of the screen. + size_t tick = (visit % 20 == 0) + ? 3 // Force all dots "..." on last visit. + : (visit % 4); + pulse_msg[0] = (tick == 0) ? ' ' : '.'; + pulse_msg[1] = (tick == 0 || 1 == tick) ? ' ' : '.'; + pulse_msg[2] = (tick == 0 || 1 == tick || 2 == tick) ? ' ' : '.'; + if (visit == 1) { + screen_del_lines(0, 0, 1, (int)Rows, NULL); + } + int lastrow = (int)Rows - 1; + screen_puts_len((char_u *)pulse_msg, ARRAY_SIZE(pulse_msg), lastrow, 0, 0); + ui_flush(); + return true; +} + +/// Saves output in a quasi-ringbuffer. Used to ensure the last ~page of +/// output for a shell-command is always displayed. +/// +/// Init mode: Resets the internal state. +/// output = NULL +/// size = 0 +/// Print mode: Displays the current saved data. +/// output = NULL +/// size = SIZE_MAX +/// +/// @param output Data to save, or NULL to invoke a special mode. +/// @param size Length of `output`. +static void out_data_ring(char *output, size_t size) +{ +#define MAX_CHUNK_SIZE (OUT_DATA_THRESHOLD / 2) + static char last_skipped[MAX_CHUNK_SIZE]; // Saved output. + static size_t last_skipped_len = 0; + + assert(output != NULL || (size == 0 || size == SIZE_MAX)); + + if (output == NULL && size == 0) { // Init mode + last_skipped_len = 0; + return; + } + + if (output == NULL && size == SIZE_MAX) { // Print mode + out_data_append_to_screen(last_skipped, last_skipped_len, true); + return; + } + + // This is basically a ring-buffer... + if (size >= MAX_CHUNK_SIZE) { // Save mode + size_t start = size - MAX_CHUNK_SIZE; + memcpy(last_skipped, output + start, MAX_CHUNK_SIZE); + last_skipped_len = MAX_CHUNK_SIZE; + } else if (size > 0) { + // Length of the old data that can be kept. + size_t keep_len = MIN(last_skipped_len, MAX_CHUNK_SIZE - size); + size_t keep_start = last_skipped_len - keep_len; + // Shift the kept part of the old data to the start. + if (keep_start) { + memmove(last_skipped, last_skipped + keep_start, keep_len); + } + // Copy the entire new data to the remaining space. + memcpy(last_skipped + keep_len, output, size); + last_skipped_len = keep_len + size; + } +} + +/// Continue to append data to last screen line. +/// +/// @param output Data to append to screen lines. +/// @param remaining Size of data. +/// @param new_line If true, next data output will be on a new line. +static void out_data_append_to_screen(char *output, size_t remaining, + bool new_line) +{ + static colnr_T last_col = 0; // Column of last row to append to. + + size_t off = 0; + int last_row = (int)Rows - 1; + + while (off < remaining) { + // Found end of line? + if (output[off] == NL) { + // Can we start a new line or do we need to continue the last one? + if (last_col == 0) { + screen_del_lines(0, 0, 1, (int)Rows, NULL); + } + screen_puts_len((char_u *)output, (int)off, last_row, last_col, 0); + last_col = 0; + + size_t skip = off + 1; + output += skip; + remaining -= skip; + off = 0; + continue; + } + + // Translate NUL to SOH + if (output[off] == NUL) { + output[off] = 1; + } + + off++; + } + + if (remaining) { + if (last_col == 0) { + screen_del_lines(0, 0, 1, (int)Rows, NULL); + } + screen_puts_len((char_u *)output, (int)remaining, last_row, last_col, 0); + last_col += (colnr_T)remaining; + } + + if (new_line) { + last_col = 0; + } + + ui_flush(); +} + static void out_data_cb(Stream *stream, RBuffer *buf, size_t count, void *data, bool eof) { + // We always output the whole buffer, so the buffer can never + // wrap around. size_t cnt; char *ptr = rbuffer_read_ptr(buf, &cnt); - if (!cnt) { - return; + if (ptr != NULL && cnt > 0 + && out_data_decide_throttle(cnt)) { // Skip output above a threshold. + // Save the skipped output. If it is the final chunk, we display it later. + out_data_ring(ptr, cnt); + } else { + out_data_append_to_screen(ptr, cnt, eof); } - size_t written = write_output(ptr, cnt, false, eof); - // No output written, force emptying the Rbuffer if it is full. - if (!written && rbuffer_size(buf) == rbuffer_capacity(buf)) { - screen_del_lines(0, 0, 1, (int)Rows, NULL); - screen_puts_len((char_u *)ptr, (int)cnt, (int)Rows - 1, 0, 0); - written = cnt; - } - if (written) { - rbuffer_consumed(buf, written); + if (cnt) { + rbuffer_consumed(buf, cnt); } } @@ -500,5 +676,51 @@ static size_t write_output(char *output, size_t remaining, bool to_buffer, static void shell_write_cb(Stream *stream, void *data, int status) { - stream_close(stream, NULL); + if (status) { + // Can happen if system() tries to send input to a shell command that was + // backgrounded (:call system("cat - &", "foo")). #3529 #5241 + msg_schedule_emsgf(_("E5677: Error writing input to shell-command: %s"), + uv_err_name(status)); + } + if (stream->closed) { // Process may have exited before this write. + ELOG("stream was already closed"); + return; + } + stream_close(stream, NULL, NULL); +} + +/// Applies 'shellxescape' (p_sxe) and 'shellxquote' (p_sxq) to a command. +/// +/// @param cmd Command string +/// @return Escaped/quoted command string (allocated). +static char *shell_xescape_xquote(const char *cmd) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (*p_sxq == NUL) { + return xstrdup(cmd); + } + + const char *ecmd = cmd; + if (*p_sxe != NUL && STRCMP(p_sxq, "(") == 0) { + ecmd = (char *)vim_strsave_escaped_ext((char_u *)cmd, p_sxe, '^', false); + } + size_t ncmd_size = strlen(ecmd) + STRLEN(p_sxq) * 2 + 1; + char *ncmd = xmalloc(ncmd_size); + + // When 'shellxquote' is ( append ). + // When 'shellxquote' is "( append )". + if (STRCMP(p_sxq, "(") == 0) { + vim_snprintf(ncmd, ncmd_size, "(%s)", ecmd); + } else if (STRCMP(p_sxq, "\"(") == 0) { + vim_snprintf(ncmd, ncmd_size, "\"(%s)\"", ecmd); + } else { + vim_snprintf(ncmd, ncmd_size, "%s%s%s", p_sxq, ecmd, p_sxq); + } + + if (ecmd != cmd) { + xfree((void *)ecmd); + } + + return ncmd; } + |