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