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 }