This patch implements a simple but efficient form of job-control for vimscript, enabling vim to cooperate with long-running processes without blocking the UI.
It is my second attempt to bring multitasking to vim, but unlike the event-loop/message queue patch, this one does not rely on multithreading, using only system functions available on most unixes. If required, it could also be ported to windows(I think it has a concept called IOCP which provides a replacement to the 'select' system call). Here's a simple demonstration: ```vimscript " Start a background shell and save its id into the 'bgshell' variable. " " The first argument is the job name and will be matched with the JobActivity " autocommand pattern. " The second argument is the program name, with $PATH search rules applied " The third argument is optional, but if provided it must be a list of program " arguments let g:bgshell = jobstart('background-shell', 'bash') " This autocommand will be executed whenever jobs with the name maching " 'background-shell'(eg: background-*) send data to std{out,err}. " The v:job_data variable is a list, the first element being the job id " and the second/third being the data read from stdout/stderr respectively. " In this case, it simply echoes the shell stdout or stderr au JobActivity background-shell call s:CtagsDone() fun! s:CtagsDone() if v:job_data[1] != '' echohl StatusLine echo v:job_data[1] echohl None else echohl WarningMsg echo v:job_data[2][:-2] " Cut the trailing newline from the error message echohl None endif endfun " Whenever we write a C file, tell the background shell to run ctags and report " the result. The '\n' is needed to terminate the command in the shell. au BufWrite *.c call jobwrite(g:bgshell, "ctags -R && echo -n ctags done!\n") ``` Another example, wake vim every 10 seconds: ```vimscript let g:timer = jobstart('timer', 'sh', ['-c', 'while true; do sleep 10; echo wake!; done']) au JobActivity timer echo "Time's up!" ``` Jobs are destroyed when vim exits, but it can also be done at any time by calling the `jobstop` function with the job id as argument. Internally, polling is done in a function that replaces `ui_inchar` and interrupts the blocking wait every 100 milliseconds to check for job activity. When a job sends some data, it returns a special key code to the calling loop, which will trigger the autocommand, similar to how the CursorHold event is implemented. As far as most of vim's code is concerned, its just another key being pressed, which is certainly more stable than messing with the main loops like I did on the first versions of the event loop patch. This approach to multitasking doesn't come with threading pitfalls which are beyond our control, as exposed by @ashleyh here: https://github.com/tarruda/vim/issues/5 The best of this feature, is that it makes vim extensible by any scripting language(v8/node.js, python with threading...) without requiring the engine to be linked into the executable. All that's required is some vimscript code to communicate with it via the standard streams using some protocol. I hope someone enjoys this! Thiago. --- Filelist | 2 + runtime/doc/eval.txt | 2 + runtime/doc/various.txt | 1 + src/Makefile | 5 + src/config.h.in | 3 + src/configure.in | 10 +- src/edit.c | 6 + src/eval.c | 172 ++++++++++++++++- src/ex_getln.c | 5 + src/fileio.c | 6 +- src/getchar.c | 10 +- src/globals.h | 6 + src/job.c | 485 ++++++++++++++++++++++++++++++++++++++++++++++++ src/keymap.h | 3 + src/macros.h | 8 + src/main.c | 5 +- src/misc1.c | 2 +- src/normal.c | 19 ++ src/os_unix.c | 2 +- src/os_win32.c | 2 +- src/proto.h | 4 + src/proto/eval.pro | 1 + src/proto/job.pro | 8 + src/version.c | 5 + src/vim.h | 4 +- 25 files changed, 762 insertions(+), 14 deletions(-) create mode 100644 src/job.c create mode 100644 src/proto/job.pro -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to vim_dev+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/groups/opt_out.
diff --git a/Filelist b/Filelist index b324933..77fc0a9 100644 --- a/Filelist +++ b/Filelist @@ -33,6 +33,7 @@ SRC_ALL = \ src/gui_beval.h \ src/hardcopy.c \ src/hashtab.c \ + src/job.c \ src/keymap.h \ src/macros.h \ src/main.c \ @@ -113,6 +114,7 @@ SRC_ALL = \ src/proto/gui_beval.pro \ src/proto/hardcopy.pro \ src/proto/hashtab.pro \ + src/proto/job.pro \ src/proto/main.pro \ src/proto/mark.pro \ src/proto/mbyte.pro \ diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index be0e667..d09afe1 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -6410,6 +6410,8 @@ ebcdic Compiled on a machine with ebcdic character set. emacs_tags Compiled with support for Emacs tags. eval Compiled with expression evaluation support. Always true, of course! +job_control Compiled with job control functions and autocommand + for multitasking with other processes. ex_extra Compiled with extra Ex commands |+ex_extra|. extra_search Compiled with support for |'incsearch'| and |'hlsearch'| diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt index 4a1c64a..f3d0b30 100644 --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -324,6 +324,7 @@ N *+digraphs* |digraphs| *E196* *+dnd* Support for DnD into the "~ register |quote_~|. B *+emacs_tags* |emacs-tags| files N *+eval* expression evaluation |eval.txt| + *+job_control* Compiled with job control module N *+ex_extra* Vim's extra Ex commands: |:center|, |:left|, |:normal|, |:retab| and |:right| N *+extra_search* |'hlsearch'| and |'incsearch'| options. diff --git a/src/Makefile b/src/Makefile index bceb65c..58cd6f1 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1443,6 +1443,7 @@ BASIC_SRC = \ hashtab.c \ if_cscope.c \ if_xcmdsrv.c \ + job.c \ main.c \ mark.c \ memfile.c \ @@ -1533,6 +1534,7 @@ OBJ_COMMON = \ $(HANGULIN_OBJ) \ objects/if_cscope.o \ objects/if_xcmdsrv.o \ + objects/job.o \ objects/mark.o \ objects/memline.o \ objects/menu.o \ @@ -2625,6 +2627,9 @@ objects/if_tcl.o: if_tcl.c objects/integration.o: integration.c $(CCC) -o $@ integration.c +objects/job.o: job.c + $(CCC) -o $@ job.c + objects/main.o: main.c $(CCC) -o $@ main.c diff --git a/src/config.h.in b/src/config.h.in index a4e216b..501e78c 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -323,6 +323,9 @@ /* Define for linking via dlopen() or LoadLibrary() */ #undef DYNAMIC_LUA +/* Define if you want to use thread-safe message queue. */ +#undef FEAT_JOB_CONTROL + /* Define if you want to include the MzScheme interpreter. */ #undef FEAT_MZSCHEME diff --git a/src/configure.in b/src/configure.in index 2718d31..795468a 100644 --- a/src/configure.in +++ b/src/configure.in @@ -2801,7 +2801,6 @@ AC_CHECK_HEADERS(sys/sysctl.h, [], [], # include <sys/param.h> #endif]) - dnl pthread_np.h may exist but can only be used after including pthread.h AC_MSG_CHECKING([for pthread_np.h]) AC_TRY_COMPILE([ @@ -2812,6 +2811,15 @@ AC_TRY_COMPILE([ AC_DEFINE(HAVE_PTHREAD_NP_H), AC_MSG_RESULT(no)) +AC_MSG_CHECKING(--enable-jobcontrol argument) +AC_ARG_ENABLE(jobcontrol, + [ --enable-jobcontrol[=OPTS] Include job control module. [default=no] [OPTS=no/yes]], , + [enable_jobcontrol="no"]) +AC_MSG_RESULT($enable_jobcontrol) +if test "$enable_jobcontrol" = "yes"; then + AC_DEFINE(FEAT_JOB_CONTROL) +fi + AC_CHECK_HEADERS(strings.h) if test "x$MACOSX" = "xyes"; then dnl The strings.h file on OS/X contains a warning and nothing useful. diff --git a/src/edit.c b/src/edit.c index d3c0208..0bfb1a3 100644 --- a/src/edit.c +++ b/src/edit.c @@ -1188,6 +1188,12 @@ doESCkey: break; #endif +#ifdef FEAT_JOB_CONTROL + case K_JOB_ACTIVITY: + job_activity_autocmds(); + break; +#endif + #ifdef FEAT_GUI_W32 /* On Win32 ignore <M-F4>, we get it when closing the window was * cancelled. */ diff --git a/src/eval.c b/src/eval.c index 8a62fa8..34db1c5 100644 --- a/src/eval.c +++ b/src/eval.c @@ -359,6 +359,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 */ @@ -422,7 +423,6 @@ static int dict_equal __ARGS((dict_T *d1, dict_T *d2, int ic, int recursive)); static int tv_equal __ARGS((typval_T *tv1, typval_T *tv2, int ic, int recursive)); static long list_find_nr __ARGS((list_T *l, long idx, int *errorp)); static long list_idx_of_item __ARGS((list_T *l, listitem_T *item)); -static int list_append_number __ARGS((list_T *l, varnumber_T n)); static int list_extend __ARGS((list_T *l1, list_T *l2, listitem_T *bef)); static int list_concat __ARGS((list_T *l1, list_T *l2, typval_T *tv)); static list_T *list_copy __ARGS((list_T *orig, int deep, int copyID)); @@ -591,6 +591,9 @@ static void f_invert __ARGS((typval_T *argvars, typval_T *rettv)); static void f_isdirectory __ARGS((typval_T *argvars, typval_T *rettv)); static void f_islocked __ARGS((typval_T *argvars, typval_T *rettv)); static void f_items __ARGS((typval_T *argvars, typval_T *rettv)); +static void f_job_start __ARGS((typval_T *argvars, typval_T *rettv)); +static void f_job_stop __ARGS((typval_T *argvars, typval_T *rettv)); +static void f_job_write __ARGS((typval_T *argvars, typval_T *rettv)); static void f_join __ARGS((typval_T *argvars, typval_T *rettv)); static void f_keys __ARGS((typval_T *argvars, typval_T *rettv)); static void f_last_buffer_nr __ARGS((typval_T *argvars, typval_T *rettv)); @@ -6385,7 +6388,7 @@ list_append_string(l, str, len) * Append "n" to list "l". * Returns FAIL when out of memory. */ - static int + int list_append_number(l, n) list_T *l; varnumber_T n; @@ -7983,6 +7986,11 @@ static struct fst {"isdirectory", 1, 1, f_isdirectory}, {"islocked", 1, 1, f_islocked}, {"items", 1, 1, f_items}, +#ifdef FEAT_JOB_CONTROL + {"jobstart", 2, 3, f_job_start}, + {"jobstop", 1, 1, f_job_stop}, + {"jobwrite", 2, 2, f_job_write}, +#endif {"join", 1, 2, f_join}, {"keys", 1, 1, f_keys}, {"last_buffer_nr", 0, 0, f_last_buffer_nr},/* obsolete */ @@ -12248,6 +12256,9 @@ f_has(argvars, rettv) "emacs_tags", #endif "eval", /* always present, of course! */ +#ifdef FEAT_JOB_CONTROL + "job_control", +#endif #ifdef FEAT_EX_EXTRA "ex_extra", #endif @@ -13485,6 +13496,163 @@ f_items(argvars, rettv) dict_list(argvars, rettv, 2); } +#ifdef FEAT_JOB_CONTROL +/* + * "jobstart()" function + */ + static void +f_job_start(argvars, rettv) + typval_T *argvars; + typval_T *rettv; +{ + list_T *args = NULL; + listitem_T *arg; + int i, argvl, argsl; + char_u **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)) + { + 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 (!mch_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 = (char_u **)alloc(sizeof(char_u *) * argvl); + if (argv == NULL) + goto cleanup; + + /* Initialize the array */ + for (i = 0; i < argvl; ++i) + argv[i] = NULL; + + /* Copy program name */ + argv[0] = vim_strsave(argvars[1].vval.v_string); + if (argv[0] == NULL) + goto cleanup; + + + i = 1; + /* Copy arguments to the vector */ + if (argsl > 0) + for (arg = args->lv_first; arg != NULL; arg = arg->li_next) + { + argv[i] = vim_strsave(arg->li_tv.vval.v_string); + if (argv[i++] == NULL) + goto cleanup; + } + + rettv->vval.v_number = + job_start(vim_strsave(argvars[0].vval.v_string), argv); + + if (rettv->vval.v_number == 0) + { + EMSG(_(e_jobtblfull)); + goto cleanup; + } + +cleanup: + if (rettv->vval.v_number > 0 || argv == NULL) + return; + /* Cleanup */ + for (i = 0; i < argvl; ++i) + vim_free(argv[i]); + vim_free(argv); +} + +/* + * "jobstop()" function + */ + static void +f_job_stop(argvars, rettv) + 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) + { + EMSG(_(e_invarg)); + return; + } + + if (job_stop(argvars[0].vval.v_number) <= 0) + { + EMSG(_(e_invjob)); + return; + } + + rettv->vval.v_number = 1; +} + +/* + * "jobwrite()" function + */ + static void +f_job_write(argvars, rettv) + typval_T *argvars; + typval_T *rettv; +{ + int 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) + { + EMSG(_(e_invarg)); + return; + } + + res = job_write(argvars[0].vval.v_number, argvars[1].vval.v_string, + STRLEN(argvars[1].vval.v_string)); + + if (res <= 0) + { + if (res == -1) EMSG(_(e_invjob)); + else EMSG(_(e_outofmem)); + } + + rettv->vval.v_number = 1; +} +#endif + /* * "join()" function */ diff --git a/src/ex_getln.c b/src/ex_getln.c index f1f39d2..29e56c3 100644 --- a/src/ex_getln.c +++ b/src/ex_getln.c @@ -926,6 +926,11 @@ getcmdline(firstc, count, indent) */ switch (c) { +#ifdef FEAT_JOB_CONTROL + case K_JOB_ACTIVITY: + job_activity_autocmds(); + goto cmdline_not_changed; +#endif case K_BS: case Ctrl_H: case K_DEL: diff --git a/src/fileio.c b/src/fileio.c index cb22bd3..b90fc19 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -7733,6 +7733,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}, @@ -9386,7 +9387,7 @@ apply_autocmds_group(event, fname, fname_io, force, group, buf, eap) { 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 @@ -9394,7 +9395,8 @@ apply_autocmds_group(event, fname, fname_io, force, group, buf, eap) || 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/getchar.c b/src/getchar.c index fe6423d..ed195b3 100644 --- a/src/getchar.c +++ b/src/getchar.c @@ -3019,7 +3019,7 @@ inchar(buf, maxlen, wait_time, tb_change_cnt) for (;;) { - len = ui_inchar(dum, DUM_LEN, 0L, 0); + len = io_inchar(dum, DUM_LEN, 0L, 0); if (len == 0 || (len == 1 && dum[0] == 3)) break; } @@ -3036,7 +3036,7 @@ inchar(buf, maxlen, wait_time, tb_change_cnt) * Fill up to a third of the buffer, because each character may be * tripled below. */ - len = ui_inchar(buf, maxlen / 3, wait_time, tb_change_cnt); + len = io_inchar(buf, maxlen / 3, wait_time, tb_change_cnt); } if (typebuf_changed(tb_change_cnt)) @@ -3091,13 +3091,15 @@ fix_input_buffer(buf, len, script) if (p[0] == NUL || (p[0] == K_SPECIAL && !script #ifdef FEAT_AUTOCMD /* timeout may generate K_CURSORHOLD */ - && (i < 2 || p[1] != KS_EXTRA || p[2] != (int)KE_CURSORHOLD) + && (i < 2 || p[1] != KS_EXTRA + || (p[2] != (int)KE_CURSORHOLD + && p[2] != (int)KE_JOB_ACTIVITY) #endif #if defined(WIN3264) && !defined(FEAT_GUI) /* Win32 console passes modifiers */ && (i < 2 || p[1] != KS_MODIFIER) #endif - )) + ))) { mch_memmove(p + 3, p + 1, (size_t)i); p[2] = K_THIRD(p[0]); diff --git a/src/globals.h b/src/globals.h index 916f7e3..7053051 100644 --- a/src/globals.h +++ b/src/globals.h @@ -1433,6 +1433,12 @@ EXTERN char_u e_invcmd[] INIT(= N_("E476: Invalid command")); #if defined(UNIX) || defined(FEAT_SYN_HL) || defined(FEAT_SPELL) EXTERN char_u e_isadir2[] INIT(= N_("E17: \"%s\" is a directory")); #endif +#ifdef FEAT_JOB_CONTROL +EXTERN char_u e_invjob[] INIT(= N_("E900: Invalid job id")); +EXTERN char_u e_jobpollerr[] INIT(= N_("E901: Job poll failed")); +EXTERN char_u e_jobtblfull[] INIT(= N_("E902: Job table is full")); +EXTERN char_u e_jobexe[] INIT(= N_("E902: '%s' is not an executable")); +#endif #ifdef FEAT_LIBCALL EXTERN char_u e_libcall[] INIT(= N_("E364: Library call failed for \"%s()\"")); #endif diff --git a/src/job.c b/src/job.c new file mode 100644 index 0000000..0a9e8a8 --- /dev/null +++ b/src/job.c @@ -0,0 +1,485 @@ +/* vi:set ts=8 sts=4 sw=4: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + */ + +/* Job control module for cooperating with child processes in a non-blocking + * way. + * + * Job polling is done by 'job_activity_poll', a drop-in replacement for + * 'ui_inchar' which is always called when idling for new characters are + * needed. + * + * When some job produces data, the K_JOB_ACTIVITY key is returned, + * which will be handled at higher levels by triggering 'JobActivity' + * autocommands matchin the job name. */ +#include "vim.h" + +#ifdef FEAT_JOB_CONTROL + +#include <unistd.h> +#include <signal.h> +#include <sys/select.h> +#include <sys/types.h> + +#define POLL_INTERVAL 100 /* Interval used to poll for job activity */ +#define KILL_TIMEOUT 25 +#define MAX_RUNNING_JOBS 5 +#define BUF_SIZE 4096 +#define max(x,y) ((x) > (y) ? (x) : (y)) + +/* Check if the job_id is valid */ +#define JOB_CHECK if (job_id <= 0 || job_id > MAX_RUNNING_JOBS || \ + (job = job_table[job_id - 1]) == NULL) \ + return -1 + +/* Check if the job's process is alive */ +#define IS_ALIVE(job) kill(job->pid, 0) != -1 + +/* Free job in a loop */ +#define JOB_FREE \ + close(job->in); \ + close(job->out); \ + close(job->err); \ + for (arg = job->argv; *arg != NULL; arg++) vim_free(*arg); \ + vim_free(job->argv); \ + vim_free(job->name); \ + job_table[job->id - 1] = NULL; \ + vim_free(job); + +struct job_T { + int id, in, out, err, stopped, kill_timeout; + pid_t pid; + char_u **argv; + /* Name is used to match JobActivity autocmds */ + char_u *name; + /* Fixed-width buffer for stdout/stderr */ + char_u stdout_buf[BUF_SIZE], stderr_buf[BUF_SIZE]; + unsigned int stdout_buf_pos, stderr_buf_pos; + /* Pending stdin data is stored as a linked list, where each + * node points the data to be written and tracks its length and + * position in order to free and advance to the next node correctly. */ + struct in_buf_node_T { + char_u *data; + int len, pos; + struct in_buf_node_T *next; + } *stdin_head, *stdin_tail; +} *job_table[MAX_RUNNING_JOBS] = {NULL}; + + +int job_count = 0; +int initialized = FALSE; + + +/* Poll every running job that has space in its buffers for data. */ + static int +jobs_poll() +{ + int res, mfd, i; + fd_set rfds, wfds; + struct timeval tv; + struct job_T *job; + struct in_buf_node_T *chunk; + size_t count, to_write; + + if (!job_count) + return FALSE; + + FD_ZERO(&rfds); + FD_ZERO(&wfds); + mfd = 0; + + /* Iterate through each job, either adding their fds to the appropriate + * select set or killing stopped jobs */ + for (i = 0; i < MAX_RUNNING_JOBS; i++) + { + if ((job = job_table[i]) == NULL) continue; + if (job->stopped) + { + if (job->kill_timeout-- == KILL_TIMEOUT) + { + /* Vimscript stopped this job, close stdin and + * send SIGTERM */ + close(job->in); + kill(job->pid, SIGTERM); + } + else if (job->kill_timeout == 0) + { + /* We've waited too long, send SIGKILL */ + kill(job->pid, SIGKILL); + } + } + else + { + /* Only poll stdin if we have something to write */ + if (job->stdin_head != NULL) + { + FD_SET(job->in, &wfds); + mfd = max(mfd, job->in); + } + + /* Only poll stdout/stderr if we have room in the buffer */ + if (job->stdout_buf_pos < BUF_SIZE) + { + FD_SET(job->out, &rfds); + mfd = max(mfd, job->out); + } + + if (job->stderr_buf_pos < BUF_SIZE) + { + FD_SET(job->err, &rfds); + mfd = max(mfd, job->err); + } + } + } + + /* This argument is the maximum fd plus one */ + mfd++; + + /* Check what fds wont block when read/written */ + tv.tv_sec = 0; + tv.tv_usec = 0; + res = select(mfd, &rfds, &wfds, NULL, &tv); + + if (res <= 0) + { + if (res == -1 && errno != EINTR) + EMSG(_(e_jobpollerr)); + return FALSE; + } + + /* Read/write pending data from/to stdfds */ + for (i = 0; i < MAX_RUNNING_JOBS; i++) + { + if ((job = job_table[i]) == NULL) continue; + + /* Collect pending stdout/stderr data into the job buffers */ + if (FD_ISSET(job->out, &rfds)) + { + count = read(job->out, job->stdout_buf + job->stdout_buf_pos, + BUF_SIZE - job->stdout_buf_pos); + job->stdout_buf_pos += count; + } + if (FD_ISSET(job->err, &rfds)) + { + count = read(job->err, job->stderr_buf + job->stderr_buf_pos, + BUF_SIZE - job->stderr_buf_pos); + job->stderr_buf_pos += count; + } + + if (FD_ISSET(job->in, &wfds)) + { + /* Stdin ready, write as much data as possible */ + while (job->stdin_head != NULL) + { + chunk = job->stdin_head; + to_write = chunk->len - chunk->pos; + count = write(job->in, chunk->data, to_write); + + if (count != to_write) + { + /* Not enough space in the OS buffer, advance position + * in the head and break */ + chunk->pos += count; + break; + } + + /* We have written everything on this chunk, advance + * to the next */ + job->stdin_head = chunk->next; + vim_free(chunk->data); + vim_free(chunk); + } + } + } + + return TRUE; +} + +/* Set the JOB_ACTIVITY special key into the input buffer */ + static int +job_activity(buf) + char_u *buf; +{ + buf[0] = K_SPECIAL; + buf[1] = KS_EXTRA; + buf[2] = (int)KE_JOB_ACTIVITY; + + return 3; +} + + +/* Set the CURSORHOLD special key into the input buffer */ + static int +cursorhold(buf) + char_u *buf; +{ + buf[0] = K_SPECIAL; + buf[1] = KS_EXTRA; + buf[2] = (int)KE_CURSORHOLD; + + return 3; +} + + +/* Start a job and return its id. */ + int +job_start(name, argv) + char_u *name; + char_u **argv; +{ + struct job_T *job = NULL; + int in[2], out[2], err[2], i; + + if (!initialized) + { + signal(SIGCHLD, SIG_IGN); + initialized = TRUE; + } + + for (i = 0; i < MAX_RUNNING_JOBS; i++) + if (job_table[i] == NULL) + break; + + if (i == MAX_RUNNING_JOBS) + /* No more free slots */ + return 0; + + job = (struct job_T *)alloc(sizeof(struct job_T)); + + /* Create pipes for the stdio streams */ + pipe(in); + pipe(out); + pipe(err); + + if ((job->pid = fork()) == 0) + { + /* Child, copy the child parts of the pipes into the appropriate + * stdio fds */ + dup2(in[0], 0); + dup2(out[1], 1); + dup2(err[1], 2); + + /* TODO close all open file descriptors > 2 in a more reliable + * way. + * + * For some options, see: + * http://stackoverflow.com/questions/899038/getting-the-highest-allocated-file-descriptor/918469#918469 */ + for (i = 3; i <= 2048; i++) + close(i); + + /* Reset ignored signal handlers(does vim ignore other signals?) */ + signal(SIGCHLD, SIG_DFL); + + /* Exec program */ + execvp((const char *)argv[0], (char **)argv); + } + else + { + /* Close the other sides of the pipes */ + close(in[0]); + close(out[1]); + close(err[1]); + + /* Parent, initialize job structure */ + job->id = i + 1; + job->name = name; + job->argv = argv; + job->in = in[1]; + job->out = out[0]; + job->err = err[0]; + job->stdin_head = job->stdin_tail = NULL; + job->stdout_buf_pos = 0; + job->stderr_buf_pos = 0; + job->stopped = FALSE; + job->kill_timeout = KILL_TIMEOUT; + + /* Insert into the job table */ + job_table[i] = job; + job_count++; + } + + /* Return job id */ + return job->id; +} + +/* Stop a job */ + int +job_stop(job_id) + int job_id; +{ + struct job_T *job; + + JOB_CHECK; + job->stopped = TRUE; + + return 1; +} + +/* Write data to the stdin of a job's process */ + int +job_write(job_id, data, len) + int job_id; + char_u *data; + unsigned int len; +{ + struct job_T *job; + struct in_buf_node_T *chunk; + + JOB_CHECK; + + chunk = (struct in_buf_node_T *)alloc(sizeof(struct in_buf_node_T)); + + if (chunk == NULL) + return 0; /* Not enough memory */ + + chunk->data = (char_u *)alloc(len); + + if (chunk->data == NULL) + { + vim_free(chunk); + return 0; /* Not enough memory */ + } + + chunk->len = len; + chunk->pos = 0; + chunk->next = NULL; + mch_memmove(chunk->data, data, len); + + if (job->stdin_head == NULL) + job->stdin_head = job->stdin_tail = chunk; + else + job->stdin_tail = job->stdin_tail->next = chunk; + + return 1; +} + +/* Cleanup all jobs */ + void +jobs_cleanup() +{ + struct timeval tv; + int i, kill_now = FALSE; + struct job_T *job; + char_u **arg; + + /* Politely ask each job to terminate */ + for (i = 0; i < MAX_RUNNING_JOBS; i++) + if ((job = job_table[i]) != NULL) + { + close(job->in); + kill(job_table[i]->pid, SIGTERM); + } + + /* Give at most 300 ms for all jobs to exit, then start shooting */ + for (i = 0; i < MAX_RUNNING_JOBS; i++) + { + if ((job = job_table[i]) == NULL) continue; + if (IS_ALIVE(job)) + { + if (!kill_now) + { + tv.tv_sec = 0; + tv.tv_usec = 300000; + select(0, NULL, NULL, NULL, &tv); + kill_now = TRUE; + } + kill(job->pid, SIGKILL); + } + JOB_FREE; + } +} + +/* + * Bridge between vim and the job control module, 'disguised' as a + * function that returns keys(where one of the special keys is K_JOBACTIVITY + */ + int +job_activity_poll(buf, maxlen, wtime, tb_change_cnt) + char_u *buf; + int maxlen; + long wtime; + int tb_change_cnt; +{ + int len; + int trig_curshold; + long ellapsed; + + /* Dont poll for job activity when a timeout is passed */ + if (wtime >= 0) + return ui_inchar(buf, maxlen, wtime, tb_change_cnt); + + trig_curshold = trigger_cursorhold(); + + if (trig_curshold) + ellapsed = 0; /* Time waiting for a char in milliseconds */ + else + before_blocking(); /* Normally called when doing a blocking wait */ + + do + { + len = ui_inchar(buf, maxlen, POLL_INTERVAL, tb_change_cnt); + if (len > 0) + return len; /* User-initiated input */ + + if (jobs_poll()) + return job_activity(buf); + + /* We must trigger cursorhold events ourselves. Normally cursorholds + * are triggered at a platform-specific lower function when an + * infinite timeout is passed, but those won't get the chance + * because we never pass infinite timeout in order to poll for + * job activity */ + if (trig_curshold) + { + ellapsed += POLL_INTERVAL; + if (ellapsed >= p_ut) + return cursorhold(buf); + } + } while (1); +} + +/* + * Invoke the JobActivity autocommand. Called by other layers after we return + * K_JOBACTIVITY. + */ + void +job_activity_autocmds() +{ + struct job_T *job; + char_u **arg; + list_T *list; + int i, alive; + + for (i = 0; i < MAX_RUNNING_JOBS; i++) + { + if ((job = job_table[i]) == NULL) + continue; + + alive = IS_ALIVE(job); + /* Ignore alive jobs that were stopped or that have no data available + * stderr/stdout */ + if (alive && job->stdout_buf_pos == 0 && job->stderr_buf_pos == 0) + continue; + + list = list_alloc(); + list_append_number(list, job->id); + list_append_string(list, job->stdout_buf, job->stdout_buf_pos); + list_append_string(list, job->stderr_buf, job->stderr_buf_pos); + job->stdout_buf_pos = job->stderr_buf_pos = 0; + set_vim_var_list(VV_JOB_DATA, list); + apply_autocmds(EVENT_JOBACTIVITY, job->name, NULL, TRUE, NULL); + + if (!alive) + { + /* Process exited, free the job memory and remove it from + * the table */ + JOB_FREE; + continue; + } + } +} +#endif diff --git a/src/keymap.h b/src/keymap.h index 986006d..34b6dea 100644 --- a/src/keymap.h +++ b/src/keymap.h @@ -267,6 +267,7 @@ enum key_extra , KE_NOP /* doesn't do something */ , KE_FOCUSGAINED /* focus gained */ , KE_FOCUSLOST /* focus lost */ + , KE_JOB_ACTIVITY /* One or more jobs have data available */ }; /* @@ -467,6 +468,8 @@ enum key_extra #define K_CURSORHOLD TERMCAP2KEY(KS_EXTRA, KE_CURSORHOLD) +#define K_JOB_ACTIVITY TERMCAP2KEY(KS_EXTRA, KE_JOB_ACTIVITY) + /* Bits for modifier mask */ /* 0x01 cannot be used, because the modifier must be 0x02 or higher */ #define MOD_MASK_SHIFT 0x02 diff --git a/src/macros.h b/src/macros.h index f4a068c..9442348 100644 --- a/src/macros.h +++ b/src/macros.h @@ -303,3 +303,11 @@ # endif # endif #endif + +#if defined(FEAT_JOB_CONTROL) +# define io_inchar(buf, maxlen, wtime, tb_change_cnt) \ + job_activity_poll(buf, maxlen, wtime, tb_change_cnt) +#else +# define io_inchar(buf, maxlen, wtime, tb_change_cnt) \ + ui_inchar(buf, maxlen, wtime, tb_change_cnt) +#endif diff --git a/src/main.c b/src/main.c index 0407795..e9a75e5 100644 --- a/src/main.c +++ b/src/main.c @@ -171,7 +171,6 @@ main #ifdef STARTUPTIME int i; #endif - /* * Do any system-specific initialisations. These can NOT use IObuff or * NameBuff. Thus emsg2() cannot be called! @@ -1367,6 +1366,10 @@ getout(exitval) exiting = TRUE; +#ifdef FEAT_JOB_CONTROL + jobs_cleanup(); +#endif + /* When running in Ex mode an error causes us to exit with a non-zero exit * code. POSIX requires this, although it's not 100% clear from the * standard. */ diff --git a/src/misc1.c b/src/misc1.c index b258d0b..eac3cfd 100644 --- a/src/misc1.c +++ b/src/misc1.c @@ -3378,7 +3378,7 @@ get_keystroke() /* First time: blocking wait. Second time: wait up to 100ms for a * terminal code to complete. */ - n = ui_inchar(buf + len, maxlen, len == 0 ? -1L : 100L, 0); + n = io_inchar(buf + len, maxlen, len == 0 ? -1L : 100L, 0); if (n > 0) { /* Replace zero and CSI by a special key code. */ diff --git a/src/normal.c b/src/normal.c index f76aeee..495256a 100644 --- a/src/normal.c +++ b/src/normal.c @@ -186,6 +186,9 @@ static void nv_drop __ARGS((cmdarg_T *cap)); #ifdef FEAT_AUTOCMD static void nv_cursorhold __ARGS((cmdarg_T *cap)); #endif +#ifdef FEAT_JOB_CONTROL +static void nv_jobactivity __ARGS((cmdarg_T *cap)); +#endif static char *e_noident = N_("E349: No identifier under cursor"); @@ -454,6 +457,9 @@ static const struct nv_cmd #ifdef FEAT_AUTOCMD {K_CURSORHOLD, nv_cursorhold, NV_KEEPREG, 0}, #endif +#ifdef FEAT_JOB_CONTROL + {K_JOB_ACTIVITY, nv_jobactivity, NV_KEEPREG, 0}, +#endif }; /* Number of commands in nv_cmds[]. */ @@ -9631,3 +9637,16 @@ nv_cursorhold(cap) cap->retval |= CA_COMMAND_BUSY; /* don't call edit() now */ } #endif + +#ifdef FEAT_JOB_CONTROL +/* + * Trigger JobActivity event + */ + static void +nv_jobactivity(cap) + cmdarg_T *cap; +{ + job_activity_autocmds(); + cap->retval |= CA_COMMAND_BUSY; /* don't call edit() now */ +} +#endif diff --git a/src/os_unix.c b/src/os_unix.c index 784c5a3..0da6cbc 100644 --- a/src/os_unix.c +++ b/src/os_unix.c @@ -4535,7 +4535,7 @@ mch_call_shell(cmd, options) # if defined(HAVE_GETTIMEOFDAY) && defined(HAVE_SYS_TIME_H) gettimeofday(&start_tv, NULL); # endif - len = ui_inchar(ta_buf, BUFLEN, 10L, 0); + len = io_inchar(ta_buf, BUFLEN, 10L, 0); } if (ta_len > 0 || len > 0) { diff --git a/src/os_win32.c b/src/os_win32.c index 4feb697..6c23ef8 100644 --- a/src/os_win32.c +++ b/src/os_win32.c @@ -4380,7 +4380,7 @@ mch_system_piped(char *cmd, int options) # if defined(HAVE_GETTIMEOFDAY) && defined(HAVE_SYS_TIME_H) gettimeofday(&start_tv, NULL); # endif - len = ui_inchar(ta_buf, BUFLEN, 10L, 0); + len = io_inchar(ta_buf, BUFLEN, 10L, 0); } if (ta_len > 0 || len > 0) { diff --git a/src/proto.h b/src/proto.h index 191ecd8..d1f3ce0 100644 --- a/src/proto.h +++ b/src/proto.h @@ -133,6 +133,10 @@ int vim_vsnprintf(char *str, size_t str_m, char *fmt, va_list ap, typval_T *tvs) # endif # endif +# ifdef FEAT_JOB_CONTROL +# include "job.pro" +# endif + # include "message.pro" # include "misc1.pro" # include "misc2.pro" diff --git a/src/proto/eval.pro b/src/proto/eval.pro index ee2da1b..18a27bf 100644 --- a/src/proto/eval.pro +++ b/src/proto/eval.pro @@ -57,6 +57,7 @@ char_u *list_find_str __ARGS((list_T *l, long idx)); void list_append __ARGS((list_T *l, listitem_T *item)); int list_append_tv __ARGS((list_T *l, typval_T *tv)); int list_append_dict __ARGS((list_T *list, dict_T *dict)); +int list_append_number __ARGS((list_T *l, varnumber_T n)); int list_append_string __ARGS((list_T *l, char_u *str, int len)); int list_insert_tv __ARGS((list_T *l, typval_T *tv, listitem_T *item)); void list_remove __ARGS((list_T *l, listitem_T *item, listitem_T *item2)); diff --git a/src/proto/job.pro b/src/proto/job.pro new file mode 100644 index 0000000..efe24ea --- /dev/null +++ b/src/proto/job.pro @@ -0,0 +1,8 @@ +/* job.c */ +int job_start __ARGS((char_u *name, char_u **argv)); +int job_stop __ARGS((int job_id)); +int job_write __ARGS((int job_id, char_u *data, unsigned int len)); +void jobs_cleanup __ARGS((void)); +int job_activity_poll __ARGS((char_u *buf, int maxlen, long wtime, + int tb_change_cnt)); +void job_activity_autocmds __ARGS((void)); diff --git a/src/version.c b/src/version.c index 1fd98ea..412cc1f 100644 --- a/src/version.c +++ b/src/version.c @@ -209,6 +209,11 @@ static char *(features[]) = #else "-eval", #endif +#ifdef FEAT_JOB_CONTROL + "+job_control", +#else + "-job_control", +#endif #ifdef FEAT_EX_EXTRA "+ex_extra", #else diff --git a/src/vim.h b/src/vim.h index 88f3dc2..c9660c7 100644 --- a/src/vim.h +++ b/src/vim.h @@ -1280,6 +1280,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. */ @@ -1876,7 +1877,8 @@ typedef int proftime_T; /* dummy for function prototypes */ #define VV_HLSEARCH 54 #define VV_OLDFILES 55 #define VV_WINDOWID 56 -#define VV_LEN 57 /* number of v: vars */ +#define VV_JOB_DATA 57 +#define VV_LEN 58 /* number of v: vars */ #ifdef FEAT_CLIPBOARD