0
0
mirror of https://github.com/vim/vim.git synced 2025-09-25 03:54:15 -04:00

patch 8.0.0902: cannot specify directory or environment for a job

Problem:    Cannot specify directory or environment for a job.
Solution:   Add the "cwd" and "env" arguments to job options. (Yasuhiro
            Matsumoto, closes #1160)
This commit is contained in:
Bram Moolenaar
2017-08-11 19:12:11 +02:00
parent 76ca1b4041
commit 05aafed54b
9 changed files with 252 additions and 43 deletions

View File

@@ -427,8 +427,8 @@ When no message was available then the result is v:none for a JSON or JS mode
channels, an empty string for a RAW or NL channel. You can use |ch_canread()| channels, an empty string for a RAW or NL channel. You can use |ch_canread()|
to check if there is something to read. to check if there is something to read.
Note that when there is no callback message are dropped. To avoid that add a Note that when there is no callback, messages are dropped. To avoid that add
close callback to the channel. a close callback to the channel.
To read all output from a RAW channel that is available: > To read all output from a RAW channel that is available: >
let output = ch_readraw(channel) let output = ch_readraw(channel)
@@ -475,11 +475,6 @@ it like this: >
Without the handler you need to read the output with |ch_read()| or Without the handler you need to read the output with |ch_read()| or
|ch_readraw()|. You can do this in the close callback, see |read-in-close-cb|. |ch_readraw()|. You can do this in the close callback, see |read-in-close-cb|.
Note that if the job exits before you read the output, the output may be lost.
This depends on the system (on Unix this happens because closing the write end
of a pipe causes the read end to get EOF). To avoid this make the job sleep
for a short while before it exits.
The handler defined for "out_cb" will not receive stderr. If you want to The handler defined for "out_cb" will not receive stderr. If you want to
handle that separately, add an "err_cb" handler: > handle that separately, add an "err_cb" handler: >
let job = job_start(command, {"out_cb": "MyHandler", let job = job_start(command, {"out_cb": "MyHandler",
@@ -494,6 +489,11 @@ started job gets the focus. To avoid that, use the `foreground()` function.
This might not always work when called early, put in the callback handler or This might not always work when called early, put in the callback handler or
use a timer to call it after the job has started. use a timer to call it after the job has started.
Depending on the system, starting a job can put Vim in the background, the
started job gets the focus. To avoid that, use the `foreground()` function.
This might not always work when called early, put in the callback handler or
use a timer to call it after the job has started.
You can send a message to the command with ch_evalraw(). If the channel is in You can send a message to the command with ch_evalraw(). If the channel is in
JSON or JS mode you can use ch_evalexpr(). JSON or JS mode you can use ch_evalexpr().
@@ -696,6 +696,10 @@ See |job_setoptions()| and |ch_setoptions()|.
"block_write": number only for testing: pretend every other write to stdin "block_write": number only for testing: pretend every other write to stdin
will block will block
"env": dict environment variables for the new process
"cwd": "/path/to/dir" current working directory for the new process;
if the directory does not exist an error is given
Writing to a buffer ~ Writing to a buffer ~
*out_io-buffer* *out_io-buffer*
@@ -731,10 +735,6 @@ The "out_msg" option can be used to specify whether a new buffer will have the
first line set to "Reading from channel output...". The default is to add the first line set to "Reading from channel output...". The default is to add the
message. "err_msg" does the same for channel error. message. "err_msg" does the same for channel error.
'modifiable' option off, or write to a buffer that has 'modifiable' off. That
means that lines will be appended to the buffer, but the user can't easily
change the buffer.
When an existing buffer is to be written where 'modifiable' is off and the When an existing buffer is to be written where 'modifiable' is off and the
"out_modifiable" or "err_modifiable" options is not zero, an error is given "out_modifiable" or "err_modifiable" options is not zero, an error is given
and the buffer will not be written to. and the buffer will not be written to.

View File

@@ -4153,6 +4153,8 @@ free_job_options(jobopt_T *opt)
partial_unref(opt->jo_exit_partial); partial_unref(opt->jo_exit_partial);
else if (opt->jo_exit_cb != NULL) else if (opt->jo_exit_cb != NULL)
func_unref(opt->jo_exit_cb); func_unref(opt->jo_exit_cb);
if (opt->jo_env != NULL)
dict_unref(opt->jo_env);
} }
/* /*
@@ -4433,6 +4435,26 @@ get_job_options(typval_T *tv, jobopt_T *opt, int supported)
opt->jo_term_finish = *val; opt->jo_term_finish = *val;
} }
#endif #endif
else if (STRCMP(hi->hi_key, "env") == 0)
{
if (!(supported & JO2_ENV))
break;
opt->jo_set |= JO2_ENV;
opt->jo_env = item->vval.v_dict;
++item->vval.v_dict->dv_refcount;
}
else if (STRCMP(hi->hi_key, "cwd") == 0)
{
if (!(supported & JO2_CWD))
break;
opt->jo_cwd = get_tv_string_buf_chk(item, opt->jo_cwd_buf);
if (opt->jo_cwd == NULL || !mch_isdir(opt->jo_cwd))
{
EMSG2(_(e_invarg2), "cwd");
return FAIL;
}
opt->jo_set |= JO2_CWD;
}
else if (STRCMP(hi->hi_key, "waittime") == 0) else if (STRCMP(hi->hi_key, "waittime") == 0)
{ {
if (!(supported & JO_WAITTIME)) if (!(supported & JO_WAITTIME))

View File

@@ -5320,6 +5320,22 @@ mch_job_start(char **argv, job_T *job, jobopt_T *options)
# endif # endif
set_default_child_environment(); set_default_child_environment();
if (options->jo_env != NULL)
{
dict_T *dict = options->jo_env;
hashitem_T *hi;
int todo = (int)dict->dv_hashtab.ht_used;
for (hi = dict->dv_hashtab.ht_array; todo > 0; ++hi)
if (!HASHITEM_EMPTY(hi))
{
typval_T *item = &dict_lookup(hi)->di_tv;
vim_setenv((char_u*)hi->hi_key, get_tv_string(item));
--todo;
}
}
if (use_null_for_in || use_null_for_out || use_null_for_err) if (use_null_for_in || use_null_for_out || use_null_for_err)
null_fd = open("/dev/null", O_RDWR | O_EXTRA, 0); null_fd = open("/dev/null", O_RDWR | O_EXTRA, 0);
@@ -5387,6 +5403,9 @@ mch_job_start(char **argv, job_T *job, jobopt_T *options)
if (null_fd >= 0) if (null_fd >= 0)
close(null_fd); close(null_fd);
if (options->jo_cwd != NULL && mch_chdir((char *)options->jo_cwd) != 0)
_exit(EXEC_FAILED);
/* See above for type of argv. */ /* See above for type of argv. */
execvp(argv[0], argv); execvp(argv[0], argv);

View File

@@ -3981,31 +3981,46 @@ vim_create_process(
BOOL inherit_handles, BOOL inherit_handles,
DWORD flags, DWORD flags,
STARTUPINFO *si, STARTUPINFO *si,
PROCESS_INFORMATION *pi) PROCESS_INFORMATION *pi,
LPVOID *env,
char *cwd)
{ {
#ifdef FEAT_MBYTE #ifdef FEAT_MBYTE
if (enc_codepage >= 0 && (int)GetACP() != enc_codepage) if (enc_codepage >= 0 && (int)GetACP() != enc_codepage)
{ {
WCHAR *wcmd = enc_to_utf16((char_u *)cmd, NULL); BOOL ret;
WCHAR *wcmd, *wcwd = NULL;
if (wcmd != NULL) wcmd = enc_to_utf16((char_u *)cmd, NULL);
if (wcmd == NULL)
goto fallback;
if (cwd != NULL)
{ {
BOOL ret; wcwd = enc_to_utf16((char_u *)cwd, NULL);
ret = CreateProcessW( if (wcwd == NULL)
NULL, /* Executable name */ {
wcmd, /* Command to execute */ vim_free(wcmd);
NULL, /* Process security attributes */ goto fallback;
NULL, /* Thread security attributes */ }
inherit_handles, /* Inherit handles */
flags, /* Creation flags */
NULL, /* Environment */
NULL, /* Current directory */
(LPSTARTUPINFOW)si, /* Startup information */
pi); /* Process information */
vim_free(wcmd);
return ret;
} }
ret = CreateProcessW(
NULL, /* Executable name */
wcmd, /* Command to execute */
NULL, /* Process security attributes */
NULL, /* Thread security attributes */
inherit_handles, /* Inherit handles */
flags, /* Creation flags */
env, /* Environment */
wcwd, /* Current directory */
(LPSTARTUPINFOW)si, /* Startup information */
pi); /* Process information */
vim_free(wcmd);
if (wcwd != NULL)
vim_free(wcwd);
return ret;
} }
fallback:
#endif #endif
return CreateProcess( return CreateProcess(
NULL, /* Executable name */ NULL, /* Executable name */
@@ -4014,8 +4029,8 @@ vim_create_process(
NULL, /* Thread security attributes */ NULL, /* Thread security attributes */
inherit_handles, /* Inherit handles */ inherit_handles, /* Inherit handles */
flags, /* Creation flags */ flags, /* Creation flags */
NULL, /* Environment */ env, /* Environment */
NULL, /* Current directory */ cwd, /* Current directory */
si, /* Startup information */ si, /* Startup information */
pi); /* Process information */ pi); /* Process information */
} }
@@ -4079,7 +4094,8 @@ mch_system_classic(char *cmd, int options)
/* Now, run the command */ /* Now, run the command */
vim_create_process(cmd, FALSE, vim_create_process(cmd, FALSE,
CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE, &si, &pi); CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_CONSOLE,
&si, &pi, NULL, NULL);
/* Wait for the command to terminate before continuing */ /* Wait for the command to terminate before continuing */
{ {
@@ -4398,7 +4414,8 @@ mch_system_piped(char *cmd, int options)
* About "Inherit handles" being TRUE: this command can be litigious, * About "Inherit handles" being TRUE: this command can be litigious,
* handle inheritance was deactivated for pending temp file, but, if we * handle inheritance was deactivated for pending temp file, but, if we
* deactivate it, the pipes don't work for some reason. */ * deactivate it, the pipes don't work for some reason. */
vim_create_process(p, TRUE, CREATE_DEFAULT_ERROR_MODE, &si, &pi); vim_create_process(p, TRUE, CREATE_DEFAULT_ERROR_MODE,
&si, &pi, NULL, NULL);
if (p != cmd) if (p != cmd)
vim_free(p); vim_free(p);
@@ -4835,7 +4852,8 @@ mch_call_shell(
* inherit our handles which causes unpleasant dangling swap * inherit our handles which causes unpleasant dangling swap
* files if we exit before the spawned process * files if we exit before the spawned process
*/ */
if (vim_create_process((char *)newcmd, FALSE, flags, &si, &pi)) if (vim_create_process((char *)newcmd, FALSE, flags,
&si, &pi, NULL, NULL))
x = 0; x = 0;
else if (vim_shell_execute((char *)newcmd, n_show_cmd) else if (vim_shell_execute((char *)newcmd, n_show_cmd)
> (HINSTANCE)32) > (HINSTANCE)32)
@@ -4976,6 +4994,67 @@ job_io_file_open(
return h; return h;
} }
/*
* Turn the dictionary "env" into a NUL separated list that can be used as the
* environment argument of vim_create_process().
*/
static void
make_job_env(garray_T *gap, dict_T *env)
{
hashitem_T *hi;
int todo = (int)env->dv_hashtab.ht_used;
LPVOID base = GetEnvironmentStringsW();
/* for last \0 */
if (ga_grow(gap, 1) == FAIL)
return;
if (base)
{
WCHAR *p = (WCHAR*) base;
/* for last \0 */
if (ga_grow(gap, 1) == FAIL)
return;
while (*p != 0 || *(p + 1) != 0)
{
if (ga_grow(gap, 1) == OK)
*((WCHAR*)gap->ga_data + gap->ga_len++) = *p;
p++;
}
FreeEnvironmentStrings(base);
*((WCHAR*)gap->ga_data + gap->ga_len++) = L'\0';
}
for (hi = env->dv_hashtab.ht_array; todo > 0; ++hi)
{
if (!HASHITEM_EMPTY(hi))
{
typval_T *item = &dict_lookup(hi)->di_tv;
WCHAR *wkey = enc_to_utf16((char_u *)hi->hi_key, NULL);
WCHAR *wval = enc_to_utf16(get_tv_string(item), NULL);
--todo;
if (wkey != NULL && wval != NULL)
{
int n, lkey = wcslen(wkey), lval = wcslen(wval);
if (ga_grow(gap, lkey + lval + 2) != OK)
continue;
for (n = 0; n < lkey; n++)
*((WCHAR*)gap->ga_data + gap->ga_len++) = wkey[n];
*((WCHAR*)gap->ga_data + gap->ga_len++) = L'=';
for (n = 0; n < lval; n++)
*((WCHAR*)gap->ga_data + gap->ga_len++) = wval[n];
*((WCHAR*)gap->ga_data + gap->ga_len++) = L'\0';
}
if (wkey != NULL) vim_free(wkey);
if (wval != NULL) vim_free(wval);
}
}
*((WCHAR*)gap->ga_data + gap->ga_len++) = L'\0';
}
void void
mch_job_start(char *cmd, job_T *job, jobopt_T *options) mch_job_start(char *cmd, job_T *job, jobopt_T *options)
{ {
@@ -4987,6 +5066,7 @@ mch_job_start(char *cmd, job_T *job, jobopt_T *options)
HANDLE ifd[2]; HANDLE ifd[2];
HANDLE ofd[2]; HANDLE ofd[2];
HANDLE efd[2]; HANDLE efd[2];
garray_T ga;
int use_null_for_in = options->jo_io[PART_IN] == JIO_NULL; int use_null_for_in = options->jo_io[PART_IN] == JIO_NULL;
int use_null_for_out = options->jo_io[PART_OUT] == JIO_NULL; int use_null_for_out = options->jo_io[PART_OUT] == JIO_NULL;
@@ -5005,6 +5085,7 @@ mch_job_start(char *cmd, job_T *job, jobopt_T *options)
ofd[1] = INVALID_HANDLE_VALUE; ofd[1] = INVALID_HANDLE_VALUE;
efd[0] = INVALID_HANDLE_VALUE; efd[0] = INVALID_HANDLE_VALUE;
efd[1] = INVALID_HANDLE_VALUE; efd[1] = INVALID_HANDLE_VALUE;
ga_init2(&ga, (int)sizeof(wchar_t), 500);
jo = CreateJobObject(NULL, NULL); jo = CreateJobObject(NULL, NULL);
if (jo == NULL) if (jo == NULL)
@@ -5013,6 +5094,9 @@ mch_job_start(char *cmd, job_T *job, jobopt_T *options)
goto failed; goto failed;
} }
if (options->jo_env != NULL)
make_job_env(&ga, options->jo_env);
ZeroMemory(&pi, sizeof(pi)); ZeroMemory(&pi, sizeof(pi));
ZeroMemory(&si, sizeof(si)); ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si); si.cb = sizeof(si);
@@ -5100,14 +5184,19 @@ mch_job_start(char *cmd, job_T *job, jobopt_T *options)
CREATE_SUSPENDED | CREATE_SUSPENDED |
CREATE_DEFAULT_ERROR_MODE | CREATE_DEFAULT_ERROR_MODE |
CREATE_NEW_PROCESS_GROUP | CREATE_NEW_PROCESS_GROUP |
CREATE_UNICODE_ENVIRONMENT |
CREATE_NEW_CONSOLE, CREATE_NEW_CONSOLE,
&si, &pi)) &si, &pi,
ga.ga_data,
(char *)options->jo_cwd))
{ {
CloseHandle(jo); CloseHandle(jo);
job->jv_status = JOB_FAILED; job->jv_status = JOB_FAILED;
goto failed; goto failed;
} }
ga_clear(&ga);
if (!AssignProcessToJobObject(jo, pi.hProcess)) if (!AssignProcessToJobObject(jo, pi.hProcess))
{ {
/* if failing, switch the way to terminate /* if failing, switch the way to terminate
@@ -5148,6 +5237,7 @@ failed:
CloseHandle(ofd[1]); CloseHandle(ofd[1]);
CloseHandle(efd[1]); CloseHandle(efd[1]);
channel_unref(channel); channel_unref(channel);
ga_clear(&ga);
} }
char * char *

View File

@@ -1686,7 +1686,9 @@ struct channel_S {
#define JO2_ERR_MSG 0x0002 /* "err_msg" (JO_OUT_ << 1) */ #define JO2_ERR_MSG 0x0002 /* "err_msg" (JO_OUT_ << 1) */
#define JO2_TERM_NAME 0x0004 /* "term_name" */ #define JO2_TERM_NAME 0x0004 /* "term_name" */
#define JO2_TERM_FINISH 0x0008 /* "term_finish" */ #define JO2_TERM_FINISH 0x0008 /* "term_finish" */
#define JO2_ALL 0x000F #define JO2_ENV 0x0010 /* "env" */
#define JO2_CWD 0x0020 /* "cwd" */
#define JO2_ALL 0x003F
#define JO_MODE_ALL (JO_MODE + JO_IN_MODE + JO_OUT_MODE + JO_ERR_MODE) #define JO_MODE_ALL (JO_MODE + JO_IN_MODE + JO_OUT_MODE + JO_ERR_MODE)
#define JO_CB_ALL \ #define JO_CB_ALL \
@@ -1738,6 +1740,9 @@ typedef struct
int jo_id; int jo_id;
char_u jo_soe_buf[NUMBUFLEN]; char_u jo_soe_buf[NUMBUFLEN];
char_u *jo_stoponexit; char_u *jo_stoponexit;
dict_T *jo_env; /* environment variables */
char_u jo_cwd_buf[NUMBUFLEN];
char_u *jo_cwd;
#ifdef FEAT_TERMINAL #ifdef FEAT_TERMINAL
/* when non-zero run the job in a terminal window of this size */ /* when non-zero run the job in a terminal window of this size */

View File

@@ -2362,7 +2362,8 @@ f_term_start(typval_T *argvars, typval_T *rettv)
&& get_job_options(&argvars[1], &opt, && get_job_options(&argvars[1], &opt,
JO_TIMEOUT_ALL + JO_STOPONEXIT JO_TIMEOUT_ALL + JO_STOPONEXIT
+ JO_EXIT_CB + JO_CLOSE_CALLBACK + JO_EXIT_CB + JO_CLOSE_CALLBACK
+ JO2_TERM_NAME + JO2_TERM_FINISH) == FAIL) + JO2_TERM_NAME + JO2_TERM_FINISH
+ JO2_CWD + JO2_ENV) == FAIL)
return; return;
term_start(cmd, &opt); term_start(cmd, &opt);

