rightkit_child_process/run/
failure.rs1use std::{error::Error, fmt, io, time::Duration};
2
3#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum LeaderTermination {
21 NotStarted,
22 Confirmed,
23 Unconfirmed,
24}
25
26#[derive(Debug)]
28pub struct RunnerFailure {
29 phase: RunnerPhase,
30 termination: LeaderTermination,
31 source: io::Error,
32}
33
34impl RunnerFailure {
35 pub fn from_io(error: &io::Error) -> Option<&Self> {
37 error.get_ref()?.downcast_ref()
38 }
39
40 pub fn phase(&self) -> RunnerPhase {
42 self.phase
43 }
44
45 pub fn termination(&self) -> LeaderTermination {
47 self.termination
48 }
49
50 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
98pub(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}