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