View File

@@ -1664,6 +1664,45 @@ func Test_read_from_terminated_job()
call assert_equal(1, g:linecount) call assert_equal(1, g:linecount)
endfunc endfunc
func Test_env()
if !has('job')
return
endif
let s:envstr = ''
if has('win32')
call job_start(['cmd', '/c', 'echo %FOO%'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'env':{'FOO': 'bar'}})
else
call job_start([&shell, &shellcmdflag, 'echo $FOO'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'env':{'FOO': 'bar'}})
endif
call WaitFor('"" != s:envstr')
call assert_equal("bar", s:envstr)
unlet s:envstr
endfunc
func Test_cwd()
if !has('job')
return
endif
let s:envstr = ''
if has('win32')
let expect = $TEMP
call job_start(['cmd', '/c', 'echo %CD%'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'cwd': expect})
else
let expect = $HOME
call job_start(['pwd'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'cwd': expect})
endif
call WaitFor('"" != s:envstr')
let expect = substitute(expect, '[/\\]$', '', '')
let s:envstr = substitute(s:envstr, '[/\\]$', '', '')
if $CI != '' && stridx(s:envstr, '/private/') == 0
let s:envstr = s:envstr[8:]
endif
call assert_equal(expect, s:envstr)
unlet s:envstr
endfunc
function Ch_test_close_lambda(port) function Ch_test_close_lambda(port)
let handle = ch_open('localhost:' . a:port, s:chopt) let handle = ch_open('localhost:' . a:port, s:chopt)
if ch_status(handle) == "fail" if ch_status(handle) == "fail"

