1 /**
2  * Spawn detached process.
3  * Authors: 
4  *  $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov)
5  *  
6  * 
7  *  Some parts are merely copied from $(LINK2 https://github.com/dlang/phobos/blob/master/std/process.d, std.process)
8  * Copyright:
9  *  Roman Chistokhodov, 2016
10  * License: 
11  *  $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
12  */
13 
14 module detached;
15 
16 version(Posix) private {
17     import core.sys.posix.unistd;
18     import core.sys.posix.fcntl;
19     import core.stdc.errno;
20     import std.typecons : tuple, Tuple;
21     import std.process : environ;
22 }
23 
24 version(Windows) private {
25     import core.sys.windows.windows;
26     import std.process : environment, escapeWindowsArgument;
27 }
28 
29 import findexecutable;
30 static import std.stdio;
31 
32 public import std.process : ProcessException, Config;
33 public import std.stdio : File, StdioException;
34 
35 version(Posix) private @nogc @trusted char* mallocToStringz(in char[] s) nothrow
36 {
37     import core.stdc.string : strncpy;
38     import core.stdc.stdlib : malloc;
39     auto sz = cast(char*)malloc(s.length + 1);
40     if (s !is null) {
41         strncpy(sz, s.ptr, s.length);
42     }
43     sz[s.length] = '\0';
44     return sz;
45 }
46 
47 version(Posix) unittest
48 {
49     import core.stdc.stdlib : free;
50     import core.stdc.string : strcmp;
51     auto s = mallocToStringz("string");
52     assert(strcmp(s, "string") == 0);
53     free(s);
54     
55     assert(strcmp(mallocToStringz(null), "") == 0);
56 }
57 
58 version(Posix) private @nogc @trusted char** createExecArgv(in char[][] args, in char[] filePath) nothrow {
59     import core.stdc.stdlib : malloc;
60     auto argv = cast(char**)malloc((args.length+1)*(char*).sizeof);
61     argv[0] = mallocToStringz(filePath);
62     foreach(i; 1..args.length) {
63         argv[i] = mallocToStringz(args[i]);
64     }
65     argv[args.length] = null;
66     return argv;
67 }
68 
69 version(Posix) unittest
70 {
71     import core.stdc.string : strcmp;
72     auto argv= createExecArgv(["program", "arg", "arg2"], "/absolute/path/program");
73     assert(strcmp(argv[0], "/absolute/path/program") == 0);
74     assert(strcmp(argv[1], "arg") == 0);
75     assert(strcmp(argv[2], "arg2") == 0);
76     assert(argv[3] is null);
77 }
78 
79 version(Windows) private string escapeShellArguments(in char[][] args...) @trusted pure nothrow
80 {
81     import std.exception : assumeUnique;
82     char[] buf;
83 
84     @safe nothrow
85     char[] allocator(size_t size)
86     {
87         if (buf.length == 0)
88             return buf = new char[size];
89         else
90         {
91             auto p = buf.length;
92             buf.length = buf.length + 1 + size;
93             buf[p++] = ' ';
94             return buf[p..p+size];
95         }
96     }
97 
98     foreach (arg; args)
99         escapeWindowsArgumentImpl!allocator(arg);
100     return assumeUnique(buf);
101 }
102 
103 version(Windows) private char[] escapeWindowsArgumentImpl(alias allocator)(in char[] arg)
104     @safe nothrow
105     if (is(typeof(allocator(size_t.init)[0] = char.init)))
106 {
107     // References:
108     // * http://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
109     // * http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx
110 
111     // Check if the string needs to be escaped,
112     // and calculate the total string size.
113 
114     // Trailing backslashes must be escaped
115     bool escaping = true;
116     bool needEscape = false;
117     // Result size = input size + 2 for surrounding quotes + 1 for the
118     // backslash for each escaped character.
119     size_t size = 1 + arg.length + 1;
120 
121     foreach_reverse (char c; arg)
122     {
123         if (c == '"')
124         {
125             needEscape = true;
126             escaping = true;
127             size++;
128         }
129         else
130         if (c == '\\')
131         {
132             if (escaping)
133                 size++;
134         }
135         else
136         {
137             if (c == ' ' || c == '\t')
138                 needEscape = true;
139             escaping = false;
140         }
141     }
142 
143     import std.ascii : isDigit;
144     // Empty arguments need to be specified as ""
145     if (!arg.length)
146         needEscape = true;
147     else
148     // Arguments ending with digits need to be escaped,
149     // to disambiguate with 1>file redirection syntax
150     if (isDigit(arg[$-1]))
151         needEscape = true;
152 
153     if (!needEscape)
154         return allocator(arg.length)[] = arg;
155 
156     // Construct result string.
157 
158     auto buf = allocator(size);
159     size_t p = size;
160     buf[--p] = '"';
161     escaping = true;
162     foreach_reverse (char c; arg)
163     {
164         if (c == '"')
165             escaping = true;
166         else
167         if (c != '\\')
168             escaping = false;
169 
170         buf[--p] = c;
171         if (escaping)
172             buf[--p] = '\\';
173     }
174     buf[--p] = '"';
175     assert(p == 0);
176 
177     return buf;
178 }
179 
180 //from std.process
181 version(Posix) private void setCLOEXEC(int fd, bool on) nothrow @nogc
182 {
183     import core.sys.posix.fcntl : fcntl, F_GETFD, FD_CLOEXEC, F_SETFD;
184     auto flags = fcntl(fd, F_GETFD);
185     if (flags >= 0)
186     {
187         if (on) flags |= FD_CLOEXEC;
188         else    flags &= ~(cast(typeof(flags)) FD_CLOEXEC);
189         flags = fcntl(fd, F_SETFD, flags);
190     }
191     assert (flags != -1 || .errno == EBADF);
192 }
193 
194 //From std.process
195 version(Posix) private const(char*)* createEnv(const string[string] childEnv, bool mergeWithParentEnv)
196 {
197     // Determine the number of strings in the parent's environment.
198     int parentEnvLength = 0;
199     if (mergeWithParentEnv)
200     {
201         if (childEnv.length == 0) return environ;
202         while (environ[parentEnvLength] != null) ++parentEnvLength;
203     }
204 
205     // Convert the "new" variables to C-style strings.
206     auto envz = new const(char)*[parentEnvLength + childEnv.length + 1];
207     int pos = 0;
208     foreach (var, val; childEnv)
209         envz[pos++] = (var~'='~val~'\0').ptr;
210 
211     // Add the parent's environment.
212     foreach (environStr; environ[0 .. parentEnvLength])
213     {
214         int eqPos = 0;
215         while (environStr[eqPos] != '=' && environStr[eqPos] != '\0') ++eqPos;
216         if (environStr[eqPos] != '=') continue;
217         auto var = environStr[0 .. eqPos];
218         if (var in childEnv) continue;
219         envz[pos++] = environStr;
220     }
221     envz[pos] = null;
222     return envz.ptr;
223 }
224 
225 //From std.process
226 version(Posix) @system unittest
227 {
228     auto e1 = createEnv(null, false);
229     assert (e1 != null && *e1 == null);
230 
231     auto e2 = createEnv(null, true);
232     assert (e2 != null);
233     int i = 0;
234     for (; environ[i] != null; ++i)
235     {
236         assert (e2[i] != null);
237         import core.stdc.string;
238         assert (strcmp(e2[i], environ[i]) == 0);
239     }
240     assert (e2[i] == null);
241 
242     auto e3 = createEnv(["foo" : "bar", "hello" : "world"], false);
243     assert (e3 != null && e3[0] != null && e3[1] != null && e3[2] == null);
244     assert ((e3[0][0 .. 8] == "foo=bar\0" && e3[1][0 .. 12] == "hello=world\0")
245          || (e3[0][0 .. 12] == "hello=world\0" && e3[1][0 .. 8] == "foo=bar\0"));
246 }
247 
248 version (Windows) private LPVOID createEnv(const string[string] childEnv, bool mergeWithParentEnv)
249 {
250     if (mergeWithParentEnv && childEnv.length == 0) return null;
251     import std.array : appender;
252     import std.uni : toUpper;
253     auto envz = appender!(wchar[])();
254     void put(string var, string val)
255     {
256         envz.put(var);
257         envz.put('=');
258         envz.put(val);
259         envz.put(cast(wchar) '\0');
260     }
261 
262     // Add the variables in childEnv, removing them from parentEnv
263     // if they exist there too.
264     auto parentEnv = mergeWithParentEnv ? environment.toAA() : null;
265     foreach (k, v; childEnv)
266     {
267         auto uk = toUpper(k);
268         put(uk, v);
269         if (uk in parentEnv) parentEnv.remove(uk);
270     }
271 
272     // Add remaining parent environment variables.
273     foreach (k, v; parentEnv) put(k, v);
274 
275     // Two final zeros are needed in case there aren't any environment vars,
276     // and the last one does no harm when there are.
277     envz.put("\0\0"w);
278     return envz.data.ptr;
279 }
280 
281 version (Windows) @system unittest
282 {
283     assert (createEnv(null, true) == null);
284     assert ((cast(wchar*) createEnv(null, false))[0 .. 2] == "\0\0"w);
285     auto e1 = (cast(wchar*) createEnv(["foo":"bar", "ab":"c"], false))[0 .. 14];
286     assert (e1 == "FOO=bar\0AB=c\0\0"w || e1 == "AB=c\0FOO=bar\0\0"w);
287 }
288 
289 private enum InternalError : ubyte
290 {
291     noerror,
292     doubleFork,
293     exec,
294     chdir,
295     getrlimit,
296     environment
297 }
298 
299 version(Posix) private Tuple!(int, string) spawnProcessDetachedImpl(in char[][] args, 
300                                                      ref File stdin, ref File stdout, ref File stderr, 
301                                                      const string[string] env, 
302                                                      Config config, 
303                                                      in char[] workingDirectory, 
304                                                      ulong* pid) nothrow
305 {
306     import std.path : baseName;
307     import std.string : toStringz;
308     
309     string filePath = args[0].idup;
310     if (filePath.baseName == filePath) {
311         auto candidate = findExecutable(filePath);
312         if (!candidate.length) {
313             return tuple(ENOENT, "Could not find executable: " ~ filePath);
314         }
315         filePath = candidate;
316     }
317     
318     if (access(toStringz(filePath), X_OK) != 0) {
319         return tuple(.errno, "Not an executable file: " ~ filePath);
320     }
321     
322     static @trusted @nogc int safePipe(ref int[2] pipefds) nothrow
323     {
324         int result = pipe(pipefds);
325         if (result != 0) {
326             return result;
327         }
328         if (fcntl(pipefds[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefds[1], F_SETFD, FD_CLOEXEC) == -1) {
329             close(pipefds[0]);
330             close(pipefds[1]);
331             return -1;
332         }
333         return result;
334     }
335     
336     int[2] execPipe, pidPipe;
337     if (safePipe(execPipe) != 0) {
338         return tuple(.errno, "Could not create pipe to check startup of child");
339     }
340     scope(exit) close(execPipe[0]);
341     if (safePipe(pidPipe) != 0) {
342         auto pipeError = .errno;
343         close(execPipe[1]);
344         return tuple(pipeError, "Could not create pipe to get pid of child");
345     }
346     scope(exit) close(pidPipe[0]);
347     
348     int getFD(ref File f) { 
349         import core.stdc.stdio : fileno;
350         return fileno(f.getFP()); 
351     }
352     
353     int stdinFD, stdoutFD, stderrFD;
354     try {
355         stdinFD  = getFD(stdin);
356         stdoutFD = getFD(stdout);
357         stderrFD = getFD(stderr);
358     } catch(Exception e) {
359         return tuple(.errno ? .errno : EBADF, "Could not get file descriptors of standard streams");
360     }
361     
362     static void abortOnError(int execPipeOut, InternalError errorType, int error) nothrow {
363         error = error ? error : EINVAL;
364         write(execPipeOut, &errorType, errorType.sizeof);
365         write(execPipeOut, &error, error.sizeof);
366         close(execPipeOut);
367         _exit(1);
368     }
369     
370     pid_t firstFork = fork();
371     int lastError = .errno;
372     if (firstFork == 0) {
373         setsid();
374         close(execPipe[0]);
375         close(pidPipe[0]);
376         
377         int execPipeOut = execPipe[1];
378         int pidPipeOut = pidPipe[1];
379         
380         pid_t secondFork = fork();
381         if (secondFork == 0) {
382             close(pidPipeOut);
383         
384             if (workingDirectory.length) {
385                 import core.stdc.stdlib : free;
386                 auto workDir = mallocToStringz(workingDirectory);
387                 if (chdir(workDir) == -1) {
388                     free(workDir);
389                     abortOnError(execPipeOut, InternalError.chdir, .errno);
390                 } else {
391                     free(workDir);
392                 }
393             }
394             
395             // ===== From std.process =====
396             if (stderrFD == STDOUT_FILENO) {
397                 stderrFD = dup(stderrFD);
398             }
399             dup2(stdinFD,  STDIN_FILENO);
400             dup2(stdoutFD, STDOUT_FILENO);
401             dup2(stderrFD, STDERR_FILENO);
402 
403             setCLOEXEC(STDIN_FILENO, false);
404             setCLOEXEC(STDOUT_FILENO, false);
405             setCLOEXEC(STDERR_FILENO, false);
406             
407             if (!(config & Config.inheritFDs)) {
408                 import core.sys.posix.poll : pollfd, poll, POLLNVAL;
409                 import core.sys.posix.sys.resource : rlimit, getrlimit, RLIMIT_NOFILE;
410 
411                 rlimit r;
412                 if (getrlimit(RLIMIT_NOFILE, &r) != 0) {
413                     abortOnError(execPipeOut, InternalError.getrlimit, .errno);
414                 }
415                 immutable maxDescriptors = cast(int)r.rlim_cur;
416                 immutable maxToClose = maxDescriptors - 3;
417 
418                 @nogc nothrow static bool pollClose(int maxToClose, int dontClose)
419                 {
420                     import core.stdc.stdlib : malloc, free;
421 
422                     pollfd* pfds = cast(pollfd*)malloc(pollfd.sizeof * maxToClose);
423                     scope(exit) free(pfds);
424                     foreach (i; 0 .. maxToClose) {
425                         pfds[i].fd = i + 3;
426                         pfds[i].events = 0;
427                         pfds[i].revents = 0;
428                     }
429                     if (poll(pfds, maxToClose, 0) >= 0) {
430                         foreach (i; 0 .. maxToClose) {
431                             if (pfds[i].fd != dontClose && !(pfds[i].revents & POLLNVAL)) {
432                                 close(pfds[i].fd);
433                             }
434                         }
435                         return true;
436                     }
437                     else {
438                         return false;
439                     }
440                 }
441 
442                 if (!pollClose(maxToClose, execPipeOut)) {
443                     foreach (i; 3 .. maxDescriptors) {
444                         if (i != execPipeOut) {
445                             close(i);
446                         }
447                     }
448                 }
449             } else {
450                 if (stdinFD  > STDERR_FILENO)  close(stdinFD);
451                 if (stdoutFD > STDERR_FILENO)  close(stdoutFD);
452                 if (stderrFD > STDERR_FILENO)  close(stderrFD);
453             }
454             // =====================
455             
456             const(char*)* envz;
457             try {
458                 envz = createEnv(env, !(config & Config.newEnv));
459             } catch(Exception e) {
460                 abortOnError(execPipeOut, InternalError.environment, EINVAL);
461             }
462             auto argv = createExecArgv(args, filePath);
463             execve(argv[0], argv, envz);
464             abortOnError(execPipeOut, InternalError.exec, .errno);
465         }
466         int forkErrno = .errno;
467         
468         write(pidPipeOut, &secondFork, pid_t.sizeof);
469         close(pidPipeOut);
470         
471         if (secondFork == -1) {
472             abortOnError(execPipeOut, InternalError.doubleFork, forkErrno);
473         } else {
474             close(execPipeOut);
475             _exit(0);
476         }
477     }
478     
479     close(execPipe[1]);
480     close(pidPipe[1]);
481     
482     if (firstFork == -1) {
483         return tuple(lastError, "Could not fork");
484     }
485     
486     InternalError status;
487     auto readExecResult = read(execPipe[0], &status, status.sizeof);
488     lastError = .errno;
489     
490     import core.sys.posix.sys.wait : waitpid;
491     int waitResult;
492     waitpid(firstFork, &waitResult, 0);
493     
494     if (readExecResult == -1) {
495         return tuple(lastError, "Could not read from pipe to get child status");
496     }
497     
498     try {
499         if (!(config & Config.retainStdin ) && stdinFD  > STDERR_FILENO
500                                         && stdinFD  != getFD(std.stdio.stdin ))
501         stdin.close();
502         if (!(config & Config.retainStdout) && stdoutFD > STDERR_FILENO
503                                             && stdoutFD != getFD(std.stdio.stdout))
504             stdout.close();
505         if (!(config & Config.retainStderr) && stderrFD > STDERR_FILENO
506                                             && stderrFD != getFD(std.stdio.stderr))
507             stderr.close();
508     } catch(Exception e) {
509         
510     }
511     
512     if (status == 0) {
513         if (pid !is null) {
514             pid_t actualPid = 0;
515             if (read(pidPipe[0], &actualPid, pid_t.sizeof) >= 0) {
516                 *pid = actualPid;
517             } else {
518                 *pid = 0;
519             }
520         }
521         return tuple(0, "");
522     } else {
523         int error;
524         readExecResult = read(execPipe[0], &error, error.sizeof);
525         if (readExecResult == -1) {
526             return tuple(.errno, "Error occured but could not read exec errno from pipe");
527         }
528         switch(status) {
529             case InternalError.doubleFork: return tuple(error, "Could not fork twice");
530             case InternalError.exec: return tuple(error, "Could not exec");
531             case InternalError.chdir: return tuple(error, "Could not set working directory");
532             case InternalError.getrlimit: return tuple(error, "getrlimit");
533             case InternalError.environment: return tuple(error, "Could not set environment variables");
534             default:return tuple(error, "Unknown error occured");
535         }
536     }
537 }
538 
539 version(Windows) private void spawnProcessDetachedImpl(in char[] commandLine, 
540                                                      ref File stdin, ref File stdout, ref File stderr, 
541                                                      const string[string] env, 
542                                                      Config config, 
543                                                      in char[] workingDirectory, 
544                                                      ulong* pid)
545 {
546     import std.windows.syserror;
547     
548     // from std.process
549     // Prepare environment.
550     auto envz = createEnv(env, !(config & Config.newEnv));
551 
552     // Startup info for CreateProcessW().
553     STARTUPINFO_W startinfo;
554     startinfo.cb = startinfo.sizeof;
555     static int getFD(ref File f) { return f.isOpen ? f.fileno : -1; }
556 
557     // Extract file descriptors and HANDLEs from the streams and make the
558     // handles inheritable.
559     static void prepareStream(ref File file, DWORD stdHandle, string which,
560                               out int fileDescriptor, out HANDLE handle)
561     {
562         fileDescriptor = getFD(file);
563         handle = null;
564         if (fileDescriptor >= 0)
565             handle = file.windowsHandle;
566         // Windows GUI applications have a fd but not a valid Windows HANDLE.
567         if (handle is null || handle == INVALID_HANDLE_VALUE)
568             handle = GetStdHandle(stdHandle);
569 
570         DWORD dwFlags;
571         if (GetHandleInformation(handle, &dwFlags))
572         {
573             if (!(dwFlags & HANDLE_FLAG_INHERIT))
574             {
575                 if (!SetHandleInformation(handle,
576                                           HANDLE_FLAG_INHERIT,
577                                           HANDLE_FLAG_INHERIT))
578                 {
579                     throw new StdioException(
580                         "Failed to make "~which~" stream inheritable by child process ("
581                         ~sysErrorString(GetLastError()) ~ ')',
582                         0);
583                 }
584             }
585         }
586     }
587     int stdinFD = -1, stdoutFD = -1, stderrFD = -1;
588     prepareStream(stdin,  STD_INPUT_HANDLE,  "stdin" , stdinFD,  startinfo.hStdInput );
589     prepareStream(stdout, STD_OUTPUT_HANDLE, "stdout", stdoutFD, startinfo.hStdOutput);
590     prepareStream(stderr, STD_ERROR_HANDLE,  "stderr", stderrFD, startinfo.hStdError );
591 
592     if ((startinfo.hStdInput  != null && startinfo.hStdInput  != INVALID_HANDLE_VALUE)
593      || (startinfo.hStdOutput != null && startinfo.hStdOutput != INVALID_HANDLE_VALUE)
594      || (startinfo.hStdError  != null && startinfo.hStdError  != INVALID_HANDLE_VALUE))
595         startinfo.dwFlags = STARTF_USESTDHANDLES;
596 
597     // Create process.
598     PROCESS_INFORMATION pi;
599     DWORD dwCreationFlags =
600         CREATE_UNICODE_ENVIRONMENT |
601         ((config & Config.suppressConsole) ? CREATE_NO_WINDOW : 0);
602         
603         
604     import std.utf : toUTF16z, toUTF16;
605     auto pworkDir = workingDirectory.toUTF16z();
606     if (!CreateProcessW(null, (commandLine ~ "\0").toUTF16.dup.ptr, null, null, true, dwCreationFlags,
607                         envz, workingDirectory.length ? pworkDir : null, &startinfo, &pi))
608         throw ProcessException.newFromLastError("Failed to spawn new process");
609 
610     enum STDERR_FILENO = 2;
611     // figure out if we should close any of the streams
612     if (!(config & Config.retainStdin ) && stdinFD  > STDERR_FILENO
613                                         && stdinFD  != getFD(std.stdio.stdin ))
614         stdin.close();
615     if (!(config & Config.retainStdout) && stdoutFD > STDERR_FILENO
616                                         && stdoutFD != getFD(std.stdio.stdout))
617         stdout.close();
618     if (!(config & Config.retainStderr) && stderrFD > STDERR_FILENO
619                                         && stderrFD != getFD(std.stdio.stderr))
620         stderr.close();
621 
622     CloseHandle(pi.hThread);
623     CloseHandle(pi.hProcess);
624     if (pid) {
625         *pid = pi.dwProcessId;
626     }
627 }
628 
629 /**
630  * Spawns a new process, optionally assigning it an arbitrary set of standard input, output, and error streams.
631  * 
632  * The function returns immediately, leaving the spawned process to execute in parallel with its parent. 
633  * 
634  * The spawned process is detached from its parent, so you should not wait on the returned pid.
635  * 
636  * Params:
637  *  args = An array which contains the program name as the zeroth element and any command-line arguments in the following elements.
638  *  stdin = The standard input stream of the spawned process.
639  *  stdout = The standard output stream of the spawned process.
640  *  stderr = The standard error stream of the spawned process.
641  *  env = Additional environment variables for the child process.
642  *  config = Flags that control process creation. Same as for spawnProcess.
643  *  workingDirectory = The working directory for the new process.
644  *  pid = Pointer to variable that will get pid value in case spawnProcessDetached succeed. Not used if null.
645  * 
646  * See_Also: $(LINK2 https://dlang.org/phobos/std_process.html#.spawnProcess, spawnProcess documentation)
647  */
648 void spawnProcessDetached(in char[][] args, 
649                           File stdin = std.stdio.stdin, 
650                           File stdout = std.stdio.stdout, 
651                           File stderr = std.stdio.stderr, 
652                           const string[string] env = null, 
653                           Config config = Config.none, 
654                           in char[] workingDirectory = null, 
655                           ulong* pid = null)
656 {
657     import core.exception : RangeError;
658     
659     version(Posix) {
660         if (args.length == 0) throw new RangeError();
661         auto result = spawnProcessDetachedImpl(args, stdin, stdout, stderr, env, config, workingDirectory, pid);
662         if (result[0] != 0) {
663             .errno = result[0];
664             throw ProcessException.newFromErrno(result[1]);
665         }
666     } else version(Windows) {
667         auto commandLine = escapeShellArguments(args);
668         if (commandLine.length == 0) throw new RangeError("Command line is empty");
669         spawnProcessDetachedImpl(commandLine, stdin, stdout, stderr, env, config, workingDirectory, pid);
670     }
671 }
672 
673 ///
674 unittest
675 {
676     import std.exception : assertThrown;
677     version(Posix) {
678         try {
679             auto devNull = File("/dev/null", "rwb");
680             ulong pid;
681             spawnProcessDetached(["whoami"], devNull, devNull, devNull, null, Config.none, "./test", &pid);
682             assert(pid != 0);
683             
684             assertThrown(spawnProcessDetached(["./test/nonexistent"]));
685             assertThrown(spawnProcessDetached(["./test/executable.sh"], devNull, devNull, devNull, null, Config.none, "./test/nonexistent"));
686             assertThrown(spawnProcessDetached(["./dub.json"]));
687             assertThrown(spawnProcessDetached(["./test/notreallyexecutable"]));
688         } catch(Exception e) {
689             
690         }
691     }
692     version(Windows) {
693         try {
694             ulong pid;
695             spawnProcessDetached(["whoami"], std.stdio.stdin, std.stdio.stdout, std.stdio.stderr, null, Config.none, "./test", &pid);
696             
697             assertThrown(spawnProcessDetached(["dub.json"]));
698         } catch(Exception e) {
699             
700         }
701     }
702 }
703 
704 ///ditto
705 void spawnProcessDetached(in char[][] args, const string[string] env, Config config = Config.none, in char[] workingDirectory = null, ulong* pid = null)
706 {
707     spawnProcessDetached(args, std.stdio.stdin, std.stdio.stdout, std.stdio.stderr, env, config, workingDirectory, pid);
708 }