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