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