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