use crate::{ExitStatus, Handle, Output, OutputStream};
use std::{
error::Error as StdError,
fmt::{self, Display},
io, process, str,
};
#[derive(Debug)]
pub enum Cause {
SpawnFailed(io::Error),
WaitFailed(io::Error),
CommandFailed(ExitStatus),
CommandFailedWithOutput(Output),
InvalidUtf8 {
stream: OutputStream,
source: std::str::Utf8Error,
},
}
impl Cause {
fn from_io_err(err: io::Error) -> Self {
Self::WaitFailed(err)
}
fn from_status(status: process::ExitStatus) -> Result<ExitStatus, Self> {
if status.success() {
Ok(status)
} else {
Err(Self::CommandFailed(status))
}
}
fn from_output(command: String, output: process::Output) -> Result<Output, Self> {
let output = Output::new(command, output);
if output.success() {
Ok(output)
} else {
Err(Self::CommandFailedWithOutput(output))
}
}
fn status(&self) -> Option<ExitStatus> {
if let Self::CommandFailed(status) = self {
Some(*status)
} else {
self.output().map(|output| output.status())
}
}
fn output(&self) -> Option<&Output> {
if let Self::CommandFailedWithOutput(output) = self {
Some(output)
} else {
None
}
}
}
#[derive(Debug)]
pub struct Error {
command: String,
cause: Cause,
}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn command_failed(
f: &mut fmt::Formatter,
command: &str,
status: ExitStatus,
) -> fmt::Result {
write!(f, "Command {command:?} didn't complete successfully, ")?;
if let Some(exit_code) = status.code() {
write!(f, "exiting with code {exit_code}.")
} else {
write!(f, "but returned no exit code.")
}
}
match &self.cause {
Cause::SpawnFailed(err) => write!(
f,
"Failed to spawn child process for command {:?}: {err}",
self.command
),
Cause::WaitFailed(err) => write!(
f,
"Failed to wait for child process for command {:?} to exit: {err}",
self.command
),
Cause::CommandFailed(status) => command_failed(f, &self.command, *status),
Cause::CommandFailedWithOutput(output) => {
command_failed(f, &self.command, output.status())?;
if !output.stderr().is_empty() {
write!(
f,
" stderr contents: {}",
String::from_utf8_lossy(output.stderr())
)
} else {
write!(f, " stderr was empty.")
}
}
Cause::InvalidUtf8 { stream, source, .. } => write!(
f,
"{stream} for command {:?} contained invalid UTF-8: {source}",
self.command
),
}
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match &self.cause {
Cause::SpawnFailed(err) => Some(err as _),
Cause::WaitFailed(err) => Some(err as _),
Cause::InvalidUtf8 { source, .. } => Some(source as _),
_ => None,
}
}
}
impl Error {
pub(crate) fn from_status_result(
command: String,
result: io::Result<process::ExitStatus>,
) -> Result<ExitStatus, Self> {
result
.map_err(Cause::from_io_err)
.and_then(Cause::from_status)
.map_err(|cause| Self { command, cause })
}
pub(crate) fn from_output_result(
command: String,
result: io::Result<process::Output>,
) -> Result<Output, Self> {
result
.map_err(Cause::from_io_err)
.and_then(|output| Cause::from_output(command.clone(), output))
.map_err(|cause| Self { command, cause })
}
pub(crate) fn from_child_result(
command: String,
result: io::Result<process::Child>,
) -> Result<Handle, Self> {
match result {
Ok(child) => Ok(Handle::new(command, child)),
Err(err) => Err(Self {
command,
cause: Cause::from_io_err(err),
}),
}
}
pub(crate) fn from_utf8_result<'a>(
command: &str,
stream: OutputStream,
result: Result<&'a str, std::str::Utf8Error>,
) -> Result<&'a str, Self> {
result.map_err(|source| Self {
command: command.to_owned(),
cause: Cause::InvalidUtf8 { stream, source },
})
}
pub fn command(&self) -> &str {
&self.command
}
pub fn status(&self) -> Option<ExitStatus> {
self.cause.status()
}
pub fn code(&self) -> Option<i32> {
self.status().and_then(|status| status.code())
}
pub fn output(&self) -> Option<&Output> {
self.cause.output()
}
pub fn stdout(&self) -> Option<&[u8]> {
self.output().map(|output| output.stdout())
}
pub fn stdout_str(&self) -> Option<crate::Result<&str>> {
self.output().map(|output| output.stdout_str())
}
pub fn stderr(&self) -> Option<&[u8]> {
self.output().map(|output| output.stderr())
}
pub fn stderr_str(&self) -> Option<crate::Result<&str>> {
self.output().map(|output| output.stderr_str())
}
}