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 13 version(Posix) { 14 private import core.sys.posix.unistd; 15 private import core.sys.posix.pwd; 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; 48 this._args = args; 49 this.status = status; 50 this.output = output; 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( 94 msg, 95 true, 96 expected); 97 } 98 99 /// ditto 100 auto ref ensureStatus(E : Throwable = ProcessException)(in int expected=0) const { 101 return ensureStatus( 102 "Program %s with args %s failed! Expected exit-code %s, got %s.".format( 103 _program, _args, expected, status), 104 expected); 105 } 106 107 /// ditto 108 alias ensureOk = ensureStatus; 109 110 } 111 112 113 /** This struct is used to prepare configuration for process and run it. 114 * 115 * The following methods of running a process are supported: 116 * 117 * - execute: run process and catch its output and exit code. 118 * - spawn: spawn the process in background, and optionally pipe its output. 119 * - pipe: spawn the process and attach configurable pipes to catch output. 120 * 121 * The configuration of a process can be done like so: 122 * 123 * 1. Create the `Process` instance specifying the program to run. 124 * 2. Apply your desired configuration (args, env, workDir) 125 * via calls to one of the corresponding methods. 126 * 3. Run one of `execute`, `spawn` or `pipe` methods, that will actually 127 * start the process. 128 * 129 * Configuration methods are usually prefixed with `set` word, but they 130 * may also have semantic aliases. For example, the method `setArgs` also has 131 * an alias `withArgs`, and the method `setWorkDir` has an alias `inWorkDir`. 132 * Additionally, configuration methods always 133 * return the reference to current instance of the Process being configured. 134 * 135 * Examples: 136 * --- 137 * // It is possible to run process in following way: 138 * auto result = Process('my-program') 139 * .withArgs("--verbose", "--help") 140 * .withEnv("MY_ENV_VAR", "MY_VALUE") 141 * .inWorkDir("my/working/directory") 142 * .execute() 143 * .ensureStatus!MyException("My error message on failure"); 144 * writeln(result.output); 145 * --- 146 * --- 147 * // Also, in Posix system it is possible to run command as different user: 148 * auto result = Process('my-program') 149 * .withUser("bob") 150 * .execute() 151 * .ensureStatus!MyException("My error message on failure"); 152 * writeln(result.output); 153 * --- 154 **/ 155 @safe struct Process { 156 private string _program; 157 private string[] _args; 158 private string[string] _env=null; 159 private string _workdir=null; 160 private std.process.Config _config=std.process.Config.none; 161 162 /* TODO: May be it have sense to add somekind of lock 163 * to wrap execution of process in multithreaded mode. 164 * It seems that this is needed on Posix systems, 165 * especially in case when running process with different 166 * uid/git, that requires temporary change of uid/gid of current 167 * process, but uid and gid are attributes of process, not thread. 168 */ 169 170 version(Posix) { 171 /* On posix we have ability to run process with different user, 172 * thus we have to keep desired uid/gid to run process with and 173 * original uid/git to revert uid/gid change after process completed. 174 */ 175 private Nullable!uid_t _uid; 176 private Nullable!gid_t _gid; 177 private Nullable!uid_t _original_uid; 178 private Nullable!gid_t _original_gid; 179 } 180 181 /** Create new Process instance to run specified program. 182 * 183 * Params: 184 * program = name of program to run or path of program to run 185 **/ 186 this(in string program) { 187 _program = program; 188 } 189 190 /// ditto 191 this(in Path program) { 192 _program = program.toAbsolute.toString; 193 } 194 195 /** Return string representation of process to be started 196 **/ 197 string toString() const { 198 return "Program: %s, args: %s, env: %s, workdir: %s".format( 199 _program, _args.join(" "), _env, _workdir); 200 } 201 202 /** Set arguments for the process 203 * 204 * Params: 205 * args = array of arguments to run program with 206 * 207 * Returns: 208 * reference to this (process instance) 209 **/ 210 auto ref setArgs(in string[] args...) { 211 _args = args.dup; 212 return this; 213 } 214 215 /// ditto 216 alias withArgs = setArgs; 217 218 /** Add arguments to the process. 219 * 220 * This could be used if you do not know all the arguments for program 221 * to run at single point, and you need to add it conditionally. 222 * 223 * Params: 224 * args = array of arguments to run program with 225 * 226 * Returns: 227 * reference to this (process instance) 228 * 229 * Examples: 230 * --- 231 * auto program = Process('my-program') 232 * .withArgs("--some-option"); 233 * 234 * if (some condition) 235 * program.addArgs("--some-other-opt", "--verbose"); 236 * 237 * auto result = program 238 * .execute() 239 * .ensureStatus!MyException("My error message on failure"); 240 * writeln(result.output); 241 * --- 242 **/ 243 auto ref addArgs(in string[] args...) { 244 _args ~= args; 245 return this; 246 } 247 248 /** Set work directory for the process to be started 249 * 250 * Params: 251 * workdir = working directory path to run process in 252 * 253 * Returns: 254 * reference to this (process instance) 255 * 256 **/ 257 auto ref setWorkDir(in string workdir) { 258 _workdir = workdir; 259 return this; 260 } 261 262 /// ditto 263 auto ref setWorkDir(in Path workdir) { 264 _workdir = workdir.toString; 265 return this; 266 } 267 268 /// ditto 269 alias inWorkDir = setWorkDir; 270 271 /** Set environemnt for the process to be started 272 * 273 * Params: 274 * env = associative array to update environment to run process with. 275 * 276 * Returns: 277 * reference to this (process instance) 278 * 279 **/ 280 auto ref setEnv(in string[string] env) { 281 foreach(i; env.byKeyValue) 282 _env[i.key] = i.value; 283 return this; 284 } 285 286 /** Set environment variable (specified by key) to provided value 287 * 288 * Params: 289 * key = environment variable name 290 * value = environment variable value 291 * 292 * Returns: 293 * reference to this (process instance) 294 * 295 **/ 296 auto ref setEnv(in string key, in string value) { 297 _env[key] = value; 298 return this; 299 } 300 301 /// ditto 302 alias withEnv = setEnv; 303 304 /** Set process configuration 305 **/ 306 auto ref setConfig(in std.process.Config config) { 307 _config = config; 308 return this; 309 } 310 311 /// ditto 312 alias withConfig = setConfig; 313 314 /** Set configuration flag for process to be started 315 **/ 316 auto ref setFlag(in std.process.Config.Flags flag) { 317 _config.flags |= flag; 318 return this; 319 } 320 321 /// ditto 322 auto ref setFlag(in std.process.Config flags) { 323 _config |= flags; 324 return this; 325 } 326 327 /// ditto 328 alias withFlag = setFlag; 329 330 /** Set UID to run process with 331 * 332 * Params: 333 * uid = UID (id of system user) to run process with 334 * 335 * Returns: 336 * reference to this (process instance) 337 * 338 **/ 339 version(Posix) auto ref setUID(in uid_t uid) { 340 _uid = uid; 341 return this; 342 } 343 344 /// ditto 345 version(Posix) alias withUID = setUID; 346 347 /** Set GID to run process with 348 * 349 * Params: 350 * gid = GID (id of system group) to run process with 351 * 352 * Returns: 353 * reference to this (process instance) 354 * 355 **/ 356 version(Posix) auto ref setGID(in gid_t gid) { 357 _gid = gid; 358 return this; 359 } 360 361 /// ditto 362 version(Posix) alias withGID = setGID; 363 364 /** Run process as specified user 365 * 366 * If this method applied, then the UID and GID to run process with 367 * will be taked from record in passwd database 368 * 369 * Params: 370 * username = login of user to run process as 371 * 372 * Returns: 373 * reference to this (process instance) 374 * 375 **/ 376 version(Posix) auto ref setUser(in string username) @trusted { 377 import std.string: toStringz; 378 379 /* pw info has following fields: 380 * - pw_name, 381 * - pw_passwd, 382 * - pw_uid, 383 * - pw_gid, 384 * - pw_gecos, 385 * - pw_dir, 386 * - pw_shell, 387 */ 388 auto pw = getpwnam(username.toStringz); 389 errnoEnforce( 390 pw !is null, 391 "Cannot get info about user %s".format(username)); 392 setUID(pw.pw_uid); 393 setGID(pw.pw_gid); 394 // TODO: add ability to automatically set user's home directory 395 // if needed 396 return this; 397 } 398 399 /// 400 version(Posix) alias withUser = setUser; 401 402 /// Called before running process to run pre-exec hooks; 403 private void setUpProcess() { 404 version(Posix) { 405 /* We set real user and real group here, 406 * keeping original effective user and effective group 407 * (usually original user/group is root, when such logic used) 408 * Later in preExecFunction, we can update effective user 409 * for child process to be same as real user. 410 * This is needed, because bash, changes effective user to real 411 * user when effective user is different from real. 412 * Thus, we have to set both real user and effective user 413 * for child process. 414 * 415 * We can accomplish this in two steps: 416 * - Change real uid/gid here for current process 417 * - Change effective uid/gid to match real uid/gid 418 * in preexec fuction. 419 * Because preexec function is executed in child process, 420 * that will be replaced by specified command proces, it works. 421 * 422 * Also, note, that first we have to change group ID, because 423 * when we change user id first, it may not be possible to change 424 * group. 425 */ 426 427 /* 428 * TODO: May be it have sense to change effective user/group 429 * instead of real user, and update real user in 430 * child process. 431 */ 432 if (!_gid.isNull && _gid.get != getgid) { 433 _original_gid = getgid().nullable; 434 errnoEnforce( 435 setregid(_gid.get, -1) == 0, 436 "Cannot set real GID to %s before starting process: %s".format( 437 _gid, this.toString)); 438 } 439 if (!_uid.isNull && _uid.get != getuid) { 440 _original_uid = getuid().nullable; 441 errnoEnforce( 442 setreuid(_uid.get, -1) == 0, 443 "Cannot set real UID to %s before starting process: %s".format( 444 _uid, this.toString)); 445 } 446 447 if (!_original_uid.isNull || !_original_gid.isNull) 448 _config.preExecFunction = () @trusted nothrow @nogc { 449 /* Because we cannot pass any parameters here, 450 * we just need to make real user/group equal to 451 * effective user/group for child proces. 452 * This is needed, because bash could change effective user 453 * when it is different from real user. 454 * 455 * We change here effective user/group equal 456 * to real user/group because we have changed 457 * real user/group in parent process 458 * before running this function. 459 * 460 * Also, note, that this function will be executed 461 * in child process, just before calling execve. 462 */ 463 if (setegid(getgid) != 0) 464 return false; 465 if (seteuid(getuid) != 0) 466 return false; 467 return true; 468 }; 469 470 } 471 } 472 473 /// Called after process started to run post-exec hooks; 474 private void tearDownProcess() { 475 version(Posix) { 476 // Restore original uid/gid after process started. 477 if (!_original_gid.isNull) 478 errnoEnforce( 479 setregid(_original_gid.get, -1) == 0, 480 "Cannot restore real GID to %s after process started: %s".format( 481 _original_gid, this.toString)); 482 if (!_original_uid.isNull) 483 errnoEnforce( 484 setreuid(_original_uid.get, -1) == 0, 485 "Cannot restore real UID to %s after process started: %s".format( 486 _original_uid, this.toString)); 487 } 488 } 489 490 /** Execute the configured process and capture output. 491 * 492 * Params: 493 * max_output = max size of output to capture. 494 * 495 * Returns: 496 * ProcessResult instance that contains output and exit-code 497 * of program 498 * 499 **/ 500 auto execute(in size_t max_output=size_t.max) { 501 setUpProcess(); 502 auto res = std.process.execute( 503 [_program] ~ _args, 504 _env, 505 _config, 506 max_output, 507 _workdir); 508 tearDownProcess(); 509 return ProcessResult(_program, _args.idup, res.status, res.output); 510 } 511 512 /// Spawn process 513 auto spawn(File stdin=std.stdio.stdin, 514 File stdout=std.stdio.stdout, 515 File stderr=std.stdio.stderr) { 516 setUpProcess(); 517 auto res = std.process.spawnProcess( 518 [_program] ~ _args, 519 stdin, 520 stdout, 521 stderr, 522 _env, 523 _config, 524 _workdir); 525 tearDownProcess(); 526 return res; 527 } 528 529 /// Pipe process 530 auto pipe(in Redirect redirect=Redirect.all) { 531 setUpProcess(); 532 auto res = std.process.pipeProcess( 533 [_program] ~ _args, 534 redirect, 535 _env, 536 _config, 537 _workdir); 538 tearDownProcess(); 539 return res; 540 } 541 542 /** Replace current process by executing program as configured by 543 * Process instance. 544 * 545 * Under the hood, this method will call $(REF execvpe, std, process) or 546 * $(REF execvp, std, process). 547 **/ 548 version(Posix) void execv() @system { 549 import std.algorithm; 550 import std.array; 551 552 if (_workdir) 553 // Change working directory, when needed before executing the program 554 std.file.chdir(_workdir); 555 556 if (!_env) { 557 /* Run the program, and in case when it could not be started for 558 * some reason, throw exception. 559 * 560 * For more info, see docs: https://dlang.org/library/std/process/execv.html 561 */ 562 enforce!ProcessException( 563 std.process.execvp(_program, [_program] ~ _args) != -1, 564 "Cannot exec program %s".format(this)); 565 566 /* This will not be executed in any way, because on success 567 * the current process will be replaced and on failure exception will be thrown. 568 * but add it here to make code look consistent. 569 */ 570 return; 571 } 572 573 /* In case when we have environment specified, we have to 574 * call `execvpe` function, and thus we have to convert environment 575 * variables to format suitable for this function 576 * (array of strings in format `key=value` 577 */ 578 string[] env_arr = _env.byKeyValue.map!( 579 (i) => "%s=%s".format(i.key, i.value) 580 ).array; 581 enforce!ProcessException( 582 std.process.execvpe(_program, [_program] ~ _args, env_arr) != -1, 583 "Cannot exec program %s".format(this)); 584 } 585 } 586 587 588 // Test simple api 589 @safe unittest { 590 import unit_threaded.assertions; 591 592 auto process = Process("my-program") 593 .withArgs("--verbose", "--help") 594 .withEnv("MY_VAR", "42") 595 .inWorkDir("/my/path"); 596 process._program.should == "my-program"; 597 process._args.should == ["--verbose", "--help"]; 598 process._env.should == ["MY_VAR": "42"]; 599 process._workdir.should == "/my/path"; 600 process.toString.should == 601 "Program: %s, args: %s, env: %s, workdir: %s".format( 602 process._program, process._args.join(" "), 603 process._env, process._workdir); 604 605 // Change some params of the process 606 process.setWorkDir(Path("/some/other/path")); 607 process.setEnv([ 608 "MY_VAR_2": "72", 609 ]); 610 process.addArgs("arg2", "arg3"); 611 612 // Check that changes took effect 613 process._program.should == "my-program"; 614 process._args.should == ["--verbose", "--help", "arg2", "arg3"]; 615 process._env.should == ["MY_VAR": "42", "MY_VAR_2": "72"]; 616 process._workdir.should == "/some/other/path"; 617 process.toString.should == 618 "Program: %s, args: %s, env: %s, workdir: %s".format( 619 process._program, process._args.join(" "), 620 process._env, process._workdir); 621 } 622 623 /// Test simple execution of the script 624 @safe unittest { 625 import std.string; 626 import std.ascii : newline; 627 628 import unit_threaded.assertions; 629 630 auto temp_root = createTempPath(); 631 scope(exit) temp_root.remove(); 632 633 version(Posix) { 634 import std.conv: octal; 635 auto script_path = temp_root.join("test-script.sh"); 636 script_path.writeFile( 637 "#!" ~ nativeShell ~ newline ~ 638 `echo "Test out: $1 $2"` ~ newline); 639 // Add permission to run this script 640 script_path.setAttributes(octal!755); 641 } else version(Windows) { 642 auto script_path = temp_root.join("test-script.cmd"); 643 script_path.writeFile( 644 "@echo off" ~ newline ~ 645 "echo Test out: %1 %2" ~ newline); 646 } 647 648 // Test the case when process executes fine 649 auto result = Process(script_path) 650 .withArgs("Hello", "World", "test") 651 .execute 652 .ensureOk; 653 result.status.should == 0; 654 result.output.chomp.should == "Test out: Hello World"; 655 result.isOk.shouldBeTrue; 656 result.isNotOk.shouldBeFalse; 657 // When we expect different successful exit-code 658 result.isOk(42).shouldBeFalse; 659 result.isNotOk(42).shouldBeTrue; 660 result.ensureOk(42).shouldThrow!ProcessException; 661 } 662 663 /// Test simple execution of the script that handles environment variables 664 @safe unittest { 665 import std.string; 666 import std.ascii : newline; 667 668 import unit_threaded.assertions; 669 670 auto temp_root = createTempPath(); 671 scope(exit) temp_root.remove(); 672 673 version(Posix) { 674 import std.conv: octal; 675 auto script_path = temp_root.join("test-script.sh"); 676 script_path.writeFile( 677 "#!" ~ nativeShell ~ newline ~ 678 `echo "Test out: $1 $2, $MY_PARAM_1 $MY_PARAM_2"` ~ newline); 679 // Add permission to run this script 680 script_path.setAttributes(octal!755); 681 } else version(Windows) { 682 auto script_path = temp_root.join("test-script.cmd"); 683 script_path.writeFile( 684 "@echo off" ~ newline ~ 685 "echo Test out: %1 %2, %MY_PARAM_1% %MY_PARAM_2%" ~ newline); 686 } 687 688 // Test the case when process executes fine 689 auto result = Process(script_path) 690 .withArgs("Hello") 691 .addArgs("World") 692 .withEnv("MY_PARAM_1", "the") 693 .withEnv("MY_PARAM_2", "Void") 694 .execute 695 .ensureOk; 696 result.status.should == 0; 697 result.output.chomp.should == "Test out: Hello World, the Void"; 698 result.isOk.shouldBeTrue; 699 result.isNotOk.shouldBeFalse; 700 // When we expect different successful exit-code 701 result.isOk(42).shouldBeFalse; 702 result.isNotOk(42).shouldBeTrue; 703 result.ensureOk(42).shouldThrow!ProcessException; 704 }