bootc_lib/
task.rs

1use std::{
2    ffi::OsStr,
3    io::{Seek, Write},
4    process::{Command, Stdio},
5};
6
7use anyhow::{Context, Result};
8use cap_std::fs::Dir;
9use cap_std_ext::cap_std;
10use cap_std_ext::prelude::CapStdExtCommandExt;
11
12/// How much information we output
13#[derive(Debug, PartialEq, Eq, Default)]
14enum CmdVerbosity {
15    /// Nothing is output
16    Quiet,
17    /// Only the task description is output
18    #[default]
19    Description,
20    /// The task description and the full command line are output
21    Verbose,
22}
23
24/// Too many things in the install path are conditional
25pub(crate) struct Task {
26    description: String,
27    verbosity: CmdVerbosity,
28    quiet_output: bool,
29    pub(crate) cmd: Command,
30}
31
32#[allow(dead_code)]
33impl Task {
34    pub(crate) fn new(description: impl AsRef<str>, exe: impl AsRef<str>) -> Self {
35        Self::new_cmd(description, Command::new(exe.as_ref()))
36    }
37
38    /// This API can be used in place of Command::new() generally and just adds error
39    /// checking on top.
40    pub(crate) fn new_quiet(exe: impl AsRef<str>) -> Self {
41        let exe = exe.as_ref();
42        Self::new(exe, exe).quiet()
43    }
44
45    /// Set the working directory for this task.
46    pub(crate) fn cwd(mut self, dir: &Dir) -> Result<Self> {
47        self.cmd.cwd_dir(dir.try_clone()?);
48        Ok(self)
49    }
50
51    pub(crate) fn new_cmd(description: impl AsRef<str>, mut cmd: Command) -> Self {
52        let description = description.as_ref().to_string();
53        // Default to noninteractive
54        cmd.stdin(Stdio::null());
55        Self {
56            description,
57            verbosity: Default::default(),
58            quiet_output: false,
59            cmd,
60        }
61    }
62
63    /// Don't output description by default
64    pub(crate) fn quiet(mut self) -> Self {
65        self.verbosity = CmdVerbosity::Quiet;
66        self
67    }
68
69    /// Output description and cmdline
70    pub(crate) fn verbose(mut self) -> Self {
71        self.verbosity = CmdVerbosity::Verbose;
72        self
73    }
74
75    // Do not print stdout/stderr, unless the command fails
76    pub(crate) fn quiet_output(mut self) -> Self {
77        self.quiet_output = true;
78        self
79    }
80
81    pub(crate) fn args<S: AsRef<OsStr>>(mut self, args: impl IntoIterator<Item = S>) -> Self {
82        self.cmd.args(args);
83        self
84    }
85
86    pub(crate) fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
87        self.cmd.args([arg]);
88        self
89    }
90
91    /// Run the command, returning an error if the command does not exit successfully.
92    pub(crate) fn run(self) -> Result<()> {
93        self.run_with_stdin_buf(None)
94    }
95
96    fn pre_run_output(&self) {
97        match self.verbosity {
98            CmdVerbosity::Quiet => {}
99            CmdVerbosity::Description => {
100                println!("{}", self.description);
101            }
102            CmdVerbosity::Verbose => {
103                // Output the description first
104                println!("{}", self.description);
105
106                // Lock stdout so we buffer
107                let mut stdout = std::io::stdout().lock();
108                let cmd_args = std::iter::once(self.cmd.get_program())
109                    .chain(self.cmd.get_args())
110                    .map(|arg| arg.to_string_lossy());
111                // We unwrap() here to match the default for println!() even though
112                // arguably that's wrong
113                stdout.write_all(b">").unwrap();
114                for s in cmd_args {
115                    stdout.write_all(b" ").unwrap();
116                    stdout.write_all(s.as_bytes()).unwrap();
117                }
118                stdout.write_all(b"\n").unwrap();
119            }
120        }
121    }
122
123    /// Run the command with optional stdin buffer, returning an error if the command does not exit successfully.
124    pub(crate) fn run_with_stdin_buf(self, stdin: Option<&[u8]>) -> Result<()> {
125        self.pre_run_output();
126        let description = self.description;
127        let mut cmd = self.cmd;
128        let mut output = None;
129        if self.quiet_output {
130            let tmpf = tempfile::tempfile()?;
131            cmd.stdout(Stdio::from(tmpf.try_clone()?));
132            cmd.stderr(Stdio::from(tmpf.try_clone()?));
133            output = Some(tmpf);
134        }
135        tracing::debug!("exec: {cmd:?}");
136        let st = if let Some(stdin_value) = stdin {
137            cmd.stdin(Stdio::piped());
138            let mut child = cmd.spawn()?;
139            // SAFETY: We used piped for stdin
140            let mut stdin = child.stdin.take().unwrap();
141            // If this was async, we could avoid spawning a thread here
142            std::thread::scope(|s| {
143                s.spawn(move || stdin.write_all(stdin_value))
144                    .join()
145                    .map_err(|e| anyhow::anyhow!("Failed to spawn thread: {e:?}"))?
146                    .context("Failed to write to cryptsetup stdin")
147            })?;
148            child.wait()?
149        } else {
150            cmd.status()?
151        };
152        tracing::trace!("{st:?}");
153        if !st.success() {
154            if let Some(mut output) = output {
155                output.seek(std::io::SeekFrom::Start(0))?;
156                let mut stderr = std::io::stderr().lock();
157                std::io::copy(&mut output, &mut stderr)?;
158            }
159            anyhow::bail!("Task {description} failed: {st:?}");
160        }
161        Ok(())
162    }
163
164    /// Like [`run()`], but return stdout.
165    pub(crate) fn read(self) -> Result<String> {
166        self.pre_run_output();
167        let description = self.description;
168        let mut cmd = self.cmd;
169        tracing::debug!("exec: {cmd:?}");
170        cmd.stdout(Stdio::piped());
171        let child = cmd
172            .spawn()
173            .with_context(|| format!("Spawning {description} failed"))?;
174        let o = child
175            .wait_with_output()
176            .with_context(|| format!("Executing {description} failed"))?;
177        let st = o.status;
178        if !st.success() {
179            anyhow::bail!("Task {description} failed: {st:?}");
180        }
181        Ok(String::from_utf8(o.stdout)?)
182    }
183
184    pub(crate) fn new_and_run<'a>(
185        description: impl AsRef<str>,
186        exe: impl AsRef<str>,
187        args: impl IntoIterator<Item = &'a str>,
188    ) -> Result<()> {
189        let mut t = Self::new(description.as_ref(), exe);
190        t.cmd.args(args);
191        t.run()
192    }
193}