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