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