use super::exit::Ending;
use crate::activity::ActivityFailure;
use crate::process::ProcessGroupError;
#[derive(Debug, Clone, Copy)]
pub(super) struct BodySite<'a> {
pub(super) command: &'a str,
pub(super) line: usize,
pub(super) total: usize,
}
impl BodySite<'_> {
fn subject(self, program: &str) -> String {
let Self {
command,
line,
total,
} = self;
format!("command `{command}`'s line {line} of {total} (`{program}`)")
}
fn subject_of(site: Option<Self>, program: &str) -> String {
site.map_or_else(
|| format!("the declared command `{program}`"),
|site| site.subject(program),
)
}
}
pub(super) fn spawn_failure(
program: &str,
site: Option<BodySite<'_>>,
error: &ProcessGroupError,
) -> ActivityFailure {
let sentence = format!(
"{subject} could not be run to completion: {error}",
subject = BodySite::subject_of(site, program)
);
if spawn_failure_permits_retry(error) {
ActivityFailure::retryable(sentence)
} else {
ActivityFailure::terminal(sentence)
}
}
#[must_use]
pub fn spawn_failure_permits_retry(error: &ProcessGroupError) -> bool {
match error {
ProcessGroupError::Spawn { source } => spawn_permits_retry_by_errno(source),
_ => false,
}
}
fn spawn_permits_retry_by_errno(error: &std::io::Error) -> bool {
const EPERM: i32 = 1;
const ENOENT: i32 = 2;
const ENOEXEC: i32 = 8;
const EACCES: i32 = 13;
const EISDIR: i32 = 21;
if matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied
) {
return false;
}
!matches!(
error.raw_os_error(),
Some(EPERM | ENOENT | ENOEXEC | EACCES | EISDIR)
)
}
pub(super) const fn ending_permits_retry(ending: Ending) -> bool {
ending.reported_code().is_some()
}
pub(super) fn unreadable_ending_clause(ending: Ending) -> &'static str {
if ending_permits_retry(ending) {
""
} else {
", so this host cannot say whether it did its work and will not run it again on its own"
}
}
#[cfg(test)]
mod tests {
use super::{
BodySite, ending_permits_retry, spawn_failure, spawn_failure_permits_retry,
unreadable_ending_clause,
};
use crate::activity::Classification;
use crate::process::ProcessGroupError;
use crate::shell::exit::Ending;
fn observation_error() -> ProcessGroupError {
ProcessGroupError::Spawn {
source: std::io::Error::from(std::io::ErrorKind::NotFound),
}
}
#[test]
fn a_spawn_failure_without_a_body_names_the_program() {
let failure = spawn_failure("nope", None, &observation_error());
assert_eq!(failure.classification(), &Classification::Terminal);
assert!(
failure
.message()
.starts_with("the declared command `nope` could not be run to completion:"),
"{}",
failure.message()
);
}
#[test]
fn a_spawn_failure_inside_a_body_names_the_command_and_which_line() {
let failure = spawn_failure(
"nope",
Some(BodySite {
command: "release",
line: 2,
total: 5,
}),
&observation_error(),
);
assert_eq!(failure.classification(), &Classification::Terminal);
assert!(
failure.message().starts_with(
"command `release`'s line 2 of 5 (`nope`) could not be run to completion:"
),
"{}",
failure.message()
);
}
#[test]
fn only_an_ending_this_host_can_read_permits_a_retry() {
assert!(ending_permits_retry(Ending::Exited(3)));
assert!(ending_permits_retry(Ending::Signalled(9)));
assert!(!ending_permits_retry(Ending::Unstated));
assert_eq!(unreadable_ending_clause(Ending::Exited(3)), "");
assert_eq!(unreadable_ending_clause(Ending::Signalled(9)), "");
assert!(
unreadable_ending_clause(Ending::Unstated)
.contains("cannot say whether it did its work")
);
}
#[test]
fn an_exhausted_descriptor_table_is_retryable() {
let error = spawn(24);
assert!(spawn_failure_permits_retry(&error));
assert!(
matches!(
spawn_failure("prog", None, &error).classification(),
Classification::Retryable
),
"resource pressure must not permanently fail a durable step"
);
}
#[test]
fn an_absent_or_unrunnable_program_stays_terminal() {
for errno in [1, 2, 8, 13, 21] {
let error = spawn(errno);
assert!(
!spawn_failure_permits_retry(&error),
"errno {errno} names the program itself as wrong and must stay terminal"
);
assert!(!matches!(
spawn_failure("prog", None, &error).classification(),
Classification::Retryable
));
}
}
#[test]
fn an_unenumerated_spawn_errno_retries() {
for errno in [4, 5, 16, 20, 28] {
let error = spawn(errno);
assert!(
spawn_failure_permits_retry(&error),
"errno {errno} is the world being transient, not the program being wrong"
);
}
}
#[test]
fn a_fork_refused_under_pressure_is_retryable_on_this_platform() {
let eagain = if cfg!(target_os = "macos") { 35 } else { 11 };
let error = spawn(eagain);
assert_eq!(error_kind_of(&error), std::io::ErrorKind::WouldBlock);
assert!(spawn_failure_permits_retry(&error));
}
#[test]
fn a_spawned_child_this_host_could_not_observe_stays_terminal() {
for error in [
ProcessGroupError::Read {
stream: "stdout",
source: std::io::Error::from_raw_os_error(24),
},
ProcessGroupError::Reap {
source: std::io::Error::from_raw_os_error(24),
},
] {
assert!(
!spawn_failure_permits_retry(&error),
"a spawned process must not be re-run on this host's own initiative"
);
}
assert!(spawn_failure_permits_retry(&spawn(24)));
}
fn spawn(errno: i32) -> ProcessGroupError {
ProcessGroupError::Spawn {
source: std::io::Error::from_raw_os_error(errno),
}
}
fn error_kind_of(error: &ProcessGroupError) -> std::io::ErrorKind {
match error {
ProcessGroupError::Spawn { source } | ProcessGroupError::Read { source, .. } => {
source.kind()
}
_ => std::io::ErrorKind::Other,
}
}
}