View File

@@ -8,8 +8,8 @@ source shared.vim
" Open a terminal with a shell, assign the job to g:job and return the buffer " Open a terminal with a shell, assign the job to g:job and return the buffer
" number. " number.
func Run_shell_in_terminal() func Run_shell_in_terminal(options)
let buf = term_start(&shell) let buf = term_start(&shell, a:options)
let termlist = term_list() let termlist = term_list()
call assert_equal(1, len(termlist)) call assert_equal(1, len(termlist))
@@ -32,7 +32,7 @@ func Stop_shell_in_terminal(buf)
endfunc endfunc
func Test_terminal_basic() func Test_terminal_basic()
let buf = Run_shell_in_terminal() let buf = Run_shell_in_terminal({})
if has("unix") if has("unix")
call assert_match("^/dev/", job_info(g:job).tty) call assert_match("^/dev/", job_info(g:job).tty)
call assert_match("^/dev/", term_gettty('')) call assert_match("^/dev/", term_gettty(''))
@@ -51,7 +51,7 @@ func Test_terminal_basic()
endfunc endfunc
func Test_terminal_make_change() func Test_terminal_make_change()
let buf = Run_shell_in_terminal() let buf = Run_shell_in_terminal({})
call Stop_shell_in_terminal(buf) call Stop_shell_in_terminal(buf)
call term_wait(buf) call term_wait(buf)
@@ -65,7 +65,7 @@ func Test_terminal_make_change()
endfunc endfunc
func Test_terminal_wipe_buffer() func Test_terminal_wipe_buffer()
let buf = Run_shell_in_terminal() let buf = Run_shell_in_terminal({})
call assert_fails(buf . 'bwipe', 'E517') call assert_fails(buf . 'bwipe', 'E517')
exe buf . 'bwipe!' exe buf . 'bwipe!'
call WaitFor('job_status(g:job) == "dead"') call WaitFor('job_status(g:job) == "dead"')
@@ -76,7 +76,7 @@ func Test_terminal_wipe_buffer()
endfunc endfunc
func Test_terminal_hide_buffer() func Test_terminal_hide_buffer()
let buf = Run_shell_in_terminal() let buf = Run_shell_in_terminal({})
quit quit
for nr in range(1, winnr('$')) for nr in range(1, winnr('$'))
call assert_notequal(winbufnr(nr), buf) call assert_notequal(winbufnr(nr), buf)
@@ -266,9 +266,11 @@ func Test_terminal_size()
endfunc endfunc
func Test_finish_close() func Test_finish_close()
return
" TODO: use something that takes much less than a whole second
echo 'This will take five seconds...'
call assert_equal(1, winnr('$')) call assert_equal(1, winnr('$'))
" TODO: use something that takes much less than a whole second
if has('win32') if has('win32')
let cmd = $windir . '\system32\timeout.exe 1' let cmd = $windir . '\system32\timeout.exe 1'
else else
@@ -304,3 +306,32 @@ func Test_finish_close()
bwipe bwipe
endfunc endfunc
func Test_terminal_cwd()
if !has('unix')
return
endif
call mkdir('Xdir')
let buf = term_start('pwd', {'cwd': 'Xdir'})
sleep 100m
call term_wait(buf)
call assert_equal(getcwd() . '/Xdir', getline(1))
exe buf . 'bwipe'
call delete('Xdir', 'rf')
endfunc
func Test_terminal_env()
if !has('unix')
return
endif
let buf = Run_shell_in_terminal({'env': {'TESTENV': 'correct'}})
call term_wait(buf)
call term_sendkeys(buf, "echo $TESTENV\r")
call term_wait(buf)
call Stop_shell_in_terminal(buf)
call term_wait(buf)
call assert_equal('correct', getline(2))
exe buf . 'bwipe'
endfunc

View File

@@ -769,6 +769,8 @@ static char *(features[]) =
static int included_patches[] = static int included_patches[] =
{ /* Add new patch number below this line */ { /* Add new patch number below this line */
/**/
902,
/**/ /**/
901, 901,
/**/ /**/