Skip to main content

rightkit_child_process/run/
failure.rs

1use std::{error::Error, fmt, io, time::Duration};
2
3/// Operation that failed. `Spawn` means no child was created; `Setup` may follow creation.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum RunnerPhase {
7    Spawn,
8    Setup,
9    Wait,
10    Terminate,
11    Stdout,
12    Stderr,
13    Stdin,
14    Drain,
15    Cleanup,
16}
17
18/// Evidence about the leader only; this does not certify escaped descendants or pipe EOF.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum LeaderTermination {
21    NotStarted,
22    Confirmed,
23    Unconfirmed,
24}
25
26/// Structured runner failure retained inside `io::Error`, with its original source intact.
27#[derive(Debug)]
28pub struct RunnerFailure {
29    phase: RunnerPhase,
30    termination: LeaderTermination,
31    source: io::Error,
32}
33
34impl RunnerFailure {
35    /// Downcast runner provenance from the outer I/O error; terminal pass-through has none.
36    pub fn from_io(error: &io::Error) -> Option<&Self> {
37        error.get_ref()?.downcast_ref()
38    }
39
40    /// Operation that failed; no message parsing is needed.
41    pub fn phase(&self) -> RunnerPhase {
42        self.phase
43    }
44
45    /// Evidence about the leader only, independent of escaped descendants or pipe EOF.
46    pub fn termination(&self) -> LeaderTermination {
47        self.termination
48    }
49
50    /// Original I/O error, including its OS code when one exists. The outer error is a wrapper.
51    pub fn source_io_error(&self) -> &io::Error {
52        &self.source
53    }
54}
55
56impl fmt::Display for RunnerFailure {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self.phase {
59            RunnerPhase::Stdout => write!(
60                f,
61                "stdout capture failed, output is incomplete: {}",
62                self.source
63            ),
64            RunnerPhase::Stderr => write!(
65                f,
66                "stderr capture failed, output is incomplete: {}",
67                self.source
68            ),
69            _ => fmt::Display::fmt(&self.source, f),
70        }
71    }
72}
73
74impl Error for RunnerFailure {
75    fn source(&self) -> Option<&(dyn Error + 'static)> {
76        Some(&self.source)
77    }
78}
79
80pub(crate) fn failure(
81    phase: RunnerPhase,
82    termination: LeaderTermination,
83    source: io::Error,
84) -> io::Error {
85    if RunnerFailure::from_io(&source).is_some() {
86        return source;
87    }
88    io::Error::new(
89        source.kind(),
90        RunnerFailure {
91            phase,
92            termination,
93            source,
94        },
95    )
96}
97
98/// Cleanup after an operation failed while a leader might still be running. Never infer
99/// termination from the original error or rely on Drop's best-effort cleanup as proof.
100pub(crate) fn stop_after_failure(
101    child: &mut crate::OwnedChild,
102    phase: RunnerPhase,
103    source: io::Error,
104) -> io::Error {
105    let termination = match child.terminate_tree_bounded(Duration::from_secs(5)) {
106        Ok(crate::ReapOutcome::Exited(_)) => LeaderTermination::Confirmed,
107        _ => LeaderTermination::Unconfirmed,
108    };
109    failure(
110        if termination == LeaderTermination::Unconfirmed {
111            RunnerPhase::Cleanup
112        } else {
113            phase
114        },
115        termination,
116        source,
117    )
118}
119
120pub(crate) fn terminate_runner(
121    child: &mut crate::OwnedChild,
122    timeout: Duration,
123) -> io::Result<std::process::ExitStatus> {
124    match child.terminate_tree_bounded(timeout) {
125        Ok(crate::ReapOutcome::Exited(status)) => Ok(status),
126        Ok(crate::ReapOutcome::TimedOut) => Err(failure(
127            RunnerPhase::Cleanup,
128            LeaderTermination::Unconfirmed,
129            io::Error::new(
130                io::ErrorKind::TimedOut,
131                "the terminated process was not reaped within its bound",
132            ),
133        )),
134        Err(error) => Err(stop_after_failure(child, RunnerPhase::Terminate, error)),
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn partial_spawn_is_never_reclassified_as_not_started() {
144        let error = crate::owned::spawn_setup_failure(io::Error::from_raw_os_error(5));
145        let error = failure(RunnerPhase::Spawn, LeaderTermination::NotStarted, error);
146        let detail = RunnerFailure::from_io(&error).unwrap();
147        assert_eq!(detail.phase(), RunnerPhase::Setup);
148        assert_eq!(detail.termination(), LeaderTermination::Unconfirmed);
149        assert_eq!(detail.source_io_error().raw_os_error(), Some(5));
150        assert!(error
151            .get_ref()
152            .unwrap()
153            .downcast_ref::<RunnerFailure>()
154            .is_some());
155    }
156
157    #[test]
158    fn confirmed_read_failure_retains_original_os_error() {
159        let error = failure(
160            RunnerPhase::Stdout,
161            LeaderTermination::Confirmed,
162            io::Error::from_raw_os_error(5),
163        );
164        let detail = RunnerFailure::from_io(&error).unwrap();
165        assert_eq!(detail.phase(), RunnerPhase::Stdout);
166        assert_eq!(detail.termination(), LeaderTermination::Confirmed);
167        assert_eq!(detail.source_io_error().raw_os_error(), Some(5));
168    }
169}