aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/event/process.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nvim/event/process.c')
-rw-r--r--src/nvim/event/process.c325
1 files changed, 325 insertions, 0 deletions
diff --git a/src/nvim/event/process.c b/src/nvim/event/process.c
new file mode 100644
index 0000000000..2b1f1ae096
--- /dev/null
+++ b/src/nvim/event/process.c
@@ -0,0 +1,325 @@
+#include <assert.h>
+#include <stdlib.h>
+
+#include <uv.h>
+
+#include "nvim/os/shell.h"
+#include "nvim/event/loop.h"
+#include "nvim/event/rstream.h"
+#include "nvim/event/wstream.h"
+#include "nvim/event/process.h"
+#include "nvim/event/uv_process.h"
+#include "nvim/event/pty_process.h"
+#include "nvim/globals.h"
+#include "nvim/log.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "event/process.c.generated.h"
+#endif
+
+// {SIGNAL}_TIMEOUT is the time (in nanoseconds) that a process has to cleanly
+// exit before we send SIGNAL to it
+#define TERM_TIMEOUT 1000000000
+#define KILL_TIMEOUT (TERM_TIMEOUT * 2)
+
+#define CLOSE_PROC_STREAM(proc, stream) \
+ do { \
+ if (proc->stream && !proc->stream->closed) { \
+ stream_close(proc->stream, NULL); \
+ } \
+ } while (0)
+
+
+bool process_spawn(Loop *loop, Process *proc) FUNC_ATTR_NONNULL_ALL
+{
+ proc->loop = loop;
+ if (proc->in) {
+ uv_pipe_init(&loop->uv, &proc->in->uv.pipe, 0);
+ }
+
+ if (proc->out) {
+ uv_pipe_init(&loop->uv, &proc->out->uv.pipe, 0);
+ }
+
+ if (proc->err) {
+ uv_pipe_init(&loop->uv, &proc->err->uv.pipe, 0);
+ }
+
+ bool success;
+ switch (proc->type) {
+ case kProcessTypeUv:
+ success = uv_process_spawn((UvProcess *)proc);
+ break;
+ case kProcessTypePty:
+ success = pty_process_spawn((PtyProcess *)proc);
+ break;
+ default:
+ abort();
+ }
+
+ if (!success) {
+ if (proc->in) {
+ uv_close((uv_handle_t *)&proc->in->uv.pipe, NULL);
+ }
+ if (proc->out) {
+ uv_close((uv_handle_t *)&proc->out->uv.pipe, NULL);
+ }
+ if (proc->err) {
+ uv_close((uv_handle_t *)&proc->err->uv.pipe, NULL);
+ }
+ process_close(proc);
+ shell_free_argv(proc->argv);
+ proc->status = -1;
+ return false;
+ }
+
+ void *data = proc->data;
+
+ if (proc->in) {
+ stream_init(NULL, proc->in, -1, (uv_stream_t *)&proc->in->uv.pipe, data);
+ proc->in->internal_data = proc;
+ proc->in->internal_close_cb = on_process_stream_close;
+ proc->refcount++;
+ }
+
+ if (proc->out) {
+ stream_init(NULL, proc->out, -1, (uv_stream_t *)&proc->out->uv.pipe, data);
+ proc->out->internal_data = proc;
+ proc->out->internal_close_cb = on_process_stream_close;
+ proc->refcount++;
+ }
+
+ if (proc->err) {
+ stream_init(NULL, proc->err, -1, (uv_stream_t *)&proc->err->uv.pipe, data);
+ proc->err->internal_data = proc;
+ proc->err->internal_close_cb = on_process_stream_close;
+ proc->refcount++;
+ }
+
+ proc->internal_exit_cb = on_process_exit;
+ proc->internal_close_cb = decref;
+ proc->refcount++;
+ kl_push(WatcherPtr, loop->children, proc);
+ return true;
+}
+
+void process_teardown(Loop *loop) FUNC_ATTR_NONNULL_ALL
+{
+ kl_iter(WatcherPtr, loop->children, current) {
+ Process *proc = (*current)->data;
+ uv_kill(proc->pid, SIGTERM);
+ proc->term_sent = true;
+ process_stop(proc);
+ }
+
+ // Wait until all children exit
+ LOOP_POLL_EVENTS_UNTIL(loop, -1, kl_empty(loop->children));
+ pty_process_teardown(loop);
+}
+
+// Wrappers around `stream_close` that protect against double-closing.
+void process_close_streams(Process *proc) FUNC_ATTR_NONNULL_ALL
+{
+ process_close_in(proc);
+ process_close_out(proc);
+ process_close_err(proc);
+}
+
+void process_close_in(Process *proc) FUNC_ATTR_NONNULL_ALL
+{
+ CLOSE_PROC_STREAM(proc, in);
+}
+
+void process_close_out(Process *proc) FUNC_ATTR_NONNULL_ALL
+{
+ CLOSE_PROC_STREAM(proc, out);
+}
+
+void process_close_err(Process *proc) FUNC_ATTR_NONNULL_ALL
+{
+ CLOSE_PROC_STREAM(proc, err);
+}
+
+/// Synchronously wait for a process to finish
+///
+/// @param process The Process instance
+/// @param ms Number of milliseconds to wait, 0 for not waiting, -1 for
+/// waiting until the process quits.
+/// @return returns the status code of the exited process. -1 if the process is
+/// still running and the `timeout` has expired. Note that this is
+/// indistinguishable from the process returning -1 by itself. Which
+/// is possible on some OS. Returns -2 if an user has interruped the
+/// wait.
+int process_wait(Process *proc, int ms) FUNC_ATTR_NONNULL_ALL
+{
+ // The default status is -1, which represents a timeout
+ int status = -1;
+ bool interrupted = false;
+
+ // Increase refcount to stop the exit callback from being called(and possibly
+ // being freed) before we have a chance to get the status.
+ proc->refcount++;
+ LOOP_POLL_EVENTS_UNTIL(proc->loop, ms,
+ // Until...
+ got_int || // interrupted by the user
+ proc->refcount == 1); // job exited
+
+ // we'll assume that a user frantically hitting interrupt doesn't like
+ // the current job. Signal that it has to be killed.
+ if (got_int) {
+ interrupted = true;
+ got_int = false;
+ process_stop(proc);
+ if (ms == -1) {
+ // We can only return, if all streams/handles are closed and the job
+
+ // exited.
+ LOOP_POLL_EVENTS_UNTIL(proc->loop, -1, proc->refcount == 1);
+ } else {
+ loop_poll_events(proc->loop, 0);
+ }
+ }
+
+ if (proc->refcount == 1) {
+ // Job exited, collect status and manually invoke close_cb to free the job
+ // resources
+ status = interrupted ? -2 : proc->status;
+ decref(proc);
+ } else {
+ proc->refcount--;
+ }
+
+ return status;
+}
+
+/// Ask a process to terminate and eventually kill if it doesn't respond
+void process_stop(Process *proc) FUNC_ATTR_NONNULL_ALL
+{
+ if (proc->stopped_time) {
+ return;
+ }
+
+ proc->stopped_time = os_hrtime();
+ switch (proc->type) {
+ case kProcessTypeUv:
+ // Close the process's stdin. If the process doesn't close its own
+ // stdout/stderr, they will be closed when it exits(possibly due to being
+ // terminated after a timeout)
+ process_close_in(proc);
+ break;
+ case kProcessTypePty:
+ // close all streams for pty processes to send SIGHUP to the process
+ process_close_streams(proc);
+ pty_process_close_master((PtyProcess *)proc);
+ break;
+ default:
+ abort();
+ }
+
+ Loop *loop = proc->loop;
+ if (!loop->children_stop_requests++) {
+ // When there's at least one stop request pending, start a timer that
+ // will periodically check if a signal should be send to a to the job
+ DLOG("Starting job kill timer");
+ uv_timer_start(&loop->children_kill_timer, children_kill_cb, 100, 100);
+ }
+}
+
+/// Iterates the process list sending SIGTERM to stopped processes and SIGKILL
+/// to those that didn't die from SIGTERM after a while(exit_timeout is 0).
+static void children_kill_cb(uv_timer_t *handle)
+{
+ Loop *loop = handle->loop->data;
+ uint64_t now = os_hrtime();
+
+ kl_iter(WatcherPtr, loop->children, current) {
+ Process *proc = (*current)->data;
+ if (!proc->stopped_time) {
+ continue;
+ }
+ uint64_t elapsed = now - proc->stopped_time;
+
+ if (!proc->term_sent && elapsed >= TERM_TIMEOUT) {
+ ILOG("Sending SIGTERM to pid %d", proc->pid);
+ uv_kill(proc->pid, SIGTERM);
+ proc->term_sent = true;
+ } else if (elapsed >= KILL_TIMEOUT) {
+ ILOG("Sending SIGKILL to pid %d", proc->pid);
+ uv_kill(proc->pid, SIGKILL);
+ }
+ }
+}
+
+static void decref(Process *proc)
+{
+ if (--proc->refcount != 0) {
+ return;
+ }
+
+ Loop *loop = proc->loop;
+ kliter_t(WatcherPtr) **node = NULL;
+ kl_iter(WatcherPtr, loop->children, current) {
+ if ((*current)->data == proc) {
+ node = current;
+ break;
+ }
+ }
+
+ assert(node);
+ kl_shift_at(WatcherPtr, loop->children, node);
+ shell_free_argv(proc->argv);
+ if (proc->type == kProcessTypePty) {
+ xfree(((PtyProcess *)proc)->term_name);
+ }
+ if (proc->cb) {
+ proc->cb(proc, proc->status, proc->data);
+ }
+}
+
+static void process_close(Process *proc)
+ FUNC_ATTR_NONNULL_ARG(1)
+{
+ assert(!proc->closed);
+ proc->closed = true;
+ switch (proc->type) {
+ case kProcessTypeUv:
+ uv_process_close((UvProcess *)proc);
+ break;
+ case kProcessTypePty:
+ pty_process_close((PtyProcess *)proc);
+ break;
+ default:
+ abort();
+ }
+}
+
+static void on_process_exit(Process *proc)
+{
+ if (exiting) {
+ on_process_exit_event((Event) {.data = proc});
+ } else {
+ loop_push_event(proc->loop,
+ (Event) {.handler = on_process_exit_event, .data = proc}, false);
+ }
+
+ Loop *loop = proc->loop;
+ if (loop->children_stop_requests && !--loop->children_stop_requests) {
+ // Stop the timer if no more stop requests are pending
+ DLOG("Stopping process kill timer");
+ uv_timer_stop(&loop->children_kill_timer);
+ }
+}
+
+static void on_process_exit_event(Event event)
+{
+ Process *proc = event.data;
+ process_close_streams(proc);
+ process_close(proc);
+}
+
+static void on_process_stream_close(Stream *stream, void *data)
+{
+ Process *proc = data;
+ decref(proc);
+}
+