1 /** Module that defines the main `Process` struct and associated components. 2 **/ 3 module theprocess.process; 4 5 private import std.format; 6 private import std.process; 7 private import std.file; 8 private import std.stdio; 9 private import std.exception; 10 private import std.string: join; 11 private import std.typecons; 12 private import std.format: format; 13 14 version(Posix) { 15 private import core.sys.posix.unistd; 16 } 17 18 private import thepath; 19 20 private import theprocess.utils; 21 private import theprocess.exception: ProcessException; 22 23 24 /** Process result, produced by 'execute' method of Process. 25 **/ 26 @safe immutable struct ProcessResult { 27 /// The program that was invoked to obtain this result. 28 private string _program; 29 30 /// The arguments passed to the program to obtain this result. 31 private string[] _args; 32 33 /// exit code of the process 34 int status; 35 36 /// text output of the process 37 string output; 38 39 // Do not allow to create records without params 40 @disable this(); 41 42 private pure this( 43 in string program, 44 immutable string[] args, 45 in int status, 46 in string output) nothrow { 47 this._program = program.idup; 48 this._args = args; 49 this.status = status; 50 this.output = output.idup; 51 } 52 53 /** Check if status is Ok. 54 * 55 * Params: 56 * expected = expected exit code. Default: 0 57 * 58 * Returns: 59 * True it exit status is equal to expected result, otherwise False. 60 **/ 61 bool isOk(in int expected=0) const { return this.status == expected; } 62 63 /** Check if status is not Ok. 64 * 65 * Params: 66 * expected = expected successfule exit code. Default: 0 67 * 68 * Returns: 69 * True it exit status is NOT equal to expected result, otherwise False. 70 **/ 71 bool isNotOk(in int expected=0) const { return !isOk(expected); } 72 73 /** Ensure that program exited with expected exit code. 74 * 75 * Params: 76 * msg = message to throw in exception in case of check failure 77 * add_output = if set to True, then output of command will be attached 78 * to message on failure. 79 * expected = expected exit-code, if differ, then 80 * exception will be thrown. 81 **/ 82 auto ref ensureStatus(E : Throwable = ProcessException)( 83 in string msg, in bool add_output, in int expected=0) const { 84 enforce!E( 85 isOk(expected), 86 !add_output ? msg : msg ~ "\nOutput: " ~ output); 87 return this; 88 } 89 90 /// ditto 91 auto ref ensureStatus(E : Throwable = ProcessException)( 92 in string msg, in int expected=0) const { 93 return ensureStatus!E( 94 msg, 95 false, 96 expected); 97 } 98 99 /// ditto 100 auto ref ensureStatus(E : Throwable = ProcessException)(in bool add_output, in int expected=0) const { 101 return ensureStatus!E( 102 "Program %s with args %s failed! Expected exit-code %s, got %s.".format( 103 _program, _args, expected, status), 104 add_output, 105 expected); 106 } 107 108 /// ditto 109 auto ref ensureStatus(E : Throwable = ProcessException)(in int expected=0) const { 110 return ensureStatus!E(false, expected); 111 } 112 113 /// ditto 114 alias ensureOk = ensureStatus; 115 116 } 117 118 119 /** This struct is used to prepare configuration for process and run it. 120 * 121 * The following methods of running a process are supported: 122 * 123 * - execute: run process and catch its output and exit code. 124 * - spawn: spawn the process in background, and optionally pipe its output. 125 * - pipe: spawn the process and attach configurable pipes to catch output. 126 * 127 * The configuration of a process can be done like so: 128 * 129 * 1. Create the `Process` instance specifying the program to run. 130 * 2. Apply your desired configuration (args, env, workDir) 131 * via calls to one of the corresponding methods. 132 * 3. Run one of `execute`, `spawn` or `pipe` methods, that will actually 133 * start the process. 134 * 135 * Configuration methods come in two families with distinct semantics: 136 * 137 * - `set*` / `add*` methods mutate the current instance in place and return 138 * `void`, making them suitable for conditional modification of an 139 * already-stored `Process` variable. 140 * - `with*` / `in*` methods return a new `Process` by value, leaving the 141 * original unchanged, making them safe to use in chained expressions. 142 * Most `set*` / `add*` method has a `with*` counterpart: 143 * `addArgs` ↔ `withArgs`, 144 * `setWorkDir` ↔ `inWorkDir`, `setEnv` ↔ `withEnv`, etc. 145 * 146 * Examples: 147 * --- 148 * // It is possible to run process in following way: 149 * auto result = Process("my-program") 150 * .withArgs("--verbose", "--help") 151 * .withEnv("MY_ENV_VAR", "MY_VALUE") 152 * .inWorkDir("my/working/directory") 153 * .execute() 154 * .ensureStatus!MyException("My error message on failure"); 155 * writeln(result.output); 156 * --- 157 * --- 158 * // Also, in Posix system it is possible to run command as different user: 159 * auto result = Process("my-program") 160 * .withUser("bob") 161 * .execute() 162 * .ensureStatus!MyException("My error message on failure"); 163 * writeln(result.output); 164 * --- 165 **/ 166 @safe struct Process { 167 private string _program; 168 private string[] _args; 169 private string[string] _env=null; 170 private string _workdir=null; 171 private std.process.Config _config=std.process.Config.none; 172 173 /* TODO: May be it have sense to add somekind of lock 174 * to wrap execution of process in multithreaded mode. 175 * It seems that this is needed on Posix systems, 176 * especially in case when running process with different 177 * uid/git, that requires temporary change of uid/gid of current 178 * process, but uid and gid are attributes of process, not thread. 179 */ 180 181 version(Posix) { 182 /* On posix we have ability to run process with different user, 183 * thus we have to keep desired uid/gid to run process with and 184 * original uid/git to revert uid/gid change after process completed. 185 */ 186 private Nullable!uid_t _uid; 187 private Nullable!gid_t _gid; 188 private Nullable!uid_t _original_uid; 189 private Nullable!gid_t _original_gid; 190 } 191 192 /** Create new Process instance to run specified program. 193 * 194 * Params: 195 * program = name of program to run or path of program to run 196 **/ 197 this(in string program) { 198 _program = program.idup; 199 } 200 201 /// ditto 202 this(in Path program) { 203 _program = program.toAbsolute.toString; 204 } 205 206 /** Copy the process configuration. Could be useful when needed to run 207 * command multipe times with slightly different configuration. 208 * Returns new instance of process. 209 **/ 210 Process copy() const { 211 Process res = Process(this._program); 212 213 res._config = this._config; 214 215 if (this._args) 216 res.setArgs(this._args); 217 if (this._env) 218 res.setEnv(this._env); 219 if (this._workdir) 220 res.setWorkDir(this._workdir); 221 222 version(Posix) { 223 res._uid = this._uid; 224 res._gid = this._gid; 225 res._original_uid = this._original_uid; 226 res._original_gid = this._original_gid; 227 } 228 return res; 229 } 230 231 /// Ensure that copy, setArgs, addArgs, and withArgs work correctly 232 unittest { 233 import unit_threaded.assertions; 234 235 auto p = Process("some-test-program").withArgs("arg1", "arg2"); 236 p._args.should == ["arg1", "arg2"]; 237 238 // setArgs mutates in place (void return) 239 p.setArgs("arg3", "arg4"); 240 p._args.should == ["arg3", "arg4"]; 241 242 // addArgs mutates in place (void return) 243 p.addArgs("arg4b"); 244 p._args.should == ["arg3", "arg4", "arg4b"]; 245 246 // withArgs returns a new Process with args appended, without modifying the original 247 auto p2 = p.withArgs("arg5", "arg6"); 248 p2._args.should == ["arg3", "arg4", "arg4b", "arg5", "arg6"]; 249 p._args.should == ["arg3", "arg4", "arg4b"]; 250 251 // withArgs on a fresh process works as expected (append to empty = set) 252 auto p3 = Process("other-program").withArgs("arg7"); 253 p3._args.should == ["arg7"]; 254 } 255 256 /** Return string representation of process to be started 257 **/ 258 string toString() const { 259 return "Program: %s, args: %s, env: %s, workdir: %s".format( 260 _program, _args.join(" "), _env, _workdir); 261 } 262 263 /** Set arguments for the process 264 * 265 * Note, replaces currently configured args for the process with provided args 266 * 267 * Params: 268 * args = array of arguments to run program with 269 **/ 270 void setArgs(in string[] args...) { 271 _args = args.dup; 272 } 273 274 /** Return a new Process with the provided arguments appended, 275 * leaving the original unchanged. 276 * 277 * This is the non-mutating counterpart of addArgs. 278 * Can be called multiple times to progressively build up arguments. 279 **/ 280 Process withArgs(in string[] args...) const { 281 auto result = this.copy(); 282 result.addArgs(args); 283 return result; 284 } 285 286 /** Add arguments to the process. 287 * 288 * This could be used if you do not know all the arguments for program 289 * to run at single point, and you need to add it conditionally. 290 * 291 * Params: 292 * args = array of arguments to add 293 * 294 * Examples: 295 * --- 296 * auto program = Process("my-program") 297 * .withArgs("--some-option"); 298 * 299 * if (some condition) 300 * program.addArgs("--some-other-opt", "--verbose"); 301 * 302 * auto result = program 303 * .execute() 304 * .ensureStatus!MyException("My error message on failure"); 305 * writeln(result.output); 306 * --- 307 **/ 308 void addArgs(in string[] args...) { 309 _args ~= args; 310 } 311 312 /** Append a single argument, returning a new Process (non-mutating). 313 * Equivalent to withArgs. 314 * 315 * Examples: 316 * --- 317 * auto git = Process("git").withArgs("--git-dir", myPath); 318 * (git ~ "clone" ~ url).execute.ensureOk; 319 * --- 320 **/ 321 Process opBinary(string op)(in string arg) const if (op == "~") { 322 return this.withArgs(arg); 323 } 324 325 /** Append multiple arguments, returning a new Process (non-mutating). 326 * Equivalent to withArgs. 327 * 328 * Examples: 329 * --- 330 * auto git = Process("git").withArgs("--git-dir", myPath); 331 * (git ~ ["clone", url]).execute.ensureOk; 332 * --- 333 **/ 334 Process opBinary(string op)(in string[] args) const if (op == "~") { 335 return this.withArgs(args); 336 } 337 338 /** Append a single argument in place (mutating). 339 * Equivalent to addArgs. 340 **/ 341 void opOpAssign(string op)(in string arg) if (op == "~") { 342 this.addArgs(arg); 343 } 344 345 /** Append multiple arguments in place (mutating). 346 * Equivalent to addArgs. 347 **/ 348 void opOpAssign(string op)(in string[] args) if (op == "~") { 349 this.addArgs(args); 350 } 351 352 /// Ensure that ~ and ~= work correctly 353 unittest { 354 import unit_threaded.assertions; 355 356 auto p = Process("git").withArgs("--git-dir", "/my/path"); 357 358 // ~ with single string returns new Process, original unchanged 359 auto p2 = p ~ "clone"; 360 p2._args.should == ["--git-dir", "/my/path", "clone"]; 361 p._args.should == ["--git-dir", "/my/path"]; 362 363 // ~ with string[] returns new Process, original unchanged 364 auto p3 = p ~ ["clone", "https://example.com"]; 365 p3._args.should == ["--git-dir", "/my/path", "clone", "https://example.com"]; 366 p._args.should == ["--git-dir", "/my/path"]; 367 368 // ~ chains correctly 369 auto p4 = p ~ "clone" ~ "https://example.com"; 370 p4._args.should == ["--git-dir", "/my/path", "clone", "https://example.com"]; 371 p._args.should == ["--git-dir", "/my/path"]; 372 373 // ~= with single string mutates in place 374 auto p5 = p.copy(); 375 p5 ~= "status"; 376 p5._args.should == ["--git-dir", "/my/path", "status"]; 377 378 // ~= with string[] mutates in place 379 auto p6 = p.copy(); 380 p6 ~= ["log", "--oneline"]; 381 p6._args.should == ["--git-dir", "/my/path", "log", "--oneline"]; 382 } 383 384 /** Set work directory for the process to be started 385 * 386 * Params: 387 * workdir = working directory path to run process in 388 **/ 389 void setWorkDir(in string workdir) { 390 _workdir = workdir.idup; 391 } 392 393 /// ditto 394 void setWorkDir(in Path workdir) { 395 _workdir = workdir.toString.idup; 396 } 397 398 /** Return a new Process with the working directory set to the provided 399 * path, leaving the original unchanged. 400 **/ 401 Process inWorkDir(in string workdir) const { 402 auto result = this.copy(); 403 result.setWorkDir(workdir); 404 return result; 405 } 406 407 /// ditto 408 Process inWorkDir(in Path workdir) const { 409 auto result = this.copy(); 410 result.setWorkDir(workdir); 411 return result; 412 } 413 414 /** Set environemnt for the process to be started. 415 * Could be called multiple times to update environment. 416 * 417 * Params: 418 * env = associative array to update environment to run process with. 419 **/ 420 void setEnv(in string[string] env) { 421 foreach(i; env.byKeyValue) 422 _env[i.key] = i.value; 423 } 424 425 /** Set environment variable (specified by key) to provided value 426 * 427 * Params: 428 * key = environment variable name 429 * value = environment variable value 430 **/ 431 void setEnv(in string key, in string value) { 432 _env[key.idup] = value.idup; 433 } 434 435 /** Return a new Process with the environment updated with the provided 436 * key-value pairs, leaving the original unchanged. 437 **/ 438 Process withEnv(in string[string] env) const { 439 auto result = this.copy(); 440 result.setEnv(env); 441 return result; 442 } 443 444 /// ditto 445 Process withEnv(in string key, in string value) const { 446 auto result = this.copy(); 447 result.setEnv(key, value); 448 return result; 449 } 450 451 /** Run process with new environment 452 * (do not inherit environment variables from parent process) 453 **/ 454 void setNewEnv() { 455 _config.flags |= std.process.Config.Flags.newEnv; 456 } 457 458 /** Return a new Process configured to start with a fresh environment 459 * (not inheriting parent environment variables), leaving the original 460 * unchanged. 461 **/ 462 Process withNewEnv() const { 463 auto result = this.copy(); 464 result.setNewEnv(); 465 return result; 466 } 467 468 /** Set process configuration 469 **/ 470 void setConfig(in std.process.Config config) { 471 _config.flags = config.flags; 472 } 473 474 /** Return a new Process with the process configuration set to the 475 * provided value, leaving the original unchanged. 476 **/ 477 Process withConfig(in std.process.Config config) const { 478 auto result = this.copy(); 479 result.setConfig(config); 480 return result; 481 } 482 483 /** Set configuration flag for process to be started 484 **/ 485 void setFlag(in std.process.Config.Flags flag) { 486 _config.flags |= flag; 487 } 488 489 /// ditto 490 void setFlag(in std.process.Config flags) { 491 _config |= flags; 492 } 493 494 /** Return a new Process with the given configuration flag set, 495 * leaving the original unchanged. 496 **/ 497 Process withFlag(in std.process.Config.Flags flag) const { 498 auto result = this.copy(); 499 result.setFlag(flag); 500 return result; 501 } 502 503 /// ditto 504 Process withFlag(in std.process.Config flags) const { 505 auto result = this.copy(); 506 result.setFlag(flags); 507 return result; 508 } 509 510 /** Apply Config.stderrPassThrough flag. 511 * With this flag, stderr will not be captured, 512 * but instead directly passed to console or terminal. 513 **/ 514 void setStderrPassThrough() { 515 setFlag(std.process.Config.stderrPassThrough); 516 } 517 518 /** Return a new Process with Config.stderrPassThrough set, 519 * leaving the original unchanged. 520 **/ 521 Process withStderrPassThrough() const { 522 return withFlag(std.process.Config.stderrPassThrough); 523 } 524 525 /** Set UID to run process with 526 * 527 * Params: 528 * uid = UID (id of system user) to run process with 529 * 530 * Returns: 531 * reference to this (process instance) 532 * 533 **/ 534 version(Posix) void setUID(in uid_t uid) { 535 _uid = uid; 536 } 537 538 /** Return a new Process configured to run with the given UID, 539 * leaving the original unchanged. 540 **/ 541 version(Posix) Process withUID(in uid_t uid) const { 542 auto result = this.copy(); 543 result.setUID(uid); 544 return result; 545 } 546 547 /** Set GID to run process with 548 * 549 * Params: 550 * gid = GID (id of system group) to run process with 551 * 552 * Returns: 553 * reference to this (process instance) 554 * 555 **/ 556 version(Posix) void setGID(in gid_t gid) { 557 _gid = gid; 558 } 559 560 /** Return a new Process configured to run with the given GID, 561 * leaving the original unchanged. 562 **/ 563 version(Posix) Process withGID(in gid_t gid) const { 564 auto result = this.copy(); 565 result.setGID(gid); 566 return result; 567 } 568 569 /** Run process as specified user 570 * 571 * If this method applied, then the UID and GID to run process with 572 * will be taked from record in passwd database 573 * 574 * Params: 575 * username = login of user to run process as 576 **/ 577 version(Posix) void setUser(in string username, in bool userWorkDir=false) @trusted { 578 auto user = getSystemUser(username); 579 if (user.isNull) 580 throw new ProcessException("User %s does not exist".format(username)); 581 582 _uid = user.get.uid; 583 _gid = user.get.gid; 584 585 if (userWorkDir) 586 _workdir = user.get.homeDir; 587 } 588 589 /** Return a new Process configured to run as the given user, 590 * leaving the original unchanged. 591 **/ 592 version(Posix) Process withUser( 593 in string username, in bool userWorkDir=false) @trusted const { 594 auto result = this.copy(); 595 result.setUser(username, userWorkDir); 596 return result; 597 } 598 599 /// Called before running process to run pre-exec hooks; 600 private void setUpProcess() { 601 version(Posix) { 602 /* We set real user and real group here, 603 * keeping original effective user and effective group 604 * (usually original user/group is root, when such logic used) 605 * Later in preExecFunction, we can update effective user 606 * for child process to be same as real user. 607 * This is needed, because bash, changes effective user to real 608 * user when effective user is different from real. 609 * Thus, we have to set both real user and effective user 610 * for child process. 611 * 612 * We can accomplish this in two steps: 613 * - Change real uid/gid here for current process 614 * - Change effective uid/gid to match real uid/gid 615 * in preexec fuction. 616 * Because preexec function is executed in child process, 617 * that will be replaced by specified command proces, it works. 618 * 619 * Also, note, that first we have to change group ID, because 620 * when we change user id first, it may not be possible to change 621 * group. 622 */ 623 624 /* 625 * TODO: May be it have sense to change effective user/group 626 * instead of real user, and update real user in 627 * child process. 628 */ 629 630 // TODO: It seems that in latest releases better preexec function was implemented 631 // Check it, may be it have sense to use it. 632 if (!_gid.isNull && _gid.get != getgid) { 633 _original_gid = getgid().nullable; 634 errnoEnforce( 635 setregid(_gid.get, -1) == 0, 636 "Cannot set real GID to %s before starting process: %s".format( 637 _gid, this.toString)); 638 } 639 if (!_uid.isNull && _uid.get != getuid) { 640 _original_uid = getuid().nullable; 641 errnoEnforce( 642 setreuid(_uid.get, -1) == 0, 643 "Cannot set real UID to %s before starting process: %s".format( 644 _uid, this.toString)); 645 } 646 647 if (!_original_uid.isNull || !_original_gid.isNull) 648 _config.preExecFunction = () @trusted nothrow @nogc { 649 /* Because we cannot pass any parameters here, 650 * we just need to make real user/group equal to 651 * effective user/group for child proces. 652 * This is needed, because bash could change effective user 653 * when it is different from real user. 654 * 655 * We change here effective user/group equal 656 * to real user/group because we have changed 657 * real user/group in parent process 658 * before running this function. 659 * 660 * Also, note, that this function will be executed 661 * in child process, just before calling execve. 662 */ 663 if (setegid(getgid) != 0) 664 return false; 665 if (seteuid(getuid) != 0) 666 return false; 667 return true; 668 }; 669 670 } 671 } 672 673 /// Called after process started to run post-exec hooks; 674 private void tearDownProcess() { 675 version(Posix) { 676 // Restore original uid/gid after process started, then clear 677 // the saved values so re-running this Process is safe. 678 if (!_original_gid.isNull) { 679 errnoEnforce( 680 setregid(_original_gid.get, -1) == 0, 681 "Cannot restore real GID to %s after process started: %s".format( 682 _original_gid, this.toString)); 683 _original_gid.nullify(); 684 } 685 if (!_original_uid.isNull) { 686 errnoEnforce( 687 setreuid(_original_uid.get, -1) == 0, 688 "Cannot restore real UID to %s after process started: %s".format( 689 _original_uid, this.toString)); 690 _original_uid.nullify(); 691 } 692 _config.preExecFunction = null; 693 } 694 } 695 696 /** Execute the configured process and capture output. 697 * 698 * Params: 699 * max_output = max size of output to capture. 700 * 701 * Returns: 702 * ProcessResult instance that contains output and exit-code 703 * of program 704 * 705 **/ 706 auto execute(in size_t max_output=size_t.max) { 707 setUpProcess(); 708 scope(exit) tearDownProcess(); 709 auto res = std.process.execute( 710 [_program] ~ _args, 711 _env, 712 _config, 713 max_output, 714 _workdir); 715 return ProcessResult(_program, _args.idup, res.status, res.output); 716 } 717 718 /// Spawn process 719 auto spawn(File stdin=std.stdio.stdin, 720 File stdout=std.stdio.stdout, 721 File stderr=std.stdio.stderr) { 722 setUpProcess(); 723 scope(exit) tearDownProcess(); 724 auto res = std.process.spawnProcess( 725 [_program] ~ _args, 726 stdin, 727 stdout, 728 stderr, 729 _env, 730 _config, 731 _workdir); 732 return res; 733 } 734 735 /// Pipe process 736 auto pipe(in Redirect redirect=Redirect.all) { 737 setUpProcess(); 738 scope(exit) tearDownProcess(); 739 auto res = std.process.pipeProcess( 740 [_program] ~ _args, 741 redirect, 742 _env, 743 _config, 744 _workdir); 745 return res; 746 } 747 748 /** Replace current process by executing program as configured by 749 * Process instance. 750 * 751 * Under the hood, this method will call $(REF execvpe, std, process) or 752 * $(REF execvp, std, process). 753 **/ 754 version(Posix) void execv() @system { 755 import std.algorithm; 756 import std.array; 757 758 if (!_gid.isNull && _gid.get != getgid) { 759 // Change rgid and egid if needed 760 errnoEnforce( 761 setregid(_gid.get, _gid.get) == 0, 762 "Cannot set real GID to %s before starting process: %s".format( 763 _gid, this.toString)); 764 } 765 if (!_uid.isNull && _uid.get != getuid) { 766 // Change ruid and euid if needed 767 errnoEnforce( 768 setreuid(_uid.get, _uid.get) == 0, 769 "Cannot set real UID to %s before starting process: %s".format( 770 _uid, this.toString)); 771 } 772 773 // Change working directory, when needed before executing the program 774 if (_workdir) 775 std.file.chdir(_workdir); 776 777 // Prepare environment variable for process 778 string[string] env; 779 if (_config.flags & std.process.Config.Flags.newEnv) 780 env = _env; 781 else { 782 // If we do not need new environment, then merge parent process 783 // environment with environment configured for process execution. 784 env = std.process.environment.toAA; 785 foreach(i; _env.byKeyValue) 786 env[i.key] = i.value; 787 } 788 789 /* We call `execvpe` function, thus we have to provide environment 790 * variables in format suitable for this function 791 * (array of strings in format `key=value`). 792 * If there is no environment required, then we just need to provide 793 * empty string. 794 **/ 795 string[] env_arr = env.byKeyValue.map!( 796 (i) => "%s=%s".format(i.key, i.value) 797 ).array; 798 enforce!ProcessException( 799 std.process.execvpe(_program, [_program] ~ _args, env_arr) != -1, 800 "Cannot exec program %s".format(this.toString)); 801 } 802 } 803 804 805 // Test simple api 806 @safe unittest { 807 import unit_threaded.assertions; 808 809 auto process = Process("my-program") 810 .withArgs("--verbose", "--help") 811 .withEnv("MY_VAR", "42") 812 .inWorkDir("/my/path"); 813 process._program.should == "my-program"; 814 process._args.should == ["--verbose", "--help"]; 815 process._env.should == ["MY_VAR": "42"]; 816 process._workdir.should == "/my/path"; 817 process.toString.should == 818 "Program: %s, args: %s, env: %s, workdir: %s".format( 819 process._program, process._args.join(" "), 820 process._env, process._workdir); 821 822 // Change some params of the process 823 process.setWorkDir(Path("/some/other/path")); 824 process.setEnv([ 825 "MY_VAR_2": "72", 826 ]); 827 process.addArgs("arg2", "arg3"); 828 829 // Check that changes took effect 830 process._program.should == "my-program"; 831 process._args.should == ["--verbose", "--help", "arg2", "arg3"]; 832 process._env.should == ["MY_VAR": "42", "MY_VAR_2": "72"]; 833 process._workdir.should == "/some/other/path"; 834 process.toString.should == 835 "Program: %s, args: %s, env: %s, workdir: %s".format( 836 process._program, process._args.join(" "), 837 process._env, process._workdir); 838 } 839 840 /// Test simple execution of the script 841 @safe unittest { 842 import std.string; 843 import std.ascii : newline; 844 845 import unit_threaded.assertions; 846 847 auto temp_root = createTempPath(); 848 scope(exit) temp_root.remove(); 849 850 version(Posix) { 851 import std.conv: octal; 852 auto script_path = temp_root.join("test-script.sh"); 853 script_path.writeFile( 854 "#!" ~ nativeShell ~ newline ~ 855 `echo "Test out: $1 $2"` ~ newline); 856 // Add permission to run this script 857 script_path.setAttributes(octal!755); 858 } else version(Windows) { 859 auto script_path = temp_root.join("test-script.cmd"); 860 script_path.writeFile( 861 "@echo off" ~ newline ~ 862 "echo Test out: %1 %2" ~ newline); 863 } 864 865 // Test the case when process executes fine 866 auto result = Process(script_path) 867 .withArgs("Hello", "World", "test") 868 .execute 869 .ensureOk; 870 result.status.should == 0; 871 result.output.chomp.should == "Test out: Hello World"; 872 result.isOk.shouldBeTrue; 873 result.isNotOk.shouldBeFalse; 874 // When we expect different successful exit-code 875 result.isOk(42).shouldBeFalse; 876 result.isNotOk(42).shouldBeTrue; 877 result.ensureOk(42).shouldThrow!ProcessException; 878 } 879 880 /// Test simple execution of the script that handles environment variables 881 @safe unittest { 882 import std.string; 883 import std.ascii : newline; 884 885 import unit_threaded.assertions; 886 887 auto temp_root = createTempPath(); 888 scope(exit) temp_root.remove(); 889 890 /* Do similar trick as in Phobos for portable newline output 891 * 892 * To avoid printing the newline characters, we use the echo|set trick on 893 * Windows, and printf on POSIX (neither echo -n nor echo \c are portable). 894 */ 895 version(Posix) { 896 import std.conv: octal; 897 auto script_path = temp_root.join("test-script.sh"); 898 script_path.writeFile( 899 "#!" ~ nativeShell ~ newline ~ 900 `printf "Test out: $1 $2, $MY_PARAM_1 $MY_PARAM_2"` ~ newline); 901 // Add permission to run this script 902 script_path.setAttributes(octal!755); 903 } else version(Windows) { 904 auto script_path = temp_root.join("test-script.cmd"); 905 script_path.writeFile( 906 `@echo off` ~ newline ~ 907 `echo|set /p DUMMY="Test out: %1 %2, %MY_PARAM_1% %MY_PARAM_2%"` ~ newline); 908 } 909 910 // Test the case when process executes fine 911 auto result = Process(script_path) 912 .withArgs("Hello") 913 .withArgs("World") 914 .withEnv("MY_PARAM_1", "the") 915 .withEnv("MY_PARAM_2", "Void") 916 .execute 917 .ensureOk; 918 result.status.should == 0; 919 result.output.chomp.should == "Test out: Hello World, the Void"; 920 result.isOk.shouldBeTrue; 921 result.isNotOk.shouldBeFalse; 922 // When we expect different successful exit-code 923 result.isOk(42).shouldBeFalse; 924 result.isNotOk(42).shouldBeTrue; 925 926 // Ensure that status is ok, if not ok, then raise error 927 result.ensureOk(42).shouldThrow!ProcessException; 928 929 // Optionally allow to print command output on failure with custom error message or with standard one. 930 result.ensureOk("Custom error message", 42).shouldThrowWithMessage!ProcessException( 931 "Custom error message"); 932 result.ensureOk("Error message", true, 42).shouldThrowWithMessage!ProcessException( 933 "Error message\nOutput: %s".format("Test out: Hello World, the Void")); 934 result.ensureOk(true, 42).shouldThrowWithMessage!ProcessException( 935 "Program %s with args %s failed! Expected exit-code %s, got %s.\nOutput: %s".format( 936 result._program, result._args, 42, 0, "Test out: Hello World, the Void")); 937 } 938 939 /// Test simple execution of the script with user (use current user) 940 version(Posix) @safe unittest { 941 import std.string; 942 import std.ascii : newline; 943 944 import unit_threaded.assertions; 945 946 auto temp_root = createTempPath(); 947 scope(exit) temp_root.remove(); 948 949 import std.conv: octal; 950 auto script_path = temp_root.join("test-script.sh"); 951 script_path.writeFile( 952 "#!" ~ nativeShell ~ newline ~ 953 `echo "Test out: $1 $2"` ~ newline); 954 // Add permission to run this script 955 script_path.setAttributes(octal!755); 956 957 auto username = Process("whoami").execute.ensureOk(true).output.strip; 958 959 // Test the case when process executes fine 960 auto result = Process(script_path) 961 .withArgs("Hello", "World", "test") 962 .withUser(username) 963 .execute 964 .ensureOk; 965 result.status.should == 0; 966 result.output.chomp.should == "Test out: Hello World"; 967 result.isOk.shouldBeTrue; 968 result.isNotOk.shouldBeFalse; 969 // When we expect different successful exit-code 970 result.isOk(42).shouldBeFalse; 971 result.isNotOk(42).shouldBeTrue; 972 result.ensureOk(42).shouldThrow!ProcessException; 973 } 974 975 976 /// Test simple execution of the script within user's home directory 977 version(Posix) @safe unittest { 978 import std.string; 979 import std.ascii : newline; 980 981 import unit_threaded.assertions; 982 983 // Change current working dir to /tmp 984 Path.tempDir.chdir; 985 986 auto current_user = Process("whoami").execute.ensureOk(true).output.strip; 987 auto workdir = Process("pwd") 988 .withUser(current_user) 989 .execute 990 .ensureOk(true) 991 .output.strip; 992 993 Path(workdir).realPath.should == Path.tempDir.realPath; 994 995 workdir = Process("pwd") 996 .withUser(current_user, true) 997 .execute 998 .ensureOk(true) 999 .output.strip; 1000 1001 Path(workdir).realPath.should == Path("~").realPath; 1002 }