]> granicus.if.org Git - vim/commitdiff
patch 8.0.0902: cannot specify directory or environment for a job v8.0.0902
authorBram Moolenaar <Bram@vim.org>
Fri, 11 Aug 2017 17:12:11 +0000 (19:12 +0200)
committerBram Moolenaar <Bram@vim.org>
Fri, 11 Aug 2017 17:12:11 +0000 (19:12 +0200)
Problem:    Cannot specify directory or environment for a job.
Solution:   Add the "cwd" and "env" arguments to job options. (Yasuhiro
            Matsumoto, closes #1160)

runtime/doc/channel.txt
src/channel.c
src/os_unix.c
src/os_win32.c
src/structs.h
src/terminal.c
src/testdir/test_channel.vim
src/testdir/test_terminal.vim
src/version.c

index f85c15022e5933fc780e9c9c207732cbe12aa74d..b9af2e6d82ee43eed91d5fbe27bdc4c312d48056 100644 (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()|
 to check if there is something to read.
 
-Note that when there is no callback message are dropped.  To avoid that add a
-close callback to the channel.
+Note that when there is no callback, messages are dropped.  To avoid that add
+close callback to the channel.
 
 To read all output from a RAW channel that is available: >
        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
 |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
 handle that separately, add an "err_cb" handler: >
     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
 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
 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
                        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 ~
                                                        *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
 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
 "out_modifiable" or "err_modifiable" options is not zero, an error is given
 and the buffer will not be written to.
index 7eb6ce73e3bac970b3f0329959ed35209fe8d89e..3126cbd78f9a472310d6e7f9203d856fcbb57686 100644 (file)
@@ -4153,6 +4153,8 @@ free_job_options(jobopt_T *opt)
        partial_unref(opt->jo_exit_partial);
     else if (opt->jo_exit_cb != NULL)
        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;
            }
 #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)
            {
                if (!(supported & JO_WAITTIME))
index 141a3f0105f5c1846a35e06ca00e190f947fb36a..c56de66db2453115b6bf92241551bc4102106f89 100644 (file)
@@ -5320,6 +5320,22 @@ mch_job_start(char **argv, job_T *job, jobopt_T *options)
 # endif
            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)
            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)
            close(null_fd);
 
+       if (options->jo_cwd != NULL && mch_chdir((char *)options->jo_cwd) != 0)
+           _exit(EXEC_FAILED);
+
        /* See above for type of argv. */
        execvp(argv[0], argv);
 
index 9dc039b99284c158ecc4c4e48ef7786d4b3032b9..f98bd7e6ea3bafef8400a737184b960a4b7ab200 100644 (file)
@@ -3981,31 +3981,46 @@ vim_create_process(
     BOOL               inherit_handles,
     DWORD              flags,
     STARTUPINFO                *si,
-    PROCESS_INFORMATION *pi)
+    PROCESS_INFORMATION *pi,
+    LPVOID             *env,
+    char               *cwd)
 {
 #ifdef FEAT_MBYTE
     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;
-           ret = CreateProcessW(
-               NULL,                   /* Executable name */
-               wcmd,                   /* Command to execute */
-               NULL,                   /* Process security attributes */
-               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;
+           wcwd = enc_to_utf16((char_u *)cwd, NULL);
+           if (wcwd == NULL)
+           {
+               vim_free(wcmd);
+               goto fallback;
+           }
        }
-    }
+
+       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
     return CreateProcess(
        NULL,                   /* Executable name */
@@ -4014,8 +4029,8 @@ vim_create_process(
        NULL,                   /* Thread security attributes */
        inherit_handles,        /* Inherit handles */
        flags,                  /* Creation flags */
-       NULL,                   /* Environment */
-       NULL,                   /* Current directory */
+       env,                    /* Environment */
+       cwd,                    /* Current directory */
        si,                     /* Startup information */
        pi);                    /* Process information */
 }
@@ -4079,7 +4094,8 @@ mch_system_classic(char *cmd, int options)
 
     /* Now, run the command */
     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 */
     {
@@ -4398,7 +4414,8 @@ mch_system_piped(char *cmd, int options)
      * About "Inherit handles" being TRUE: this command can be litigious,
      * handle inheritance was deactivated for pending temp file, but, if we
      * 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)
        vim_free(p);
