aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval.c170
-rw-r--r--src/fileio.c6
-rw-r--r--src/globals.h3
-rw-r--r--src/os/event.c6
-rw-r--r--src/os/event.h12
-rw-r--r--src/os/event_defs.h19
-rw-r--r--src/os/job.c345
-rw-r--r--src/os/job.h72
-rw-r--r--src/os/job_defs.h6
-rw-r--r--src/os/signal.c1
-rw-r--r--src/os/signal.h2
-rw-r--r--src/os_unix.c2
-rw-r--r--src/vim.h2
13 files changed, 632 insertions, 14 deletions
diff --git a/src/eval.c b/src/eval.c
index 978e3899ad..dd4b233a2f 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -63,6 +63,7 @@
#include "version.h"
#include "window.h"
#include "os/os.h"
+#include "os/job.h"
#include "os/shell.h"
#if defined(FEAT_FLOAT)
@@ -388,6 +389,7 @@ static struct vimvar {
{VV_NAME("hlsearch", VAR_NUMBER), 0},
{VV_NAME("oldfiles", VAR_LIST), 0},
{VV_NAME("windowid", VAR_NUMBER), VV_RO},
+ {VV_NAME("job_data", VAR_LIST), 0}
};
/* shorthand */
@@ -401,6 +403,11 @@ static struct vimvar {
static dictitem_T vimvars_var; /* variable used for v: */
#define vimvarht vimvardict.dv_hashtab
+static void apply_job_autocmds(int id,
+ void *data,
+ char *buffer,
+ uint32_t len,
+ bool from_stdout);
static void prepare_vimvar(int idx, typval_T *save_tv);
static void restore_vimvar(int idx, typval_T *save_tv);
static int ex_let_vars(char_u *arg, typval_T *tv, int copy,
@@ -629,6 +636,9 @@ static void f_invert(typval_T *argvars, typval_T *rettv);
static void f_isdirectory(typval_T *argvars, typval_T *rettv);
static void f_islocked(typval_T *argvars, typval_T *rettv);
static void f_items(typval_T *argvars, typval_T *rettv);
+static void f_job_start(typval_T *argvars, typval_T *rettv);
+static void f_job_stop(typval_T *argvars, typval_T *rettv);
+static void f_job_write(typval_T *argvars, typval_T *rettv);
static void f_join(typval_T *argvars, typval_T *rettv);
static void f_keys(typval_T *argvars, typval_T *rettv);
static void f_last_buffer_nr(typval_T *argvars, typval_T *rettv);
@@ -1366,6 +1376,7 @@ int eval_to_number(char_u *expr)
return retval;
}
+
/*
* Prepare v: variable "idx" to be used.
* Save the current typeval in "save_tv".
@@ -6946,6 +6957,9 @@ static struct fst {
{"isdirectory", 1, 1, f_isdirectory},
{"islocked", 1, 1, f_islocked},
{"items", 1, 1, f_items},
+ {"jobstart", 2, 3, f_job_start},
+ {"jobstop", 1, 1, f_job_stop},
+ {"jobwrite", 2, 2, f_job_write},
{"join", 1, 2, f_join},
{"keys", 1, 1, f_keys},
{"last_buffer_nr", 0, 0, f_last_buffer_nr}, /* obsolete */
@@ -11001,6 +11015,143 @@ static void f_items(typval_T *argvars, typval_T *rettv)
dict_list(argvars, rettv, 2);
}
+// "jobstart()" function
+static void f_job_start(typval_T *argvars, typval_T *rettv)
+{
+ list_T *args = NULL;
+ listitem_T *arg;
+ int i, argvl, argsl;
+ char **argv = NULL;
+
+ rettv->v_type = VAR_NUMBER;
+ rettv->vval.v_number = 0;
+
+ if (check_restricted() || check_secure()) {
+ goto cleanup;
+ }
+
+ if (argvars[0].v_type != VAR_STRING
+ || argvars[1].v_type != VAR_STRING
+ || (argvars[2].v_type != VAR_LIST
+ && argvars[2].v_type != VAR_UNKNOWN)) {
+ // Wrong argument types
+ EMSG(_(e_invarg));
+ goto cleanup;
+ }
+
+ argsl = 0;
+ if (argvars[2].v_type == VAR_LIST) {
+ args = argvars[2].vval.v_list;
+ argsl = args->lv_len;
+ // Assert that all list items are strings
+ for (arg = args->lv_first; arg != NULL; arg = arg->li_next) {
+ if (arg->li_tv.v_type != VAR_STRING) {
+ EMSG(_(e_invarg));
+ goto cleanup;
+ }
+ }
+ }
+
+ if (!os_can_exe(get_tv_string(&argvars[1]))) {
+ // String is not executable
+ EMSG2(e_jobexe, get_tv_string(&argvars[1]));
+ goto cleanup;
+ }
+
+ // Allocate extra memory for the argument vector and the NULL pointer
+ argvl = argsl + 2;
+ argv = xmalloc(sizeof(char_u *) * argvl);
+
+ // Copy program name
+ argv[0] = xstrdup((char *)argvars[1].vval.v_string);
+
+ i = 1;
+ // Copy arguments to the vector
+ if (argsl > 0) {
+ for (arg = args->lv_first; arg != NULL; arg = arg->li_next) {
+ argv[i++] = xstrdup((char *)arg->li_tv.vval.v_string);
+ }
+ }
+
+ // The last item of argv must be NULL
+ argv[i] = NULL;
+
+ rettv->vval.v_number = job_start(argv,
+ xstrdup((char *)argvars[0].vval.v_string),
+ apply_job_autocmds);
+
+ if (rettv->vval.v_number <= 0) {
+ if (rettv->vval.v_number == 0) {
+ EMSG(_(e_jobtblfull));
+ } else {
+ EMSG(_(e_jobexe));
+ }
+ }
+
+cleanup:
+ if (rettv->vval.v_number > 0) {
+ // Success
+ return;
+ }
+ // Cleanup argv memory in case the `job_start` call failed
+ shell_free_argv(argv);
+}
+
+// "jobstop()" function
+static void f_job_stop(typval_T *argvars, typval_T *rettv)
+{
+ rettv->v_type = VAR_NUMBER;
+ rettv->vval.v_number = 0;
+
+ if (check_restricted() || check_secure()) {
+ return;
+ }
+
+ if (argvars[0].v_type != VAR_NUMBER) {
+ // Only argument is the job id
+ EMSG(_(e_invarg));
+ return;
+ }
+
+ if (!job_stop(argvars[0].vval.v_number)) {
+ // Probably an invalid job id
+ EMSG(_(e_invjob));
+ return;
+ }
+
+ rettv->vval.v_number = 1;
+}
+
+// "jobwrite()" function
+static void f_job_write(typval_T *argvars, typval_T *rettv)
+{
+ bool res;
+ rettv->v_type = VAR_NUMBER;
+ rettv->vval.v_number = 0;
+
+ if (check_restricted() || check_secure()) {
+ return;
+ }
+
+ if (argvars[0].v_type != VAR_NUMBER || argvars[1].v_type != VAR_STRING) {
+ // First argument is the job id and second is the string to write to
+ // the job's stdin
+ EMSG(_(e_invarg));
+ return;
+ }
+
+ res = job_write(argvars[0].vval.v_number,
+ xstrdup((char *)argvars[1].vval.v_string),
+ strlen((char *)argvars[1].vval.v_string));
+
+ if (!res) {
+ // Invalid job id
+ EMSG(_(e_invjob));
+ }
+
+ rettv->vval.v_number = 1;
+}
+
/*
* "join()" function
*/
@@ -11045,7 +11196,7 @@ static void f_keys(typval_T *argvars, typval_T *rettv)
static void f_last_buffer_nr(typval_T *argvars, typval_T *rettv)
{
int n = 0;
- buf_T *buf;
+ buf_T *buf;
for (buf = firstbuf; buf != NULL; buf = buf->b_next)
if (n < buf->b_fnum)
@@ -19593,3 +19744,20 @@ char_u *do_string_sub(char_u *str, char_u *pat, char_u *sub, char_u *flags)
return ret;
}
+static void apply_job_autocmds(int id,
+ void *data,
+ char *buffer,
+ uint32_t len,
+ bool from_stdout)
+{
+ list_T *list;
+
+ // Call JobActivity autocommands
+ list = list_alloc();
+ list_append_number(list, id);
+ list_append_string(list, (char_u *)buffer, len);
+ list_append_string(list, (char_u *)(from_stdout ? "out" : "err"), 3);
+ set_vim_var_list(VV_JOB_DATA, list);
+ apply_autocmds(EVENT_JOBACTIVITY, (char_u *)data, NULL, TRUE, NULL);
+}
+
diff --git a/src/fileio.c b/src/fileio.c
index 903b84914b..92ebb9ff19 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -5950,6 +5950,7 @@ static struct event_name {
{"InsertEnter", EVENT_INSERTENTER},
{"InsertLeave", EVENT_INSERTLEAVE},
{"InsertCharPre", EVENT_INSERTCHARPRE},
+ {"JobActivity", EVENT_JOBACTIVITY},
{"MenuPopup", EVENT_MENUPOPUP},
{"QuickFixCmdPost", EVENT_QUICKFIXCMDPOST},
{"QuickFixCmdPre", EVENT_QUICKFIXCMDPRE},
@@ -7394,7 +7395,7 @@ apply_autocmds_group (
} else {
sfname = vim_strsave(fname);
/* Don't try expanding FileType, Syntax, FuncUndefined, WindowID,
- * ColorScheme or QuickFixCmd* */
+ * ColorScheme, QuickFixCmd or JobActivity */
if (event == EVENT_FILETYPE
|| event == EVENT_SYNTAX
|| event == EVENT_FUNCUNDEFINED
@@ -7402,7 +7403,8 @@ apply_autocmds_group (
|| event == EVENT_SPELLFILEMISSING
|| event == EVENT_QUICKFIXCMDPRE
|| event == EVENT_COLORSCHEME
- || event == EVENT_QUICKFIXCMDPOST)
+ || event == EVENT_QUICKFIXCMDPOST
+ || event == EVENT_JOBACTIVITY)
fname = vim_strsave(fname);
else
fname = FullName_save(fname, FALSE);
diff --git a/src/globals.h b/src/globals.h
index 812a6f688c..836e41aa2b 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -1009,6 +1009,9 @@ EXTERN char_u e_invexpr2[] INIT(= N_("E15: Invalid expression: %s"));
EXTERN char_u e_invrange[] INIT(= N_("E16: Invalid range"));
EXTERN char_u e_invcmd[] INIT(= N_("E476: Invalid command"));
EXTERN char_u e_isadir2[] INIT(= N_("E17: \"%s\" is a directory"));
+EXTERN char_u e_invjob[] INIT(= N_("E900: Invalid job id"));
+EXTERN char_u e_jobtblfull[] INIT(= N_("E901: Job table is full"));
+EXTERN char_u e_jobexe[] INIT(= N_("E902: \"%s\" is not an executable"));
#ifdef FEAT_LIBCALL
EXTERN char_u e_libcall[] INIT(= N_("E364: Library call failed for \"%s()\""));
#endif
diff --git a/src/os/event.c b/src/os/event.c
index a4ebdb15ff..9e95159dbc 100644
--- a/src/os/event.c
+++ b/src/os/event.c
@@ -8,6 +8,7 @@
#include "os/event.h"
#include "os/input.h"
#include "os/signal.h"
+#include "os/job.h"
#include "vim.h"
#include "memory.h"
#include "misc2.h"
@@ -34,6 +35,8 @@ void event_init()
// `event_poll`
// Signals
signal_init();
+ // Jobs
+ job_init();
uv_timer_init(uv_default_loop(), &timer);
// This prepare handle that actually starts the timer
uv_prepare_init(uv_default_loop(), &timer_prepare);
@@ -88,6 +91,9 @@ static void process_all_events()
case kEventSignal:
signal_handle(event);
break;
+ case kEventJobActivity:
+ job_handle(event);
+ break;
default:
abort();
}
diff --git a/src/os/event.h b/src/os/event.h
index ba84d8ffae..7aee717213 100644
--- a/src/os/event.h
+++ b/src/os/event.h
@@ -4,16 +4,8 @@
#include <stdint.h>
#include <stdbool.h>
-typedef enum {
- kEventSignal
-} EventType;
-
-typedef struct {
- EventType type;
- union {
- int signum;
- } data;
-} Event;
+#include "os/event_defs.h"
+#include "os/job_defs.h"
void event_init(void);
bool event_poll(int32_t ms);
diff --git a/src/os/event_defs.h b/src/os/event_defs.h
new file mode 100644
index 0000000000..5764534382
--- /dev/null
+++ b/src/os/event_defs.h
@@ -0,0 +1,19 @@
+#ifndef NEOVIM_OS_EVENT_DEFS_H
+#define NEOVIM_OS_EVENT_DEFS_H
+
+#include "os/job_defs.h"
+
+typedef enum {
+ kEventSignal,
+ kEventJobActivity
+} EventType;
+
+typedef struct {
+ EventType type;
+ union {
+ int signum;
+ Job *job;
+ } data;
+} Event;
+
+#endif // NEOVIM_OS_EVENT_H
diff --git a/src/os/job.c b/src/os/job.c
new file mode 100644
index 0000000000..f8d9d6d576
--- /dev/null
+++ b/src/os/job.c
@@ -0,0 +1,345 @@
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <uv.h>
+
+#include "os/job_defs.h"
+#include "os/job.h"
+#include "os/time.h"
+#include "os/shell.h"
+#include "vim.h"
+#include "memory.h"
+#include "term.h"
+
+#define EXIT_TIMEOUT 25
+#define MAX_RUNNING_JOBS 100
+#define JOB_BUFFER_SIZE 1024
+
+/// Possible lock states of the job buffer
+typedef enum {
+ kBufferLockNone = 0, ///< No data was read
+ kBufferLockStdout, ///< Data read from stdout
+ kBufferLockStderr ///< Data read from stderr
+} BufferLock;
+
+struct _Job {
+ // Job id the index in the job table plus one.
+ int id;
+ // Number of polls after a SIGTERM that will trigger a SIGKILL
+ int exit_timeout;
+ // If the job was already stopped
+ bool stopped;
+ // Data associated with the job
+ void *data;
+ // Buffer for reading from stdout or stderr
+ char buffer[JOB_BUFFER_SIZE];
+ // Size of the data from the last read
+ uint32_t length;
+ // Buffer lock state
+ BufferLock lock;
+ // Callback for consuming data from the buffer
+ job_read_cb read_cb;
+ // Structures for process spawning/management used by libuv
+ uv_process_t proc;
+ uv_process_options_t proc_opts;
+ uv_stdio_container_t stdio[3];
+ uv_pipe_t proc_stdin, proc_stdout, proc_stderr;
+};
+
+static Job *table[MAX_RUNNING_JOBS] = {NULL};
+static uv_prepare_t job_prepare;
+
+// Some helpers shared in this module
+static bool is_alive(Job *job);
+static Job * find_job(int id);
+static void free_job(Job *job);
+
+// Callbacks for libuv
+static void job_prepare_cb(uv_prepare_t *handle, int status);
+static void alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf);
+static void read_cb(uv_stream_t *stream, ssize_t cnt, const uv_buf_t *buf);
+static void write_cb(uv_write_t *req, int status);
+static void exit_cb(uv_process_t *proc, int64_t status, int term_signal);
+
+void job_init()
+{
+ uv_disable_stdio_inheritance();
+ uv_prepare_init(uv_default_loop(), &job_prepare);
+ uv_prepare_start(&job_prepare, job_prepare_cb);
+}
+
+void job_teardown()
+{
+ // 20 tries will give processes about 1 sec to exit cleanly
+ uint32_t remaining_tries = 20;
+ bool all_dead = true;
+ int i;
+ Job *job;
+
+ // Politely ask each job to terminate
+ for (i = 0; i < MAX_RUNNING_JOBS; i++) {
+ if ((job = table[i]) != NULL) {
+ all_dead = false;
+ uv_process_kill(&job->proc, SIGTERM);
+ }
+ }
+
+ if (all_dead) {
+ return;
+ }
+
+ os_delay(10, 0);
+ // Right now any exited process are zombies waiting for us to acknowledge
+ // their status with `wait` or handling SIGCHLD. libuv does that
+ // automatically (and then calls `exit_cb`) but we have to give it a chance
+ // by running the loop one more time
+ uv_run(uv_default_loop(), UV_RUN_NOWAIT);
+
+ // Prepare to start shooting
+ for (i = 0; i < MAX_RUNNING_JOBS; i++) {
+ if ((job = table[i]) == NULL) {
+ continue;
+ }
+
+ // Still alive
+ while (is_alive(job) && remaining_tries--) {
+ // Since this is the first time we're checking, wait 300ms so
+ // every job has a chance to exit normally
+ os_delay(50, 0);
+ // Acknowledge child exits
+ uv_run(uv_default_loop(), UV_RUN_NOWAIT);
+ }
+
+ if (is_alive(job)) {
+ uv_process_kill(&job->proc, SIGKILL);
+ }
+ }
+}
+
+int job_start(char **argv, void *data, job_read_cb cb)
+{
+ int i;
+ Job *job;
+
+ // Search for a free slot in the table
+ for (i = 0; i < MAX_RUNNING_JOBS; i++) {
+ if (table[i] == NULL) {
+ break;
+ }
+ }
+
+ if (i == MAX_RUNNING_JOBS) {
+ // No free slots
+ return 0;
+ }
+
+ job = xmalloc(sizeof(Job));
+ // Initialize
+ job->id = i + 1;
+ job->data = data;
+ job->read_cb = cb;
+ job->stopped = false;
+ job->exit_timeout = EXIT_TIMEOUT;
+ job->proc_opts.file = argv[0];
+ job->proc_opts.args = argv;
+ job->proc_opts.stdio = job->stdio;
+ job->proc_opts.stdio_count = 3;
+ job->proc_opts.flags = UV_PROCESS_WINDOWS_HIDE;
+ job->proc_opts.exit_cb = exit_cb;
+ job->proc_opts.cwd = NULL;
+ job->proc_opts.env = NULL;
+
+ // Initialize the job std{in,out,err}
+ uv_pipe_init(uv_default_loop(), &job->proc_stdin, 0);
+ job->proc_stdin.data = job;
+ job->stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE;
+ job->stdio[0].data.stream = (uv_stream_t *)&job->proc_stdin;
+
+ uv_pipe_init(uv_default_loop(), &job->proc_stdout, 0);
+ job->proc_stdout.data = job;
+ job->stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE;
+ job->stdio[1].data.stream = (uv_stream_t *)&job->proc_stdout;
+
+ uv_pipe_init(uv_default_loop(), &job->proc_stderr, 0);
+ job->proc_stderr.data = job;
+ job->stdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE;
+ job->stdio[2].data.stream = (uv_stream_t *)&job->proc_stderr;
+
+ // Spawn the job
+ if (uv_spawn(uv_default_loop(), &job->proc, &job->proc_opts) != 0) {
+ free_job(job);
+ return -1;
+ }
+
+ // Start the readable streams
+ uv_read_start((uv_stream_t *)&job->proc_stdout, alloc_cb, read_cb);
+ uv_read_start((uv_stream_t *)&job->proc_stderr, alloc_cb, read_cb);
+ // Give the callback a reference to the job
+ job->proc.data = job;
+ // Save the job to the table
+ table[i] = job;
+
+ return job->id;
+}
+
+bool job_stop(int id)
+{
+ Job *job = find_job(id);
+
+ if (job == NULL || job->stopped) {
+ return false;
+ }
+
+ uv_read_stop((uv_stream_t *)&job->proc_stdout);
+ uv_read_stop((uv_stream_t *)&job->proc_stderr);
+ job->stopped = true;
+
+ return true;
+}
+
+bool job_write(int id, char *data, uint32_t len)
+{
+ uv_buf_t uvbuf;
+ uv_write_t *req;
+ Job *job = find_job(id);
+
+ if (job == NULL || job->stopped) {
+ free(data);
+ return false;
+ }
+
+ req = xmalloc(sizeof(uv_write_t));
+ req->data = data;
+ uvbuf.base = data;
+ uvbuf.len = len;
+ uv_write(req, (uv_stream_t *)&job->proc_stdin, &uvbuf, 1, write_cb);
+
+ return true;
+}
+
+void job_handle(Event event)
+{
+ Job *job = event.data.job;
+
+ // Invoke the job callback
+ job->read_cb(job->id,
+ job->data,
+ job->buffer,
+ job->length,
+ job->lock == kBufferLockStdout);
+
+ shell_resized();
+ // restart reading
+ job->lock = kBufferLockNone;
+ uv_read_start((uv_stream_t *)&job->proc_stdout, alloc_cb, read_cb);
+ uv_read_start((uv_stream_t *)&job->proc_stderr, alloc_cb, read_cb);
+}
+
+static bool is_alive(Job *job)
+{
+ return uv_process_kill(&job->proc, 0) == 0;
+}
+
+static Job * find_job(int id)
+{
+ if (id <= 0 || id > MAX_RUNNING_JOBS) {
+ return NULL;
+ }
+
+ return table[id - 1];
+}
+
+static void free_job(Job *job)
+{
+ uv_close((uv_handle_t *)&job->proc_stdout, NULL);
+ uv_close((uv_handle_t *)&job->proc_stdin, NULL);
+ uv_close((uv_handle_t *)&job->proc_stderr, NULL);
+ uv_close((uv_handle_t *)&job->proc, NULL);
+ free(job);
+}
+
+/// Iterates the table, sending SIGTERM to stopped jobs and SIGKILL to those
+/// that didn't die from SIGTERM after a while(exit_timeout is 0).
+static void job_prepare_cb(uv_prepare_t *handle, int status)
+{
+ Job *job;
+ int i;
+
+ for (i = 0; i < MAX_RUNNING_JOBS; i++) {
+ if ((job = table[i]) == NULL || !job->stopped) {
+ continue;
+ }
+
+ if ((job->exit_timeout--) == EXIT_TIMEOUT) {
+ // Job was just stopped, close all stdio handles and send SIGTERM
+ uv_process_kill(&job->proc, SIGTERM);
+ } else if (job->exit_timeout == 0) {
+ // We've waited long enough, send SIGKILL
+ uv_process_kill(&job->proc, SIGKILL);
+ }
+ }
+}
+
+/// Puts the job into a 'reading state' which 'locks' the job buffer
+/// until the data is consumed
+static void alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf)
+{
+ Job *job = (Job *)handle->data;
+
+ if (job->lock != kBufferLockNone) {
+ // Already reserved the buffer for reading from stdout or stderr.
+ buf->len = 0;
+ return;
+ }
+
+ buf->base = job->buffer;
+ buf->len = JOB_BUFFER_SIZE;
+ // Avoid `alloc_cb`, `alloc_cb` sequences on windows and also mark which
+ // stream we are reading from
+ job->lock =
+ (handle == (uv_handle_t *)&job->proc_stdout) ?
+ kBufferLockStdout :
+ kBufferLockStderr;
+}
+
+/// Pushes a event object to the event queue, which will be handled later by
+/// `job_handle`
+static void read_cb(uv_stream_t *stream, ssize_t cnt, const uv_buf_t *buf)
+{
+ Event event;
+ Job *job = (Job *)stream->data;
+ // pause reading on both streams
+ uv_read_stop((uv_stream_t *)&job->proc_stdout);
+ uv_read_stop((uv_stream_t *)&job->proc_stderr);
+
+ if (cnt <= 0) {
+ if (cnt != UV_ENOBUFS) {
+ // Assume it's EOF and exit the job. Doesn't harm sending a SIGTERM
+ // at this point
+ uv_process_kill(&job->proc, SIGTERM);
+ }
+ return;
+ }
+
+ job->length = cnt;
+ event.type = kEventJobActivity;
+ event.data.job = job;
+ event_push(event);
+}
+
+static void write_cb(uv_write_t *req, int status)
+{
+ free(req->data);
+ free(req);
+}
+
+/// Cleanup all the resources associated with the job
+static void exit_cb(uv_process_t *proc, int64_t status, int term_signal)
+{
+ Job *job = proc->data;
+
+ table[job->id - 1] = NULL;
+ shell_free_argv(job->proc_opts.args);
+ free_job(job);
+}
+
diff --git a/src/os/job.h b/src/os/job.h
new file mode 100644
index 0000000000..0350f44d58
--- /dev/null
+++ b/src/os/job.h
@@ -0,0 +1,72 @@
+// Job is a short name we use to refer to child processes that run in parallel
+// with the editor, probably executing long-running tasks and sending updates
+// asynchronously. Communication happens through anonymous pipes connected to
+// the job's std{in,out,err}. They are more like bash/zsh co-processes than the
+// usual shell background job. The name 'Job' was chosen because it applies to
+// the concept while being significantly shorter.
+#ifndef NEOVIM_OS_JOB_H
+#define NEOVIM_OS_JOB_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "os/event.h"
+
+/// Function called when the job reads data
+///
+/// @param id The job is
+/// @param data Some data associated with the job by the caller
+/// @param buffer Buffer containing the data read. It must be copied
+/// immediately.
+/// @param len Amount of bytes that must be read from `buffer`
+/// @param from_stdout This is true if data was read from the job's stdout,
+/// false if it came from stderr.
+typedef void (*job_read_cb)(int id,
+ void *data,
+ char *buffer,
+ uint32_t len,
+ bool from_stdout);
+
+/// Initializes job control resources
+void job_init(void);
+
+/// Releases job control resources and terminates running jobs
+void job_teardown(void);
+
+/// Tries to start a new job.
+///
+/// @param argv Argument vector for the process. The first item is the
+/// executable to run.
+/// @param data Caller data that will be associated with the job
+/// @param cb Callback that will be invoked everytime data is available in
+/// the job's stdout/stderr
+/// @return The job id if the job started successfully. If the the first item /
+/// of `argv`(the program) could not be executed, -1 will be returned.
+// 0 will be returned if the job table is full.
+int job_start(char **argv, void *data, job_read_cb cb);
+
+/// Terminates a job. This is a non-blocking operation, but if the job exists
+/// it's guaranteed to succeed(SIGKILL will eventually be sent)
+///
+/// @param id The job id
+/// @return true if the stop request was successfully sent, false if the job
+/// id is invalid(probably because it has already stopped)
+bool job_stop(int id);
+
+/// Writes data to the job's stdin. This is a non-blocking operation, it
+/// returns when the write request was sent.
+///
+/// @param id The job id
+/// @param data Buffer containing the data to be written
+/// @param len Size of the data
+/// @return true if the write request was successfully sent, false if the job
+/// id is invalid(probably because it has already stopped)
+bool job_write(int id, char *data, uint32_t len);
+
+/// Runs the read callback associated with the job/event
+///
+/// @param event Object containing data necessary to invoke the callback
+void job_handle(Event event);
+
+#endif // NEOVIM_OS_JOB_H
+
diff --git a/src/os/job_defs.h b/src/os/job_defs.h
new file mode 100644
index 0000000000..f1cb74d08d
--- /dev/null
+++ b/src/os/job_defs.h
@@ -0,0 +1,6 @@
+#ifndef NEOVIM_OS_JOB_DEFS_H
+#define NEOVIM_OS_JOB_DEFS_H
+
+typedef struct _Job Job;
+
+#endif // NEOVIM_OS_JOB_DEFS_H
diff --git a/src/os/signal.c b/src/os/signal.c
index 3595c63acb..58c959db6b 100644
--- a/src/os/signal.c
+++ b/src/os/signal.c
@@ -11,6 +11,7 @@
#include "memory.h"
#include "misc1.h"
#include "misc2.h"
+#include "os/event_defs.h"
#include "os/event.h"
#include "os/signal.h"
diff --git a/src/os/signal.h b/src/os/signal.h
index 4507bea26b..13231a0998 100644
--- a/src/os/signal.h
+++ b/src/os/signal.h
@@ -1,7 +1,7 @@
#ifndef NEOVIM_OS_SIGNAL_H
#define NEOVIM_OS_SIGNAL_H
-#include "os/event.h"
+#include "os/event_defs.h"
void signal_init(void);
void signal_stop(void);
diff --git a/src/os_unix.c b/src/os_unix.c
index c0fcc63fb0..9727140597 100644
--- a/src/os_unix.c
+++ b/src/os_unix.c
@@ -56,6 +56,7 @@
#include "os/input.h"
#include "os/shell.h"
#include "os/signal.h"
+#include "os/job.h"
#include "os_unixx.h" /* unix includes for os_unix.c only */
@@ -589,6 +590,7 @@ void mch_exit(int r)
{
exiting = TRUE;
+ job_teardown();
{
settmode(TMODE_COOK);
diff --git a/src/vim.h b/src/vim.h
index eeab1c8aa0..7922212cc8 100644
--- a/src/vim.h
+++ b/src/vim.h
@@ -792,6 +792,7 @@ enum auto_event {
EVENT_INSERTCHANGE, /* when changing Insert/Replace mode */
EVENT_INSERTENTER, /* when entering Insert mode */
EVENT_INSERTLEAVE, /* when leaving Insert mode */
+ EVENT_JOBACTIVITY, /* when job sent some data */
EVENT_MENUPOPUP, /* just before popup menu is displayed */
EVENT_QUICKFIXCMDPOST, /* after :make, :grep etc. */
EVENT_QUICKFIXCMDPRE, /* before :make, :grep etc. */
@@ -1304,6 +1305,7 @@ enum {
VV_HLSEARCH,
VV_OLDFILES,
VV_WINDOWID,
+ VV_JOB_DATA,
VV_LEN, /* number of v: vars */
};