Skip to content

Commit

Permalink
spawn-ext: add checked_output
Browse files Browse the repository at this point in the history
Summary:
Similar to Python's checked_output, return an error if the process exited non-zero.
The error format is similar to what `git.py` does:

   program exited with code 1:
     git foo bar ...
       stdout + stderr

Reviewed By: muirdm

Differential Revision: D54218774

fbshipit-source-id: d9f2931b059f53cbd9f16616a77dbd5f7e9523b2
  • Loading branch information
quark-zju authored and facebook-github-bot committed Mar 18, 2024
1 parent 763f020 commit bc6f7a6
Showing 1 changed file with 130 additions and 0 deletions.
130 changes: 130 additions & 0 deletions eden/scm/lib/spawn-ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
//! Unix.
//! - `spawn_detached` is a quicker way to spawn and forget.
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::io;
use std::process::Child;
use std::process::Command;
use std::process::ExitStatus;
use std::process::Output;
use std::process::Stdio;

/// Extensions to `std::process::Command`.
Expand All @@ -30,13 +35,114 @@ pub trait CommandExt {
/// Return the process id.
fn spawn_detached(&mut self) -> io::Result<Child>;

/// Similar to `Output` but reports as an error for non-zero exit code.
fn checked_output(&mut self) -> io::Result<Output>;

/// Similar to `status` but reports an error for non-zero exits.
fn checked_run(&mut self) -> io::Result<ExitStatus>;

/// Create a `Command` to run `shell_cmd` through system's shell. This uses "cmd.exe"
/// on Windows and "/bin/sh" otherwise. Do not add more args to the returned
/// `Command`. On Windows, you do not need to use the shell to run batch files (the
/// Rust stdlib detects batch files and uses "cmd.exe" automatically).
fn new_shell(shell_cmd: impl AsRef<str>) -> Command;
}

#[derive(Debug)]
struct CommandError {
title: String,
command: String,
output: String,
source: Option<io::Error>,
}

impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// title
// command
// output
let title = if self.title.is_empty() {
"CommandError:"
} else {
self.title.as_str()
};
write!(f, "{}\n {}\n", title, &self.command)?;
for line in self.output.lines() {
write!(f, " {line}\n")?;
}
Ok(())
}
}

impl Error for CommandError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|s| s as &dyn Error)
}
}

fn os_str_to_naive_quoted_str(s: &OsStr) -> String {
let debug_format = format!("{:?}", s);
if debug_format.len() == s.len() + 2
&& debug_format.split_ascii_whitespace().take(2).count() == 1
{
debug_format[1..debug_format.len() - 1].to_string()
} else {
debug_format
}
}

impl CommandError {
fn new(command: &Command, source: Option<io::Error>) -> Self {
let arg0 = os_str_to_naive_quoted_str(command.get_program());
let args = command
.get_args()
.map(os_str_to_naive_quoted_str)
.collect::<Vec<String>>()
.join(" ");
let command = format!("{arg0} {args}");
Self {
title: Default::default(),
output: Default::default(),
command,
source,
}
}

fn with_output(mut self, output: &Output) -> Self {
for out in [&output.stdout, &output.stderr] {
self.output.push_str(&String::from_utf8_lossy(out));
}
self.with_status(&output.status)
}

fn with_status(mut self, exit: &ExitStatus) -> Self {
match exit.code() {
None =>
{
#[cfg(unix)]
match std::os::unix::process::ExitStatusExt::signal(exit) {
Some(sig) => self.title = format!("Command terminated by signal {}", sig),
None => {}
}
}
Some(code) => {
if code != 0 {
self.title = format!("Command exited with code {}", code);
}
}
}
self
}

fn into_io_error(self: CommandError) -> io::Error {
let kind = match self.source.as_ref() {
None => io::ErrorKind::Other,
Some(e) => e.kind(),
};
io::Error::new(kind, self)
}
}

impl CommandExt for Command {
fn avoid_inherit_handles(&mut self) -> &mut Self {
#[cfg(unix)]
Expand Down Expand Up @@ -67,6 +173,30 @@ impl CommandExt for Command {
.spawn()
}

fn checked_output(&mut self) -> io::Result<Output> {
let out = self
.output()
.map_err(|e| CommandError::new(self, Some(e)).into_io_error())?;
if !out.status.success() {
return Err(CommandError::new(self, None)
.with_output(&out)
.into_io_error());
}
Ok(out)
}

fn checked_run(&mut self) -> io::Result<ExitStatus> {
let status = self
.status()
.map_err(|e| CommandError::new(self, Some(e)).into_io_error())?;
if !status.success() {
return Err(CommandError::new(self, None)
.with_status(&status)
.into_io_error());
}
Ok(status)
}

fn new_shell(shell_cmd: impl AsRef<str>) -> Command {
#[cfg(unix)]
return unix::new_shell(shell_cmd);
Expand Down

0 comments on commit bc6f7a6

Please sign in to comment.