1 /**
2  * Spawn detached process.
3  * Authors: 
4  *  $(LINK2 https://github.com/MyLittleRobo, 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 version(Posix) private @trusted void ignorePipeErrors() nothrow
181 {
182     import core.sys.posix.signal;
183     import core.stdc.string : memset;
184     
185     sigaction_t ignoreAction;
186     memset(&ignoreAction, 0, sigaction_t.sizeof);
187     ignoreAction.sa_handler = SIG_IGN;
188     sigaction(SIGPIPE, &ignoreAction, null);
189 }
190 
191 //from std.process
192 version(Posix) private void setCLOEXEC(int fd, bool on) nothrow @nogc
193 {
194     import core.sys.posix.fcntl : fcntl, F_GETFD, FD_CLOEXEC, F_SETFD;
195     auto flags = fcntl(fd, F_GETFD);
196     if (flags >= 0)
197     {
198         if (on) flags |= FD_CLOEXEC;
199         else    flags &= ~(cast(typeof(flags)) FD_CLOEXEC);
200         flags = fcntl(fd, F_SETFD, flags);
201     }
202     assert (flags != -1 || .errno == EBADF);
203 }
204 
205 //From std.process
206 version(Posix) private const(char*)* createEnv(const string[string] childEnv, bool mergeWithParentEnv)
207 {
208     // Determine the number of strings in the parent's environment.
209     int parentEnvLength = 0;
210     if (mergeWithParentEnv)
211     {
212         if (childEnv.length == 0) return environ;
213         while (environ[parentEnvLength] != null) ++parentEnvLength;
214     }
215 
216     // Convert the "new" variables to C-style strings.
217     auto envz = new const(char)*[parentEnvLength + childEnv.length + 1];
218     int pos = 0;
219     foreach (var, val; childEnv)
220         envz[pos++] = (var~'='~val~'\0').ptr;
221 
222     // Add the parent's environment.
223     foreach (environStr; environ[0 .. parentEnvLength])
224     {
225         int eqPos = 0;
226         while (environStr[eqPos] != '=' && environStr[eqPos] != '\0') ++eqPos;
227         if (environStr[eqPos] != '=') continue;
228         auto var = environStr[0 .. eqPos];
229         if (var in childEnv) continue;
230         envz[pos++] = environStr;
231     }
232     envz[pos] = null;
233     return envz.ptr;
234 }
235 
236 //From std.process
237 version(Posix) @system unittest
238 {
239     auto e1 = createEnv(null, false);
240     assert (e1 != null && *e1 == null);
241 
242     auto e2 = createEnv(null, true);
243     assert (e2 != null);
244     int i = 0;
245     for (; environ[i] != null; ++i)
246     {
247         assert (e2[i] != null);
248         import core.stdc.string;
249         assert (strcmp(e2[i], environ[i]) == 0);
250     }
251     assert (e2[i] == null);
252 
253     auto e3 = createEnv(["foo" : "bar", "hello" : "world"], false);
254     assert (e3 != null && e3[0] != null && e3[1] != null && e3[2] == null);
255     assert ((e3[0][0 .. 8] == "foo=bar\0" && e3[1][0 .. 12] == "hello=world\0")
256          || (e3[0][0 .. 12] == "hello=world\0" && e3[1][0 .. 8] == "foo=bar\0"));
257 }
258 
259 version (Windows) private LPVOID createEnv(const string[string] childEnv, bool mergeWithParentEnv)
260 {
261     if (mergeWithParentEnv && childEnv.length == 0) return null;
262     import std.array : appender;
263     import std.uni : toUpper;
264     auto envz = appender!(wchar[])();
265     void put(string var, string val)
266     {
267         envz.put(var);
268         envz.put('=');
269         envz.put(val);
270         envz.put(cast(wchar) '\0');
271     }
272 
273     // Add the variables in childEnv, removing them from parentEnv
274     // if they exist there too.
275     auto parentEnv = mergeWithParentEnv ? environment.toAA() : null;
276     foreach (k, v; childEnv)
277     {
278         auto uk = toUpper(k);
279         put(uk, v);
280         if (uk in parentEnv) parentEnv.remove(uk);
281     }
282 
283     // Add remaining parent environment variables.
284     foreach (k, v; parentEnv) put(k, v);
285 
286     // Two final zeros are needed in case there aren't any environment vars,
287     // and the last one does no harm when there are.
288     envz.put("\0\0"w);
289     return envz.data.ptr;
290 }
291 
292 version (Windows) @system unittest
293 {
294     assert (createEnv(null, true) == null);
295     assert ((cast(wchar*) createEnv(null, false))[0 .. 2] == "\0\0"w);
296     auto e1 = (cast(wchar*) createEnv(["foo":"bar", "ab":"c"], false))[0 .. 14];
297     assert (e1 == "FOO=bar\0AB=c\0\0"w || e1 == "AB=c\0FOO=bar\0\0"w);
298 }
299 
300 private enum InternalError : ubyte
301 {
302     noerror,
303     doubleFork,
304     exec,
305     chdir,
306     getrlimit,
307     environment
308 }
309 
310 version(Posix) private Tuple!(int, string) spawnProcessDetachedImpl(in char[][] args, 
311                                                      ref File stdin, ref File stdout, ref File stderr, 
312                                                      const string[string] env, 
313                                                      Config config, 
314                                                      in char[] workingDirectory, 
315                                                      ulong* pid) nothrow
316 {
317     import std.path : baseName;
318     import std.string : toStringz;
319     
320     string filePath = args[0].idup;
321     if (filePath.baseName == filePath) {
322         auto candidate = findExecutable(filePath);
323         if (!candidate.length) {
324             return tuple(ENOENT, "Could not find executable: " ~ filePath);
325         }
326         filePath = candidate;
327     }
328     
329     if (access(toStringz(filePath), X_OK) != 0) {
330         return tuple(.errno, "Not an executable file: " ~ filePath);
331     }
332     
333     static @trusted @nogc int safePipe(ref int[2] pipefds) nothrow
334     {
335         int result = pipe(pipefds);
336         if (result != 0) {
337             return result;
338         }
339         if (fcntl(pipefds[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefds[1], F_SETFD, FD_CLOEXEC) == -1) {
340             close(pipefds[0]);
341             close(pipefds[1]);
342             return -1;
343         }
344         return result;
345     }
346     
347     int[2] execPipe, pidPipe;
348     if (safePipe(execPipe) != 0) {
349         return tuple(.errno, "Could not create pipe to check startup of child");
350     }
351     scope(exit) close(execPipe[0]);
352     if (safePipe(pidPipe) != 0) {
353         close(execPipe[1]);
354         return tuple(.errno, "Could not create pipe to get pid of child");
355     }
356     scope(exit) close(pidPipe[0]);
357     
358     int getFD(ref File f) { 
359         import core.stdc.stdio : fileno;
360         return fileno(f.getFP()); 
361     }
362     
363     int stdinFD, stdoutFD, stderrFD;
364     try {
365         stdinFD  = getFD(stdin);
366         stdoutFD = getFD(stdout);
367         stderrFD = getFD(stderr);
368     } catch(Exception e) {
369         return tuple(.errno ? .errno : EBADF, "Could not get file descriptors of standard streams");
370     }
371     
372     static void abortOnError(int execPipeOut, InternalError errorType, int error) nothrow {
373         error = error ? error : EINVAL;
374         write(execPipeOut, &errorType, errorType.sizeof);
375         write(execPipeOut, &error, error.sizeof);
376         close(execPipeOut);
377         _exit(1);
378     }
379     
380     pid_t firstFork = fork();
381     int lastError = .errno;
382     if (firstFork == 0) {
383         close(execPipe[0]);
384         close(pidPipe[0]);
385         
386         ignorePipeErrors();
387         setsid();
388         
389         int execPipeOut = execPipe[1];
390         int pidPipeOut = pidPipe[1];
391         
392         pid_t secondFork = fork();
393         if (secondFork == 0) {
394             close(pidPipeOut);
395             ignorePipeErrors();
396         
397             if (workingDirectory.length) {
398                 import core.stdc.stdlib : free;
399                 auto workDir = mallocToStringz(workingDirectory);
400                 if (chdir(workDir) == -1) {
401                     free(workDir);
402                     abortOnError(execPipeOut, InternalError.chdir, .errno);
403                 } else {
404                     free(workDir);
405                 }
406             }
407             
408             // ===== From std.process =====
409             if (stderrFD == STDOUT_FILENO) {
410                 stderrFD = dup(stderrFD);
411             }
412             dup2(stdinFD,  STDIN_FILENO);
413             dup2(stdoutFD, STDOUT_FILENO);
414             dup2(stderrFD, STDERR_FILENO);
415 
416             setCLOEXEC(STDIN_FILENO, false);
417             setCLOEXEC(STDOUT_FILENO, false);
418             setCLOEXEC(STDERR_FILENO, false);
419             
420             if (!(config & Config.inheritFDs)) {
421                 import core.sys.posix.poll : pollfd, poll, POLLNVAL;
422                 import core.sys.posix.sys.resource : rlimit, getrlimit, RLIMIT_NOFILE;
423 
424                 rlimit r;
425                 if (getrlimit(RLIMIT_NOFILE, &r) != 0) {
426                     abortOnError(execPipeOut, InternalError.getrlimit, .errno);
427                 }
428                 immutable maxDescriptors = cast(int)r.rlim_cur;
429                 immutable maxToClose = maxDescriptors - 3;
430 
431                 @nogc nothrow static bool pollClose(int maxToClose, int dontClose)
432                 {
433                     import core.stdc.stdlib : alloca;
434 
435                     pollfd* pfds = cast(pollfd*)alloca(pollfd.sizeof * maxToClose);
436                     foreach (i; 0 .. maxToClose) {
437                         pfds[i].fd = i + 3;
438                         pfds[i].events = 0;
439                         pfds[i].revents = 0;
440                     }
441                     if (poll(pfds, maxToClose, 0) >= 0) {
442                         foreach (i; 0 .. maxToClose) {
443                             if (pfds[i].fd != dontClose && !(pfds[i].revents & POLLNVAL)) {
444                                 close(pfds[i].fd);
445                             }
446                         }
447                         return true;
448                     }
449                     else {
450                         return false;
451                     }
452                 }
453 
454                 if (!pollClose(maxToClose, execPipeOut)) {
455                     foreach (i; 3 .. maxDescriptors) {
456                         if (i != execPipeOut) {
457                             close(i);
458                         }
459                     }
460                 }
461             } else {
462                 if (stdinFD  > STDERR_FILENO)  close(stdinFD);
463                 if (stdoutFD > STDERR_FILENO)  close(stdoutFD);
464                 if (stderrFD > STDERR_FILENO)  close(stderrFD);
465             }
466             // =====================
467             
468             const(char*)* envz;
469             try {
470                 envz = createEnv(env, !(config & Config.newEnv));
471             } catch(Exception e) {
472                 abortOnError(execPipeOut, InternalError.environment, EINVAL);
473             }
474             auto argv = createExecArgv(args, filePath);
475             execve(argv[0], argv, envz);
476             abortOnError(execPipeOut, InternalError.exec, .errno);
477         }
478         
479         write(pidPipeOut, &secondFork, pid_t.sizeof);
480         close(pidPipeOut);
481         
482         if (secondFork == -1) {
483             abortOnError(execPipeOut, InternalError.doubleFork, .errno);
484         } else {
485             close(execPipeOut);
486             _exit(0);
487         }
488     }
489     
490     close(execPipe[1]);
491     close(pidPipe[1]);
492     
493     if (firstFork == -1) {
494         return tuple(lastError, "Could not fork");
495     }
496     
497     InternalError status;
498     auto readExecResult = read(execPipe[0], &status, status.sizeof);
499     lastError = .errno;
500     
501     import core.sys.posix.sys.wait : waitpid;
502     int waitResult;
503     waitpid(firstFork, &waitResult, 0);
504     
505     if (readExecResult == -1) {
506         return tuple(lastError, "Could not read from pipe to get child status");
507     }
508     
509     try {
510         if (!(config & Config.retainStdin ) && stdinFD  > STDERR_FILENO
511                                         && stdinFD  != getFD(std.stdio.stdin ))
512         stdin.close();
513         if (!(config & Config.retainStdout) && stdoutFD > STDERR_FILENO
514                                             && stdoutFD != getFD(std.stdio.stdout))
515             stdout.close();
516         if (!(config & Config.retainStderr) && stderrFD > STDERR_FILENO
517                                             && stderrFD != getFD(std.stdio.stderr))
518             stderr.close();
519     } catch(Exception e) {
520         
521     }
522     
523     if (status == 0) {
524         if (pid !is null) {
525             pid_t actualPid = 0;
526             if (read(pidPipe[0], &actualPid, pid_t.sizeof) >= 0) {
527                 *pid = actualPid;
528             } else {
529                 *pid = 0;
530             }
531         }
532         return tuple(0, "");
533     } else {
534         int error;
535         readExecResult = read(execPipe[0], &error, error.sizeof);
536         if (readExecResult == -1) {
537             return tuple(.errno, "Error occured but could not read exec errno from pipe");
538         }
539         switch(status) {
540             case InternalError.doubleFork: return tuple(error, "Could not fork twice");
541             case InternalError.exec: return tuple(error, "Could not exec");
542             case InternalError.chdir: return tuple(error, "Could not set working directory");
543             case InternalError.getrlimit: return tuple(error, "getrlimit");
544             case InternalError.environment: return tuple(error, "Could not set environment variables");
545             default:return tuple(error, "Unknown error occured");
546         }
547     }
548 }
549 
550 version(Windows) private void spawnProcessDetachedImpl(in char[] commandLine, 
551                                                      ref File stdin, ref File stdout, ref File stderr, 
552                                                      const string[string] env, 
553                                                      Config config, 
554                                                      in char[] workingDirectory, 
555                                                      ulong* pid)
556 {
557     import std.windows.syserror;
558     
559     // from std.process
560     // Prepare environment.
561     auto envz = createEnv(env, !(config & Config.newEnv));
562 
563     // Startup info for CreateProcessW().
564     STARTUPINFO_W startinfo;
565     startinfo.cb = startinfo.sizeof;
566     static int getFD(ref File f) { return f.isOpen ? f.fileno : -1; }
567 
568     // Extract file descriptors and HANDLEs from the streams and make the
569     // handles inheritable.
570     static void prepareStream(ref File file, DWORD stdHandle, string which,
571                               out int fileDescriptor, out HANDLE handle)
572     {
573         fileDescriptor = getFD(file);
574         handle = null;
575         if (fileDescriptor >= 0)
576             handle = file.windowsHandle;
577         // Windows GUI applications have a fd but not a valid Windows HANDLE.
578         if (handle is null || handle == INVALID_HANDLE_VALUE)
579             handle = GetStdHandle(stdHandle);
580 
581         DWORD dwFlags;
582         if (GetHandleInformation(handle, &dwFlags))
583         {
584             if (!(dwFlags & HANDLE_FLAG_INHERIT))
585             {
586                 if (!SetHandleInformation(handle,
587                                           HANDLE_FLAG_INHERIT,
588                                           HANDLE_FLAG_INHERIT))
589                 {
590                     throw new StdioException(
591                         "Failed to make "~which~" stream inheritable by child process ("
592                         ~sysErrorString(GetLastError()) ~ ')',
593                         0);
594                 }
595             }
596         }
597     }
598     int stdinFD = -1, stdoutFD = -1, stderrFD = -1;
599     prepareStream(stdin,  STD_INPUT_HANDLE,  "stdin" , stdinFD,  startinfo.hStdInput );
600     prepareStream(stdout, STD_OUTPUT_HANDLE, "stdout", stdoutFD, startinfo.hStdOutput);
601     prepareStream(stderr, STD_ERROR_HANDLE,  "stderr", stderrFD, startinfo.hStdError );
602 
603     if ((startinfo.hStdInput  != null && startinfo.hStdInput  != INVALID_HANDLE_VALUE)
604      || (startinfo.hStdOutput != null && startinfo.hStdOutput != INVALID_HANDLE_VALUE)
605      || (startinfo.hStdError  != null && startinfo.hStdError  != INVALID_HANDLE_VALUE))
606         startinfo.dwFlags = STARTF_USESTDHANDLES;
607 
608     // Create process.
609     PROCESS_INFORMATION pi;
610     DWORD dwCreationFlags =
611         CREATE_UNICODE_ENVIRONMENT |
612         ((config & Config.suppressConsole) ? CREATE_NO_WINDOW : 0);
613         
614         
615     import std.utf : toUTF16z, toUTF16;
616     auto pworkDir = workingDirectory.toUTF16z();
617     if (!CreateProcessW(null, (commandLine ~ "\0").toUTF16.dup.ptr, null, null, true, dwCreationFlags,
618                         envz, workingDirectory.length ? pworkDir : null, &startinfo, &pi))
619         throw ProcessException.newFromLastError("Failed to spawn new process");
620 
621     enum STDERR_FILENO = 2;
622     // figure out if we should close any of the streams
623     if (!(config & Config.retainStdin ) && stdinFD  > STDERR_FILENO
624                                         && stdinFD  != getFD(std.stdio.stdin ))
625         stdin.close();
626     if (!(config & Config.retainStdout) && stdoutFD > STDERR_FILENO
627                                         && stdoutFD != getFD(std.stdio.stdout))
628         stdout.close();
629     if (!(config & Config.retainStderr) && stderrFD > STDERR_FILENO
630                                         && stderrFD != getFD(std.stdio.stderr))
631         stderr.close();
632 
633     CloseHandle(pi.hThread);
634     CloseHandle(pi.hProcess);
635     if (pid) {
636         *pid = pi.dwProcessId;
637     }
638 }
639 
640 /**
641  * Spawns a new process, optionally assigning it an arbitrary set of standard input, output, and error streams.
642  * 
643  * The function returns immediately, leaving the spawned process to execute in parallel with its parent. 
644  * 
645  * The spawned process is detached from its parent, so you should not wait on the returned pid.
646  * 
647  * Params:
648  *  args = An array which contains the program name as the zeroth element and any command-line arguments in the following elements.
649  *  stdin = The standard input stream of the spawned process.
650  *  stdout = The standard output stream of the spawned process.
651  *  stderr = The standard error stream of the spawned process.
652  *  env = Additional environment variables for the child process.
653  *  config = Flags that control process creation. Same as for spawnProcess.
654  *  workingDirectory = The working directory for the new process.
655  *  pid = Pointer to variable that will get pid value in case spawnProcessDetached succeed. Not used if null.
656  * 
657  * See_Also: $(LINK2 https://dlang.org/phobos/std_process.html#.spawnProcess, spawnProcess documentation)
658  */
659 void spawnProcessDetached(in char[][] args, 
660                           File stdin = std.stdio.stdin, 
661                           File stdout = std.stdio.stdout, 
662                           File stderr = std.stdio.stderr, 
663                           const string[string] env = null, 
664                           Config config = Config.none, 
665                           in char[] workingDirectory = null, 
666                           ulong* pid = null)
667 {
668     import core.exception : RangeError;
669     
670     version(Posix) {
671         if (args.length == 0) throw new RangeError();
672         auto result = spawnProcessDetachedImpl(args, stdin, stdout, stderr, env, config, workingDirectory, pid);
673         if (result[0] != 0) {
674             .errno = result[0];
675             throw ProcessException.newFromErrno(result[1]);
676         }
677     } else version(Windows) {
678         auto commandLine = escapeShellArguments(args);
679         if (commandLine.length == 0) throw new RangeError("Command line is empty");
680         spawnProcessDetachedImpl(commandLine, stdin, stdout, stderr, env, config, workingDirectory, pid);
681     }
682 }
683 
684 ///
685 unittest
686 {
687     import std.exception : assertThrown;
688     version(Posix) {
689         try {
690             auto devNull = File("/dev/null", "rwb");
691             ulong pid;
692             spawnProcessDetached(["whoami"], devNull, devNull, devNull, null, Config.none, "./test", &pid);
693             assert(pid != 0);
694             
695             assertThrown(spawnProcessDetached(["./test/nonexistent"]));
696             assertThrown(spawnProcessDetached(["./test/executable.sh"], devNull, devNull, devNull, null, Config.none, "./test/nonexistent"));
697             assertThrown(spawnProcessDetached(["./dub.json"]));
698             assertThrown(spawnProcessDetached(["./test/notreallyexecutable"]));
699         } catch(Exception e) {
700             
701         }
702     }
703     version(Windows) {
704         try {
705             ulong pid;
706             spawnProcessDetached(["whoami"], std.stdio.stdin, std.stdio.stdout, std.stdio.stderr, null, Config.none, "./test", &pid);
707             
708             assertThrown(spawnProcessDetached(["dub.json"]));
709         } catch(Exception e) {
710             
711         }
712     }
713 }
714 
715 ///ditto
716 void spawnProcessDetached(in char[][] args, const string[string] env, Config config = Config.none, in char[] workingDirectory = null, ulong* pid = null)
717 {
718     spawnProcessDetached(args, std.stdio.stdin, std.stdio.stdout, std.stdio.stderr, env, config, workingDirectory, pid);
719 }