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