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         close(execPipe[0]);
374         close(pidPipe[0]);
375         
376         int execPipeOut = execPipe[1];
377         int pidPipeOut = pidPipe[1];
378         
379         pid_t secondFork = fork();
380         if (secondFork == 0) {
381             close(pidPipeOut);
382         
383             if (workingDirectory.length) {
384                 import core.stdc.stdlib : free;
385                 auto workDir = mallocToStringz(workingDirectory);
386                 if (chdir(workDir) == -1) {
387                     free(workDir);
388                     abortOnError(execPipeOut, InternalError.chdir, .errno);
389                 } else {
390                     free(workDir);
391                 }
392             }
393             
394             // ===== From std.process =====
395             if (stderrFD == STDOUT_FILENO) {
396                 stderrFD = dup(stderrFD);
397             }
398             dup2(stdinFD,  STDIN_FILENO);
399             dup2(stdoutFD, STDOUT_FILENO);
400             dup2(stderrFD, STDERR_FILENO);
401 
402             setCLOEXEC(STDIN_FILENO, false);
403             setCLOEXEC(STDOUT_FILENO, false);
404             setCLOEXEC(STDERR_FILENO, false);
405             
406             if (!(config & Config.inheritFDs)) {
407                 import core.sys.posix.poll : pollfd, poll, POLLNVAL;
408                 import core.sys.posix.sys.resource : rlimit, getrlimit, RLIMIT_NOFILE;
409 
410                 rlimit r;
411                 if (getrlimit(RLIMIT_NOFILE, &r) != 0) {
412                     abortOnError(execPipeOut, InternalError.getrlimit, .errno);
413                 }
414                 immutable maxDescriptors = cast(int)r.rlim_cur;
415                 immutable maxToClose = maxDescriptors - 3;
416 
417                 @nogc nothrow static bool pollClose(int maxToClose, int dontClose)
418                 {
419                     import core.stdc.stdlib : malloc, free;
420 
421                     pollfd* pfds = cast(pollfd*)malloc(pollfd.sizeof * maxToClose);
422                     scope(exit) free(pfds);
423                     foreach (i; 0 .. maxToClose) {
424                         pfds[i].fd = i + 3;
425                         pfds[i].events = 0;
426                         pfds[i].revents = 0;
427                     }
428                     if (poll(pfds, maxToClose, 0) >= 0) {
429                         foreach (i; 0 .. maxToClose) {
430                             if (pfds[i].fd != dontClose && !(pfds[i].revents & POLLNVAL)) {
431                                 close(pfds[i].fd);
432                             }
433                         }
434                         return true;
435                     }
436                     else {
437                         return false;
438                     }
439                 }
440 
441                 if (!pollClose(maxToClose, execPipeOut)) {
442                     foreach (i; 3 .. maxDescriptors) {
443                         if (i != execPipeOut) {
444                             close(i);
445                         }
446                     }
447                 }
448             } else {
449                 if (stdinFD  > STDERR_FILENO)  close(stdinFD);
450                 if (stdoutFD > STDERR_FILENO)  close(stdoutFD);
451                 if (stderrFD > STDERR_FILENO)  close(stderrFD);
452             }
453             // =====================
454             
455             const(char*)* envz;
456             try {
457                 envz = createEnv(env, !(config & Config.newEnv));
458             } catch(Exception e) {
459                 abortOnError(execPipeOut, InternalError.environment, EINVAL);
460             }
461             auto argv = createExecArgv(args, filePath);
462             execve(argv[0], argv, envz);
463             abortOnError(execPipeOut, InternalError.exec, .errno);
464         }
465         int forkErrno = .errno;
466         
467         write(pidPipeOut, &secondFork, pid_t.sizeof);
468         close(pidPipeOut);
469         
470         if (secondFork == -1) {
471             abortOnError(execPipeOut, InternalError.doubleFork, forkErrno);
472         } else {
473             close(execPipeOut);
474             _exit(0);
475         }
476     }
477     
478     close(execPipe[1]);
479     close(pidPipe[1]);
480     
481     if (firstFork == -1) {
482         return tuple(lastError, "Could not fork");
483     }
484     
485     InternalError status;
486     auto readExecResult = read(execPipe[0], &status, status.sizeof);
487     lastError = .errno;
488     
489     import core.sys.posix.sys.wait : waitpid;
490     int waitResult;
491     waitpid(firstFork, &waitResult, 0);
492     
493     if (readExecResult == -1) {
494         return tuple(lastError, "Could not read from pipe to get child status");
495     }
496     
497     try {
498         if (!(config & Config.retainStdin ) && stdinFD  > STDERR_FILENO
499                                         && stdinFD  != getFD(std.stdio.stdin ))
500         stdin.close();
501         if (!(config & Config.retainStdout) && stdoutFD > STDERR_FILENO
502                                             && stdoutFD != getFD(std.stdio.stdout))
503             stdout.close();
504         if (!(config & Config.retainStderr) && stderrFD > STDERR_FILENO
505                                             && stderrFD != getFD(std.stdio.stderr))
506             stderr.close();
507     } catch(Exception e) {
508         
509     }
510     
511     if (status == 0) {
512         if (pid !is null) {
513             pid_t actualPid = 0;
514             if (read(pidPipe[0], &actualPid, pid_t.sizeof) >= 0) {
515                 *pid = actualPid;
516             } else {
517                 *pid = 0;
518             }
519         }
520         return tuple(0, "");
521     } else {
522         int error;
523         readExecResult = read(execPipe[0], &error, error.sizeof);
524         if (readExecResult == -1) {
525             return tuple(.errno, "Error occured but could not read exec errno from pipe");
526         }
527         switch(status) {
528             case InternalError.doubleFork: return tuple(error, "Could not fork twice");
529             case InternalError.exec: return tuple(error, "Could not exec");
530             case InternalError.chdir: return tuple(error, "Could not set working directory");
531             case InternalError.getrlimit: return tuple(error, "getrlimit");
532             case InternalError.environment: return tuple(error, "Could not set environment variables");
533             default:return tuple(error, "Unknown error occured");
534         }
535     }
536 }
537 
538 version(Windows) private void spawnProcessDetachedImpl(in char[] commandLine, 
539                                                      ref File stdin, ref File stdout, ref File stderr, 
540                                                      const string[string] env, 
541                                                      Config config, 
542                                                      in char[] workingDirectory, 
543                                                      ulong* pid)
544 {
545     import std.windows.syserror;
546     
547     // from std.process
548     // Prepare environment.
549     auto envz = createEnv(env, !(config & Config.newEnv));
550 
551     // Startup info for CreateProcessW().
552     STARTUPINFO_W startinfo;
553     startinfo.cb = startinfo.sizeof;
554     static int getFD(ref File f) { return f.isOpen ? f.fileno : -1; }
555 
556     // Extract file descriptors and HANDLEs from the streams and make the
557     // handles inheritable.
558     static void prepareStream(ref File file, DWORD stdHandle, string which,
559                               out int fileDescriptor, out HANDLE handle)
560     {
561         fileDescriptor = getFD(file);
562         handle = null;
563         if (fileDescriptor >= 0)
564             handle = file.windowsHandle;
565         // Windows GUI applications have a fd but not a valid Windows HANDLE.
566         if (handle is null || handle == INVALID_HANDLE_VALUE)
567             handle = GetStdHandle(stdHandle);
568 
569         DWORD dwFlags;
570         if (GetHandleInformation(handle, &dwFlags))
571         {
572             if (!(dwFlags & HANDLE_FLAG_INHERIT))
573             {
574                 if (!SetHandleInformation(handle,
575                                           HANDLE_FLAG_INHERIT,
576                                           HANDLE_FLAG_INHERIT))
577                 {
578                     throw new StdioException(
579                         "Failed to make "~which~" stream inheritable by child process ("
580                         ~sysErrorString(GetLastError()) ~ ')',
581                         0);
582                 }
583             }
584         }
585     }
586     int stdinFD = -1, stdoutFD = -1, stderrFD = -1;
587     prepareStream(stdin,  STD_INPUT_HANDLE,  "stdin" , stdinFD,  startinfo.hStdInput );
588     prepareStream(stdout, STD_OUTPUT_HANDLE, "stdout", stdoutFD, startinfo.hStdOutput);
589     prepareStream(stderr, STD_ERROR_HANDLE,  "stderr", stderrFD, startinfo.hStdError );
590 
591     if ((startinfo.hStdInput  != null && startinfo.hStdInput  != INVALID_HANDLE_VALUE)
592      || (startinfo.hStdOutput != null && startinfo.hStdOutput != INVALID_HANDLE_VALUE)
593      || (startinfo.hStdError  != null && startinfo.hStdError  != INVALID_HANDLE_VALUE))
594         startinfo.dwFlags = STARTF_USESTDHANDLES;
595 
596     // Create process.
597     PROCESS_INFORMATION pi;
598     DWORD dwCreationFlags =
599         CREATE_UNICODE_ENVIRONMENT |
600         ((config & Config.suppressConsole) ? CREATE_NO_WINDOW : 0);
601         
602         
603     import std.utf : toUTF16z, toUTF16;
604     auto pworkDir = workingDirectory.toUTF16z();
605     if (!CreateProcessW(null, (commandLine ~ "\0").toUTF16.dup.ptr, null, null, true, dwCreationFlags,
606                         envz, workingDirectory.length ? pworkDir : null, &startinfo, &pi))
607         throw ProcessException.newFromLastError("Failed to spawn new process");
608 
609     enum STDERR_FILENO = 2;
610     // figure out if we should close any of the streams
611     if (!(config & Config.retainStdin ) && stdinFD  > STDERR_FILENO
612                                         && stdinFD  != getFD(std.stdio.stdin ))
613         stdin.close();
614     if (!(config & Config.retainStdout) && stdoutFD > STDERR_FILENO
615                                         && stdoutFD != getFD(std.stdio.stdout))
616         stdout.close();
617     if (!(config & Config.retainStderr) && stderrFD > STDERR_FILENO
618                                         && stderrFD != getFD(std.stdio.stderr))
619         stderr.close();
620 
621     CloseHandle(pi.hThread);
622     CloseHandle(pi.hProcess);
623     if (pid) {
624         *pid = pi.dwProcessId;
625     }
626 }
627 
628 /**
629  * Spawns a new process, optionally assigning it an arbitrary set of standard input, output, and error streams.
630  * 
631  * The function returns immediately, leaving the spawned process to execute in parallel with its parent. 
632  * 
633  * The spawned process is detached from its parent, so you should not wait on the returned pid.
634  * 
635  * Params:
636  *  args = An array which contains the program name as the zeroth element and any command-line arguments in the following elements.
637  *  stdin = The standard input stream of the spawned process.
638  *  stdout = The standard output stream of the spawned process.
639  *  stderr = The standard error stream of the spawned process.
640  *  env = Additional environment variables for the child process.
641  *  config = Flags that control process creation. Same as for spawnProcess.
642  *  workingDirectory = The working directory for the new process.
643  *  pid = Pointer to variable that will get pid value in case spawnProcessDetached succeed. Not used if null.
644  * 
645  * See_Also: $(LINK2 https://dlang.org/phobos/std_process.html#.spawnProcess, spawnProcess documentation)
646  */
647 void spawnProcessDetached(in char[][] args, 
648                           File stdin = std.stdio.stdin, 
649                           File stdout = std.stdio.stdout, 
650                           File stderr = std.stdio.stderr, 
651                           const string[string] env = null, 
652                           Config config = Config.none, 
653                           in char[] workingDirectory = null, 
654                           ulong* pid = null)
655 {
656     import core.exception : RangeError;
657     
658     version(Posix) {
659         if (args.length == 0) throw new RangeError();
660         auto result = spawnProcessDetachedImpl(args, stdin, stdout, stderr, env, config, workingDirectory, pid);
661         if (result[0] != 0) {
662             .errno = result[0];
663             throw ProcessException.newFromErrno(result[1]);
664         }
665     } else version(Windows) {
666         auto commandLine = escapeShellArguments(args);
667         if (commandLine.length == 0) throw new RangeError("Command line is empty");
668         spawnProcessDetachedImpl(commandLine, stdin, stdout, stderr, env, config, workingDirectory, pid);
669     }
670 }
671 
672 ///
673 unittest
674 {
675     import std.exception : assertThrown;
676     version(Posix) {
677         try {
678             auto devNull = File("/dev/null", "rwb");
679             ulong pid;
680             spawnProcessDetached(["whoami"], devNull, devNull, devNull, null, Config.none, "./test", &pid);
681             assert(pid != 0);
682             
683             assertThrown(spawnProcessDetached(["./test/nonexistent"]));
684             assertThrown(spawnProcessDetached(["./test/executable.sh"], devNull, devNull, devNull, null, Config.none, "./test/nonexistent"));
685             assertThrown(spawnProcessDetached(["./dub.json"]));
686             assertThrown(spawnProcessDetached(["./test/notreallyexecutable"]));
687         } catch(Exception e) {
688             
689         }
690     }
691     version(Windows) {
692         try {
693             ulong pid;
694             spawnProcessDetached(["whoami"], std.stdio.stdin, std.stdio.stdout, std.stdio.stderr, null, Config.none, "./test", &pid);
695             
696             assertThrown(spawnProcessDetached(["dub.json"]));
697         } catch(Exception e) {
698             
699         }
700     }
701 }
702 
703 ///ditto
704 void spawnProcessDetached(in char[][] args, const string[string] env, Config config = Config.none, in char[] workingDirectory = null, ulong* pid = null)
705 {
706     spawnProcessDetached(args, std.stdio.stdin, std.stdio.stdout, std.stdio.stderr, env, config, workingDirectory, pid);
707 }