release_utils/
cmd.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9//! Utilities for running child processes.
10
11use std::fmt::{self, Display, Formatter};
12use std::io;
13use std::process::{Child, Command, ExitStatus};
14use std::string::FromUtf8Error;
15
16/// Error returned when running a child process fails.
17#[derive(Debug)]
18pub enum RunCommandError {
19    /// Failed to launch command. May indicate the program is not installed.
20    Launch {
21        /// Stringified form of the command that failed.
22        cmd: String,
23        /// Underlying error.
24        err: io::Error,
25    },
26
27    /// Failed to wait for command to exit.
28    Wait {
29        /// Stringified form of the command that failed.
30        cmd: String,
31        /// Underlying error.
32        err: io::Error,
33    },
34
35    /// The command exited with a non-zero code, or was terminated by a
36    /// signal.
37    NonZeroExit {
38        /// Stringified form of the command that failed.
39        cmd: String,
40        /// Exit status.
41        status: ExitStatus,
42    },
43
44    /// The command's output is not valid UTF8.
45    ///
46    /// This error is only used by [`get_cmd_stdout_utf8`].
47    NonUtf8 {
48        /// Stringified form of the command that failed.
49        cmd: String,
50        /// Underlying error.
51        err: FromUtf8Error,
52    },
53}
54
55impl Display for RunCommandError {
56    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Launch { cmd, .. } => {
59                write!(f, "failed to launch command \"{cmd}\"")
60            }
61            Self::Wait { cmd, .. } => {
62                write!(f, "failed to wait for command \"{cmd}\" to exit")
63            }
64            Self::NonZeroExit { cmd, status } => {
65                write!(f, "command \"{cmd}\" failed with {status}")
66            }
67            Self::NonUtf8 { cmd, .. } => {
68                write!(f, "command \"{cmd}\" output is not utf-8")
69            }
70        }
71    }
72}
73
74impl std::error::Error for RunCommandError {
75    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
76        match self {
77            Self::Launch { err, .. } => Some(err),
78            Self::Wait { err, .. } => Some(err),
79            Self::NonZeroExit { .. } => None,
80            Self::NonUtf8 { err, .. } => Some(err),
81        }
82    }
83}
84
85/// Format a command in a way suitable for logging.
86pub fn format_cmd(cmd: &Command) -> String {
87    format!("{cmd:?}").replace('"', "")
88}
89
90/// Log a command and run it.
91///
92/// Returns an error if the process fails to launch or if the exit code
93/// is non-zero.
94pub fn run_cmd(mut cmd: Command) -> Result<(), RunCommandError> {
95    let cmd_str = format_cmd(&cmd);
96    println!("Running: {}", cmd_str);
97    let status = cmd.status().map_err(|err| RunCommandError::Launch {
98        cmd: cmd_str.clone(),
99        err,
100    })?;
101    if status.success() {
102        Ok(())
103    } else {
104        Err(RunCommandError::NonZeroExit {
105            cmd: cmd_str,
106            status,
107        })
108    }
109}
110
111/// Log a command, run it, and get its output.
112///
113/// Returns an error if the process fails to launch or if the exit code
114/// is non-zero.
115pub fn get_cmd_stdout(mut cmd: Command) -> Result<Vec<u8>, RunCommandError> {
116    let cmd_str = format_cmd(&cmd);
117    println!("Running: {}", cmd_str);
118    let output = cmd.output().map_err(|err| RunCommandError::Launch {
119        cmd: cmd_str.clone(),
120        err,
121    })?;
122    if output.status.success() {
123        Ok(output.stdout)
124    } else {
125        Err(RunCommandError::NonZeroExit {
126            cmd: cmd_str,
127            status: output.status,
128        })
129    }
130}
131
132/// Log a command, run it, and get its output as a `String`.
133///
134/// Returns an error if the process fails to launch, or if the exit code
135/// is non-zero, or if the output is not utf-8.
136pub fn get_cmd_stdout_utf8(cmd: Command) -> Result<String, RunCommandError> {
137    let cmd_str = format_cmd(&cmd);
138    let stdout = get_cmd_stdout(cmd)?;
139    String::from_utf8(stdout)
140        .map_err(|err| RunCommandError::NonUtf8 { cmd: cmd_str, err })
141}
142
143/// Wait for a child process to exit.
144///
145/// Returns an error if waiting fails, or if the exit code is non-zero.
146pub fn wait_for_child(
147    mut child: Child,
148    cmd_str: String,
149) -> Result<(), RunCommandError> {
150    let status = child.wait().map_err(|err| RunCommandError::Wait {
151        cmd: cmd_str.clone(),
152        err,
153    })?;
154    if status.success() {
155        Ok(())
156    } else {
157        Err(RunCommandError::NonZeroExit {
158            cmd: cmd_str,
159            status,
160        })
161    }
162}