Skip to main content

steward/
cmd.rs

1use std::{io, ops::Deref, process::Stdio, time::Duration};
2
3use once_cell::sync::Lazy;
4use tokio::process::Command;
5
6use crate::{Env, Location, Result, RunningProcess};
7
8/// Struct holds a specification of a command. Can be used for running one-off commands, long running processes etc.
9#[derive(Clone)]
10pub struct Cmd<Loc> {
11    /// Command to run.
12    pub exe: String,
13    /// Environment of a process.
14    pub env: Env,
15    /// Working directory of a process.
16    pub pwd: Loc,
17    /// Message displayed when running a command.
18    pub msg: Option<String>,
19}
20
21impl<Loc> Cmd<Loc>
22where
23    Loc: Location,
24{
25    /// Command to run.
26    pub fn exe(&self) -> &str {
27        &self.exe
28    }
29
30    /// Environment of a process.
31    pub fn env(&self) -> &Env {
32        &self.env
33    }
34
35    /// Working directory of a process.
36    pub fn pwd(&self) -> &Loc {
37        &self.pwd
38    }
39
40    /// Message displayed when running a command.
41    pub fn msg(&self) -> Option<&String> {
42        self.msg.as_ref()
43    }
44}
45
46/// Amount of time to wait before killing hanged process.
47///
48/// When constructing a new [`Process`](crate::Process) via [`process!`](crate::process!) macro
49/// without providing a specific timeout, the [`KillTimeout::default`](KillTimeout::default) implementation is used.
50/// By default, the timeout is 10 seconds, but it can be configured by setting `PROCESS_TIMEOUT` environment variable.
51#[derive(Clone, Debug)]
52pub struct KillTimeout(Duration);
53
54impl KillTimeout {
55    /// Constructs a new timeout.
56    pub fn new(duration: Duration) -> Self {
57        Self(duration)
58    }
59
60    /// Returns underlying [`Duration`](std::time::Duration).
61    pub fn duration(&self) -> Duration {
62        self.0
63    }
64}
65
66static DEFAULT_KILL_TIMEOUT: Lazy<Duration> = Lazy::new(|| {
67    let default = Duration::from_secs(10);
68    match std::env::var("PROCESS_TIMEOUT") {
69        Err(_) => default,
70        Ok(timeout) => match timeout.parse::<u64>() {
71            Ok(x) => Duration::from_secs(x),
72            Err(_) => {
73                eprintln!(
74                    "⚠️  TIMEOUT variable is not a valid int: {}. Using default: {}",
75                    timeout,
76                    default.as_secs()
77                );
78                default
79            }
80        },
81    }
82});
83
84impl Default for KillTimeout {
85    fn default() -> Self {
86        Self(*DEFAULT_KILL_TIMEOUT)
87    }
88}
89
90impl Deref for KillTimeout {
91    type Target = Duration;
92
93    fn deref(&self) -> &Self::Target {
94        &self.0
95    }
96}
97
98impl From<Duration> for KillTimeout {
99    fn from(value: Duration) -> Self {
100        Self(value)
101    }
102}
103
104/// Options for [`Cmd::spawn`](Cmd::spawn).
105pub struct SpawnOptions {
106    /// Stdout stream.
107    pub stdout: Stdio,
108    /// Stderr stream.
109    pub stderr: Stdio,
110    /// Amount of time to wait before killing hanged process. See [`KillTimeout`](crate::KillTimeout).
111    pub timeout: KillTimeout,
112    /// Whether to create a new process group. When true, the spawned process becomes
113    /// a process group leader, allowing all its children to be killed together.
114    pub group: bool,
115}
116
117impl Default for SpawnOptions {
118    fn default() -> Self {
119        Self {
120            stdout: Stdio::inherit(),
121            stderr: Stdio::inherit(),
122            timeout: KillTimeout::default(),
123            group: false,
124        }
125    }
126}
127
128/// Enum returned from [`Cmd::output`](Cmd::output).
129pub struct Output(Vec<u8>);
130
131impl Output {
132    /// Returns bytes from stdout. Be aware that if child process was interrupted
133    /// during the command execution (e.g. user pressed Ctrl + C), this function will terminate
134    /// current process with zero exit code.
135    pub fn bytes(self) -> Vec<u8> {
136        self.0
137    }
138
139    /// Same as [`Output::bytes`](Output::bytes) but attempts to convert bytes to `String`.
140    pub fn as_string(self) -> Result<String> {
141        let bytes = self.bytes();
142        let string = String::from_utf8(bytes)?;
143        Ok(string)
144    }
145}
146
147impl<Loc> Cmd<Loc>
148where
149    Loc: Location,
150{
151    #[cfg(unix)]
152    pub(crate) const SHELL: &'static str = "/bin/sh";
153
154    #[cfg(windows)]
155    pub(crate) const SHELL: &'static str = "cmd";
156
157    #[cfg(unix)]
158    pub(crate) fn shelled(cmd: &str) -> Vec<&str> {
159        vec!["-c", cmd]
160    }
161
162    #[cfg(windows)]
163    pub(crate) fn shelled(cmd: &str) -> Vec<&str> {
164        vec!["/c", cmd]
165    }
166
167    /// Runs one-off command with inherited [`Stdio`](std::process::Stdio). Prints headline (witn [`Cmd::msg`](Cmd::msg), if provided) to stderr.
168    pub async fn run(&self) -> Result<()> {
169        eprintln!("{}", crate::headline!(self));
170
171        let opts = SpawnOptions {
172            stdout: Stdio::inherit(),
173            stderr: Stdio::inherit(),
174            ..Default::default()
175        };
176
177        self.spawn(opts)?.wait().await?;
178
179        Ok(())
180    }
181
182    /// Runs one-off command. Doesn't print anything.
183    pub async fn silent(&self) -> Result<()> {
184        let opts = SpawnOptions {
185            stdout: Stdio::null(),
186            stderr: Stdio::null(),
187            ..Default::default()
188        };
189
190        self.spawn(opts)?.wait().await?;
191
192        Ok(())
193    }
194
195    /// Runs one-off command and returns [`Output`](Output). Doesn't print anything.
196    pub async fn output(&self) -> Result<Output> {
197        let opts = SpawnOptions {
198            stdout: Stdio::piped(),
199            stderr: Stdio::piped(),
200            ..Default::default()
201        };
202
203        let res = self.spawn(opts)?.wait().await?;
204
205        Ok(Output(res.stdout))
206    }
207
208    /// A low-level method for spawning a process and getting a handle to it.
209    #[cfg(unix)]
210    pub fn spawn(&self, opts: SpawnOptions) -> io::Result<RunningProcess> {
211        let cmd = self;
212
213        let SpawnOptions {
214            stdout,
215            stderr,
216            timeout,
217            group,
218        } = opts;
219
220        let mut command = Command::new(Cmd::<Loc>::SHELL);
221        command
222            .args(Cmd::<Loc>::shelled(&cmd.exe))
223            .envs(cmd.env.to_owned())
224            .current_dir(cmd.pwd.as_path())
225            .stdout(stdout)
226            .stderr(stderr);
227
228        if group {
229            command.process_group(0);
230        }
231
232        let process = command.spawn()?;
233
234        Ok(RunningProcess {
235            process,
236            timeout,
237            group,
238        })
239    }
240
241    /// A low-level method for spawning a process and getting a handle to it.
242    #[cfg(windows)]
243    pub fn spawn(&self, opts: SpawnOptions) -> io::Result<RunningProcess> {
244        let cmd = self;
245
246        let SpawnOptions {
247            stdout,
248            stderr,
249            timeout,
250            group,
251        } = opts;
252
253        let mut command = Command::new(Cmd::<Loc>::SHELL);
254        command
255            .args(Cmd::<Loc>::shelled(&cmd.exe))
256            .envs(cmd.env.to_owned())
257            .current_dir(cmd.pwd.as_path())
258            .stdout(stdout)
259            .stderr(stderr);
260
261        let process = command.spawn()?;
262
263        Ok(RunningProcess {
264            process,
265            timeout,
266            group,
267        })
268    }
269}
270
271/// Convenience macro for creating a [`Cmd`](Cmd).
272///
273/// ## Examples
274/// General command:
275/// ```ignore
276/// cmd! {
277///   "rm -rf target",
278///   env: Env::empty(),
279///   pwd: Loc::root(),
280///   msg: "Removing target dir",
281/// }
282/// ```
283///
284/// Dynamically constructed command:
285/// ```ignore
286/// cmd! {
287///   format!("rm -rf {}", dir),
288///   env: Env::empty(),
289///   pwd: Loc::root(),
290///   msg: format!("Removing {} dir", dir),
291/// }
292/// ```
293///
294/// Command without a message:
295/// ```ignore
296/// cmd! {
297///   "ls",
298///   env: Env::empty(),
299///   pwd: Loc::root(),
300/// }
301/// ```
302///
303/// You can label a command with `exe:` if you want to:
304/// ```ignore
305/// cmd! {
306///   exe: "ls",
307///   env: Env::empty(),
308///   pwd: Loc::root(),
309/// }
310/// ```
311#[macro_export]
312macro_rules! cmd {
313    {
314        $exe:literal,
315        env: $env:expr,
316        pwd: $pwd:expr,
317        msg: $msg:literal$(,)?
318    } => {
319        $crate::Cmd {
320            exe: $exe.to_string(),
321            env: $env,
322            pwd: $pwd,
323            msg: Some($msg.to_string()),
324        }
325    };
326    {
327        exe: $exe:literal,
328        env: $env:expr,
329        pwd: $pwd:expr,
330        msg: $msg:literal$(,)?
331    } => {
332        $crate::Cmd {
333            exe: $exe.to_string(),
334            env: $env,
335            pwd: $pwd,
336            msg: Some($msg.to_string()),
337        }
338    };
339    {
340        $exe:literal,
341        env: $env:expr,
342        pwd: $pwd:expr,
343        msg: Some($msg:expr)$(,)?
344    } => {
345        $crate::Cmd {
346            exe: $exe.to_string(),
347            env: $env,
348            pwd: $pwd,
349            msg: Some($msg),
350        }
351    };
352    {
353        exe: $exe:literal,
354        env: $env:expr,
355        pwd: $pwd:expr,
356        msg: Some($msg:expr)$(,)?
357    } => {
358        $crate::Cmd {
359            exe: $exe.to_string(),
360            env: $env,
361            pwd: $pwd,
362            msg: Some($msg),
363        }
364    };
365    {
366        $exe:literal,
367        env: $env:expr,
368        pwd: $pwd:expr,
369        msg: None$(,)?
370    } => {
371        $crate::Cmd {
372            exe: $exe.to_string(),
373            env: $env,
374            pwd: $pwd,
375            msg: None,
376        }
377    };
378    {
379        exe: $exe:literal,
380        env: $env:expr,
381        pwd: $pwd:expr,
382        msg: None$(,)?
383    } => {
384        $crate::Cmd {
385            exe: $exe.to_string(),
386            env: $env,
387            pwd: $pwd,
388            msg: None,
389        }
390    };
391    {
392        $exe:literal,
393        env: $env:expr,
394        pwd: $pwd:expr,
395        msg: $msg:expr$(,)?
396    } => {
397        $crate::Cmd {
398            exe: $exe.to_string(),
399            env: $env,
400            pwd: $pwd,
401            msg: Some($msg),
402        }
403    };
404    {
405        exe: $exe:literal,
406        env: $env:expr,
407        pwd: $pwd:expr,
408        msg: $msg:expr$(,)?
409    } => {
410        $crate::Cmd {
411            exe: $exe.to_string(),
412            env: $env,
413            pwd: $pwd,
414            msg: Some($msg),
415        }
416    };
417    {
418        $exe:expr,
419        env: $env:expr,
420        pwd: $pwd:expr,
421        msg: $msg:literal$(,)?
422    } => {
423        $crate::Cmd {
424            exe: $exe,
425            env: $env,
426            pwd: $pwd,
427            msg: Some($msg.to_string()),
428        }
429    };
430    {
431        exe: $exe:expr,
432        env: $env:expr,
433        pwd: $pwd:expr,
434        msg: $msg:literal$(,)?
435    } => {
436        $crate::Cmd {
437            exe: $exe,
438            env: $env,
439            pwd: $pwd,
440            msg: Some($msg.to_string()),
441        }
442    };
443    {
444        $exe:expr,
445        env: $env:expr,
446        pwd: $pwd:expr,
447        msg: Some($msg:expr)$(,)?
448    } => {
449        $crate::Cmd {
450            exe: $exe,
451            env: $env,
452            pwd: $pwd,
453            msg: Some($msg),
454        }
455    };
456    {
457        exe: $exe:expr,
458        env: $env:expr,
459        pwd: $pwd:expr,
460        msg: Some($msg:expr)$(,)?
461    } => {
462        $crate::Cmd {
463            exe: $exe,
464            env: $env,
465            pwd: $pwd,
466            msg: Some($msg),
467        }
468    };
469    {
470        $exe:expr,
471        env: $env:expr,
472        pwd: $pwd:expr,
473        msg: None$(,)?
474    } => {
475        $crate::Cmd {
476            exe: $exe,
477            env: $env,
478            pwd: $pwd,
479            msg: None,
480        }
481    };
482    {
483        exe: $exe:expr,
484        env: $env:expr,
485        pwd: $pwd:expr,
486        msg: None$(,)?
487    } => {
488        $crate::Cmd {
489            exe: $exe,
490            env: $env,
491            pwd: $pwd,
492            msg: None,
493        }
494    };
495    {
496        $exe:expr,
497        env: $env:expr,
498        pwd: $pwd:expr,
499        msg: $msg:expr$(,)?
500    } => {
501        $crate::Cmd {
502            exe: $exe,
503            env: $env,
504            pwd: $pwd,
505            msg: Some($msg),
506        }
507    };
508    {
509        exe: $exe:expr,
510        env: $env:expr,
511        pwd: $pwd:expr,
512        msg: $msg:expr$(,)?
513    } => {
514        $crate::Cmd {
515            exe: $exe,
516            env: $env,
517            pwd: $pwd,
518            msg: Some($msg),
519        }
520    };
521    {
522        $exe:literal,
523        env: $env:expr,
524        pwd: $pwd:expr$(,)?
525    } => {
526        $crate::Cmd {
527            exe: $exe.to_string(),
528            env: $env,
529            pwd: $pwd,
530            msg: None,
531        }
532    };
533    {
534        exe: $exe:literal,
535        env: $env:expr,
536        pwd: $pwd:expr$(,)?
537    } => {
538        $crate::Cmd {
539            exe: $exe.to_string(),
540            env: $env,
541            pwd: $pwd,
542            msg: None,
543        }
544    };
545    {
546        $exe:expr,
547        env: $env:expr,
548        pwd: $pwd:expr$(,)?
549    } => {
550        $crate::Cmd {
551            exe: $exe,
552            env: $env,
553            pwd: $pwd,
554            msg: None,
555        }
556    };
557    {
558        exe: $exe:expr,
559        env: $env:expr,
560        pwd: $pwd:expr$(,)?
561    } => {
562        $crate::Cmd {
563            exe: $exe,
564            env: $env,
565            pwd: $pwd,
566            msg: None,
567        }
568    };
569}
570
571#[cfg(test)]
572mod tests {
573    use crate::{Cmd, Env, Location};
574
575    #[allow(dead_code)]
576    fn cmd_macro_unlabeled_exe_literal_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
577        cmd! {
578          "ls",
579          env: env,
580          pwd: loc,
581          msg: "!",
582        }
583    }
584
585    #[allow(dead_code)]
586    fn cmd_macro_labeled_exe_literal_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
587        cmd! {
588          exe: "ls",
589          env: env,
590          pwd: loc,
591          msg: "!",
592        }
593    }
594
595    #[allow(dead_code)]
596    fn cmd_macro_unlabeled_exe_expr_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
597        cmd! {
598          format!("ls {}", "."),
599          env: env,
600          pwd: loc,
601          msg: "!",
602        }
603    }
604
605    #[allow(dead_code)]
606    fn cmd_macro_labeled_exe_expr_msg_literal<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
607        cmd! {
608          exe: format!("ls {}", "."),
609          env: env,
610          pwd: loc,
611          msg: "!",
612        }
613    }
614
615    #[allow(dead_code)]
616    fn cmd_macro_unlabeled_exe_expr_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
617        cmd! {
618          format!("ls {}", "."),
619          env: env,
620          pwd: loc,
621          msg: format!("!"),
622        }
623    }
624
625    #[allow(dead_code)]
626    fn cmd_macro_labeled_exe_expr_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
627        cmd! {
628          exe: format!("ls {}", "."),
629          env: env,
630          pwd: loc,
631          msg: format!("!"),
632        }
633    }
634
635    #[allow(dead_code)]
636    fn cmd_macro_unlabeled_exe_literal_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
637        cmd! {
638          "ls",
639          env: env,
640          pwd: loc,
641          msg: format!("!"),
642        }
643    }
644
645    #[allow(dead_code)]
646    fn cmd_macro_labeled_exe_literal_msg_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
647        cmd! {
648          exe: "ls",
649          env: env,
650          pwd: loc,
651          msg: format!("!"),
652        }
653    }
654
655    #[allow(dead_code)]
656    fn cmd_macro_unlabeled_exe_literal_msg_some_expr<Loc: Location>(
657        env: Env,
658        loc: Loc,
659    ) -> Cmd<Loc> {
660        cmd! {
661          "ls",
662          env: env,
663          pwd: loc,
664          msg: Some(format!("!")),
665        }
666    }
667
668    #[allow(dead_code)]
669    fn cmd_macro_labeled_exe_literal_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
670        cmd! {
671          exe: "ls",
672          env: env,
673          pwd: loc,
674          msg: Some(format!("!")),
675        }
676    }
677
678    #[allow(dead_code)]
679    fn cmd_macro_unlabeled_exe_expr_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
680        cmd! {
681          format!("ls {}", "."),
682          env: env,
683          pwd: loc,
684          msg: Some(format!("!")),
685        }
686    }
687
688    #[allow(dead_code)]
689    fn cmd_macro_labeled_exe_expr_msg_some_expr<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
690        cmd! {
691          exe: format!("ls {}", "."),
692          env: env,
693          pwd: loc,
694          msg: Some(format!("!")),
695        }
696    }
697
698    #[allow(dead_code)]
699    fn cmd_macro_unlabeled_exe_literal_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
700        cmd! {
701          "ls",
702          env: env,
703          pwd: loc,
704          msg: None,
705        }
706    }
707
708    #[allow(dead_code)]
709    fn cmd_macro_labeled_exe_literal_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
710        cmd! {
711          exe: "ls",
712          env: env,
713          pwd: loc,
714          msg: None,
715        }
716    }
717
718    #[allow(dead_code)]
719    fn cmd_macro_unlabeled_exe_expr_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
720        cmd! {
721          format!("ls {}", "."),
722          env: env,
723          pwd: loc,
724          msg: None,
725        }
726    }
727
728    #[allow(dead_code)]
729    fn cmd_macro_labeled_exe_expr_msg_none<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
730        cmd! {
731          exe: format!("ls {}", "."),
732          env: env,
733          pwd: loc,
734          msg: None,
735        }
736    }
737
738    #[allow(dead_code)]
739    fn cmd_macro_unlabeled_exe_literal_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
740        cmd! {
741          "ls",
742          env: env,
743          pwd: loc,
744        }
745    }
746
747    #[allow(dead_code)]
748    fn cmd_macro_labeled_exe_literal_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
749        cmd! {
750          exe: "ls",
751          env: env,
752          pwd: loc,
753        }
754    }
755
756    #[allow(dead_code)]
757    fn cmd_macro_unlabeled_exe_expr_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
758        cmd! {
759          format!("ls {}", "."),
760          env: env,
761          pwd: loc,
762        }
763    }
764
765    #[allow(dead_code)]
766    fn cmd_macro_labeled_exe_expr_no_msg<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
767        cmd! {
768          exe: format!("ls {}", "."),
769          env: env,
770          pwd: loc,
771        }
772    }
773
774    #[allow(dead_code)]
775    fn cmd_macro_unlabeled_exe_no_trailing_comma<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
776        cmd! { "ls", env: env, pwd: loc }
777    }
778
779    #[allow(dead_code)]
780    fn cmd_macro_labeled_exe_no_trailing_comma<Loc: Location>(env: Env, loc: Loc) -> Cmd<Loc> {
781        cmd! { exe: "ls", env: env, pwd: loc }
782    }
783}