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