aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/os/shell.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nvim/os/shell.c')
-rw-r--r--src/nvim/os/shell.c284
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;
}
+