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