-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5212 from cgwalters/cmdutils-run
tree-wide: Import and use cmdutils from bootc
- Loading branch information
Showing
12 changed files
with
282 additions
and
64 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
//! Helpers intended for [`std::process::Command`] and related structures. | ||
//! This is copied from bootc, please edit there and re-sync! | ||
// SPDX-License-Identifier: Apache-2.0 OR MIT | ||
|
||
use std::{ | ||
io::{Read, Seek}, | ||
os::unix::process::CommandExt, | ||
process::Command, | ||
}; | ||
|
||
use anyhow::{Context, Result}; | ||
|
||
#[allow(dead_code)] | ||
/// Helpers intended for [`std::process::Command`]. | ||
pub trait CommandRunExt { | ||
/// Log (at debug level) the full child commandline. | ||
fn log_debug(&mut self) -> &mut Self; | ||
|
||
/// Execute the child process. | ||
fn run(&mut self) -> Result<()>; | ||
|
||
/// Ensure the child does not outlive the parent. | ||
fn lifecycle_bind(&mut self) -> &mut Self; | ||
|
||
/// Execute the child process and capture its output. This uses `run` internally | ||
/// and will return an error if the child process exits abnormally. | ||
fn run_get_output(&mut self) -> Result<Box<dyn std::io::BufRead>>; | ||
|
||
/// Execute the child process, parsing its stdout as JSON. This uses `run` internally | ||
/// and will return an error if the child process exits abnormally. | ||
fn run_and_parse_json<T: serde::de::DeserializeOwned>(&mut self) -> Result<T>; | ||
} | ||
|
||
/// Helpers intended for [`std::process::ExitStatus`]. | ||
pub trait ExitStatusExt { | ||
/// If the exit status signals it was not successful, return an error. | ||
/// Note that we intentionally *don't* include the command string | ||
/// in the output; we leave it to the caller to add that if they want, | ||
/// as it may be verbose. | ||
fn check_status(&mut self, stderr: std::fs::File) -> Result<()>; | ||
} | ||
|
||
/// Parse the last chunk (e.g. 1024 bytes) from the provided file, | ||
/// ensure it's UTF-8, and return that value. This function is infallible; | ||
/// if the file cannot be read for some reason, a copy of a static string | ||
/// is returned. | ||
fn last_utf8_content_from_file(mut f: std::fs::File) -> String { | ||
// u16 since we truncate to just the trailing bytes here | ||
// to avoid pathological error messages | ||
const MAX_STDERR_BYTES: u16 = 1024; | ||
let size = f | ||
.metadata() | ||
.map_err(|e| { | ||
tracing::warn!("failed to fstat: {e}"); | ||
}) | ||
.map(|m| m.len().try_into().unwrap_or(u16::MAX)) | ||
.unwrap_or(0); | ||
let size = size.min(MAX_STDERR_BYTES); | ||
let seek_offset = -(size as i32); | ||
let mut stderr_buf = Vec::with_capacity(size.into()); | ||
// We should never fail to seek()+read() really, but let's be conservative | ||
let r = match f | ||
.seek(std::io::SeekFrom::End(seek_offset.into())) | ||
.and_then(|_| f.read_to_end(&mut stderr_buf)) | ||
{ | ||
Ok(_) => String::from_utf8_lossy(&stderr_buf), | ||
Err(e) => { | ||
tracing::warn!("failed seek+read: {e}"); | ||
"<failed to read stderr>".into() | ||
} | ||
}; | ||
(&*r).to_owned() | ||
} | ||
|
||
impl ExitStatusExt for std::process::ExitStatus { | ||
fn check_status(&mut self, stderr: std::fs::File) -> Result<()> { | ||
let stderr_buf = last_utf8_content_from_file(stderr); | ||
if self.success() { | ||
return Ok(()); | ||
} | ||
anyhow::bail!(format!("Subprocess failed: {self:?}\n{stderr_buf}")) | ||
} | ||
} | ||
|
||
impl CommandRunExt for Command { | ||
/// Synchronously execute the child, and return an error if the child exited unsuccessfully. | ||
fn run(&mut self) -> Result<()> { | ||
let stderr = tempfile::tempfile()?; | ||
self.stderr(stderr.try_clone()?); | ||
tracing::trace!("exec: {self:?}"); | ||
self.status()?.check_status(stderr) | ||
} | ||
|
||
#[allow(unsafe_code)] | ||
fn lifecycle_bind(&mut self) -> &mut Self { | ||
// SAFETY: This API is safe to call in a forked child. | ||
unsafe { | ||
self.pre_exec(|| { | ||
rustix::process::set_parent_process_death_signal(Some( | ||
rustix::process::Signal::Term, | ||
)) | ||
.map_err(Into::into) | ||
}) | ||
} | ||
} | ||
|
||
/// Output a debug-level log message with this command. | ||
fn log_debug(&mut self) -> &mut Self { | ||
// We unconditionally log at trace level, so avoid double logging | ||
if !tracing::enabled!(tracing::Level::TRACE) { | ||
tracing::debug!("exec: {self:?}"); | ||
} | ||
self | ||
} | ||
|
||
fn run_get_output(&mut self) -> Result<Box<dyn std::io::BufRead>> { | ||
let mut stdout = tempfile::tempfile()?; | ||
self.stdout(stdout.try_clone()?); | ||
self.run()?; | ||
stdout.seek(std::io::SeekFrom::Start(0)).context("seek")?; | ||
Ok(Box::new(std::io::BufReader::new(stdout))) | ||
} | ||
|
||
/// Synchronously execute the child, and parse its stdout as JSON. | ||
fn run_and_parse_json<T: serde::de::DeserializeOwned>(&mut self) -> Result<T> { | ||
let output = self.run_get_output()?; | ||
serde_json::from_reader(output).map_err(Into::into) | ||
} | ||
} | ||
|
||
/// Helpers intended for [`tokio::process::Command`]. | ||
#[allow(async_fn_in_trait)] | ||
#[allow(dead_code)] | ||
pub trait AsyncCommandRunExt { | ||
/// Asynchronously execute the child, and return an error if the child exited unsuccessfully. | ||
async fn run(&mut self) -> Result<()>; | ||
} | ||
|
||
impl AsyncCommandRunExt for tokio::process::Command { | ||
async fn run(&mut self) -> Result<()> { | ||
let stderr = tempfile::tempfile()?; | ||
self.stderr(stderr.try_clone()?); | ||
self.status().await?.check_status(stderr) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn command_run_ext() { | ||
// The basics | ||
Command::new("true").run().unwrap(); | ||
assert!(Command::new("false").run().is_err()); | ||
|
||
// Verify we capture stderr | ||
let e = Command::new("/bin/sh") | ||
.args(["-c", "echo expected-this-oops-message 1>&2; exit 1"]) | ||
.run() | ||
.err() | ||
.unwrap(); | ||
similar_asserts::assert_eq!( | ||
e.to_string(), | ||
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected-this-oops-message\n" | ||
); | ||
|
||
// Ignoring invalid UTF-8 | ||
let e = Command::new("/bin/sh") | ||
.args([ | ||
"-c", | ||
r"echo -e 'expected\xf5\x80\x80\x80\x80-foo\xc0bar\xc0\xc0' 1>&2; exit 1", | ||
]) | ||
.run() | ||
.err() | ||
.unwrap(); | ||
similar_asserts::assert_eq!( | ||
e.to_string(), | ||
"Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected�����-foo�bar��\n" | ||
); | ||
} | ||
|
||
#[test] | ||
fn command_run_ext_json() { | ||
#[derive(serde::Deserialize)] | ||
struct Foo { | ||
a: String, | ||
b: u32, | ||
} | ||
let v: Foo = Command::new("echo") | ||
.arg(r##"{"a": "somevalue", "b": 42}"##) | ||
.run_and_parse_json() | ||
.unwrap(); | ||
assert_eq!(v.a, "somevalue"); | ||
assert_eq!(v.b, 42); | ||
} | ||
|
||
#[tokio::test] | ||
async fn async_command_run_ext() { | ||
use tokio::process::Command as AsyncCommand; | ||
let mut success = AsyncCommand::new("true"); | ||
let mut fail = AsyncCommand::new("false"); | ||
// Run these in parallel just because we can | ||
let (success, fail) = tokio::join!(success.run(), fail.run(),); | ||
success.unwrap(); | ||
assert!(fail.is_err()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.