@@ -4835,7 +4852,8 @@ mch_call_shell(
             * inherit our handles which causes unpleasant dangling swap
             * 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;
            else if (vim_shell_execute((char *)newcmd, n_show_cmd)
                                                               > (HINSTANCE)32)
@@ -4976,6 +4994,67 @@ job_io_file_open(
     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
 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             ofd[2];
     HANDLE             efd[2];
+    garray_T           ga;
 
     int                use_null_for_in = options->jo_io[PART_IN] == 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;
     efd[0] = INVALID_HANDLE_VALUE;
     efd[1] = INVALID_HANDLE_VALUE;
+    ga_init2(&ga, (int)sizeof(wchar_t), 500);
 
     jo = CreateJobObject(NULL, NULL);
     if (jo == NULL)
@@ -5013,6 +5094,9 @@ mch_job_start(char *cmd, job_T *job, jobopt_T *options)
        goto failed;
     }
 
+    if (options->jo_env != NULL)
+       make_job_env(&ga, options->jo_env);
+
     ZeroMemory(&pi, sizeof(pi));
     ZeroMemory(&si, sizeof(si));
     si.cb = sizeof(si);
@@ -5100,14 +5184,19 @@ mch_job_start(char *cmd, job_T *job, jobopt_T *options)
            CREATE_SUSPENDED |
            CREATE_DEFAULT_ERROR_MODE |
            CREATE_NEW_PROCESS_GROUP |
+           CREATE_UNICODE_ENVIRONMENT |
            CREATE_NEW_CONSOLE,
-           &si, &pi))
+           &si, &pi,
+           ga.ga_data,
+           (char *)options->jo_cwd))
     {
        CloseHandle(jo);
        job->jv_status = JOB_FAILED;
        goto failed;
     }
 
+    ga_clear(&ga);
+
     if (!AssignProcessToJobObject(jo, pi.hProcess))
     {
        /* if failing, switch the way to terminate
@@ -5148,6 +5237,7 @@ failed:
     CloseHandle(ofd[1]);
     CloseHandle(efd[1]);
     channel_unref(channel);
+    ga_clear(&ga);
 }
 
     char *
index 618cabfe755916ae209811f28ebaf61d9234dc8c..16f2cc1d0731ba356993ec2a4c8ebd04ce204055 100644 (file)
@@ -1686,7 +1686,9 @@ struct channel_S {
 #define JO2_ERR_MSG        0x0002      /* "err_msg" (JO_OUT_ << 1) */
 #define JO2_TERM_NAME      0x0004      /* "term_name" */
 #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_CB_ALL \
@@ -1738,6 +1740,9 @@ typedef struct
     int                jo_id;
     char_u     jo_soe_buf[NUMBUFLEN];
     char_u     *jo_stoponexit;
+    dict_T     *jo_env;        /* environment variables */
+    char_u     jo_cwd_buf[NUMBUFLEN];
+    char_u     *jo_cwd;
 
 #ifdef FEAT_TERMINAL
     /* when non-zero run the job in a terminal window of this size */
index b22cc1a39acab3c6853ef586151ef626dbe9baf2..c215b3fa8ca2c59d6d525ed01c6e9f304d05249c 100644 (file)
@@ -2362,7 +2362,8 @@ f_term_start(typval_T *argvars, typval_T *rettv)
            && get_job_options(&argvars[1], &opt,
                JO_TIMEOUT_ALL + JO_STOPONEXIT
                + 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;
 
     term_start(cmd, &opt);
index c988968bb2a43b6f13c0af8d3793878af212eb36..42f0810ee354e06208b0d7d7b7089caf7ec16212 100644 (file)
@@ -1664,6 +1664,45 @@ func Test_read_from_terminated_job()
   call assert_equal(1, g:linecount)
 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)
   let handle = ch_open('localhost:' . a:port, s:chopt)
   if ch_status(handle) == "fail"
index 89a784b340c49213e7817986965f9f6e59949c5e..9cc3a55cfd166e2e9f58ba2329f0700d7a495e57 100644 (file)
@@ -8,8 +8,8 @@ source shared.vim
 
 " Open a terminal with a shell, assign the job to g:job and return the buffer
 " number.
-func Run_shell_in_terminal()
-  let buf = term_start(&shell)
+func Run_shell_in_terminal(options)
+  let buf = term_start(&shell, a:options)
 
   let termlist = term_list()
   call assert_equal(1, len(termlist))
@@ -32,7 +32,7 @@ func Stop_shell_in_terminal(buf)
 endfunc
 
 func Test_terminal_basic()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   if has("unix")
     call assert_match("^/dev/", job_info(g:job).tty)
     call assert_match("^/dev/", term_gettty(''))
@@ -51,7 +51,7 @@ func Test_terminal_basic()
 endfunc
 
 func Test_terminal_make_change()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   call Stop_shell_in_terminal(buf)
   call term_wait(buf)
 
@@ -65,7 +65,7 @@ func Test_terminal_make_change()
 endfunc
 
 func Test_terminal_wipe_buffer()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   call assert_fails(buf . 'bwipe', 'E517')
   exe buf . 'bwipe!'
   call WaitFor('job_status(g:job) == "dead"')
@@ -76,7 +76,7 @@ func Test_terminal_wipe_buffer()
 endfunc
 
 func Test_terminal_hide_buffer()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   quit
   for nr in range(1, winnr('$'))
     call assert_notequal(winbufnr(nr), buf)
@@ -266,9 +266,11 @@ func Test_terminal_size()
 endfunc
 
 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('$'))
 
-  " TODO: use something that takes much less than a whole second
   if has('win32')
     let cmd = $windir . '\system32\timeout.exe 1'
   else
@@ -304,3 +306,32 @@ func Test_finish_close()
 
   bwipe
 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
index 469e01a5ce186fde450eba95719ea1049a6d16b9..ce00ff2632b2e2b88a72a79beda4f50e1922c1e0 100644 (file)
@@ -769,6 +769,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    902,
 /**/
     901,
 /**/