1use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6use std::io;
7use std::process::ExitStatus;
8use std::sync::Arc;
9use std::time::Duration;
10use thiserror::Error;
11
12use crate::ConsumerError;
13
14#[derive(Debug, Error)]
28#[non_exhaustive]
29pub enum TerminationError {
30 #[error(
32 "Failed to send signal to process '{process_name}'.{}",
33 DisplayAttemptErrors(.attempt_errors.as_slice())
34 )]
35 SignalFailed {
36 process_name: Cow<'static, str>,
38 attempt_errors: Vec<TerminationAttemptError>,
40 },
41
42 #[error(
44 "Failed to terminate process '{process_name}'.{}",
45 DisplayAttemptErrors(.attempt_errors.as_slice())
46 )]
47 TerminationFailed {
48 process_name: Cow<'static, str>,
50 attempt_errors: Vec<TerminationAttemptError>,
52 },
53}
54
55impl TerminationError {
56 #[must_use]
58 pub fn process_name(&self) -> &str {
59 match self {
60 Self::SignalFailed { process_name, .. }
61 | Self::TerminationFailed { process_name, .. } => process_name,
62 }
63 }
64
65 #[must_use]
67 pub fn attempt_errors(&self) -> &[TerminationAttemptError] {
68 match self {
69 Self::SignalFailed { attempt_errors, .. }
70 | Self::TerminationFailed { attempt_errors, .. } => attempt_errors,
71 }
72 }
73}
74
75struct DisplayAttemptErrors<'a>(&'a [TerminationAttemptError]);
76
77impl fmt::Display for DisplayAttemptErrors<'_> {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 if self.0.is_empty() {
80 return write!(f, " No attempt error was recorded.");
81 }
82
83 write!(f, " Attempt errors:")?;
84 for (index, attempt_error) in self.0.iter().enumerate() {
85 write!(f, " [{}] {attempt_error}", index + 1)?;
86 }
87
88 Ok(())
89 }
90}
91
92#[derive(Debug, Error)]
94#[error("{action} failed")]
95#[non_exhaustive]
96pub struct TerminationAttemptError {
97 pub action: TerminationAction,
99 #[source]
101 pub source: Box<dyn Error + Send + Sync + 'static>,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum TerminationAction {
108 CheckStatus,
110
111 SendSignal {
113 signal_name: &'static str,
115 },
116
117 WaitForExit,
119}
120
121impl fmt::Display for TerminationAction {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 Self::CheckStatus => f.write_str("status check"),
125 Self::SendSignal { signal_name } => write!(f, "send signal {signal_name}"),
126 Self::WaitForExit => f.write_str("exit wait"),
127 }
128 }
129}
130
131#[derive(Debug, Error)]
133#[non_exhaustive]
134pub enum WaitError {
135 #[error("IO error occurred while waiting for process '{process_name}'")]
137 IoError {
138 process_name: Cow<'static, str>,
140 #[source]
142 source: io::Error,
143 },
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[must_use = "Discarding the result hides whether the process completed or the wait timed out; \
149 match on the variants or call `into_completed`/`expect_completed`."]
150pub enum WaitForCompletionResult<T = ExitStatus> {
151 Completed(T),
153
154 Timeout {
156 timeout: Duration,
158 },
159}
160
161impl<T> WaitForCompletionResult<T> {
162 #[must_use]
164 pub fn into_completed(self) -> Option<T> {
165 match self {
166 Self::Completed(value) => Some(value),
167 Self::Timeout { .. } => None,
168 }
169 }
170
171 pub fn expect_completed(self, message: &str) -> T {
177 self.into_completed().expect(message)
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183#[must_use = "Discarding the result hides whether the process completed naturally or was \
184 terminated after the wait timeout; match on the variants or call `into_result`."]
185pub enum WaitForCompletionOrTerminateResult<T = ExitStatus> {
186 Completed(T),
188
189 TerminatedAfterTimeout {
191 result: T,
193 timeout: Duration,
195 },
196}
197
198impl<T> WaitForCompletionOrTerminateResult<T> {
199 #[must_use]
201 pub fn into_result(self) -> T {
202 match self {
203 Self::Completed(value) | Self::TerminatedAfterTimeout { result: value, .. } => value,
204 }
205 }
206
207 #[must_use]
209 pub fn into_completed(self) -> Option<T> {
210 match self {
211 Self::Completed(value) => Some(value),
212 Self::TerminatedAfterTimeout { .. } => None,
213 }
214 }
215
216 pub fn expect_completed(self, message: &str) -> T {
223 self.into_completed().expect(message)
224 }
225
226 pub(crate) fn map<U>(self, f: impl FnOnce(T) -> U) -> WaitForCompletionOrTerminateResult<U> {
228 match self {
229 Self::Completed(value) => WaitForCompletionOrTerminateResult::Completed(f(value)),
230 Self::TerminatedAfterTimeout { result, timeout } => {
231 WaitForCompletionOrTerminateResult::TerminatedAfterTimeout {
232 result: f(result),
233 timeout,
234 }
235 }
236 }
237 }
238}
239
240#[derive(Debug, Error)]
242#[non_exhaustive]
243pub enum WaitOrTerminateError {
244 #[error(
246 "Waiting for process '{process_name}' failed; cleanup termination completed with status {termination_status}"
247 )]
248 WaitFailed {
249 process_name: Cow<'static, str>,
251 #[source]
253 wait_error: Box<WaitError>,
254 termination_status: ExitStatus,
256 },
257
258 #[error("Termination of process '{process_name}' failed after '{wait_error}'.")]
260 TerminationFailed {
261 process_name: Cow<'static, str>,
263 wait_error: Box<WaitError>,
265 #[source]
267 termination_error: TerminationError,
268 },
269
270 #[error(
272 "Process '{process_name}' did not complete within {timeout:?}; cleanup termination failed"
273 )]
274 TerminationAfterTimeoutFailed {
275 process_name: Cow<'static, str>,
277 timeout: Duration,
279 #[source]
281 termination_error: TerminationError,
282 },
283}
284
285#[derive(Debug, Error)]
292#[non_exhaustive]
293pub enum WaitWithOutputError {
294 #[error("Waiting for process completion failed")]
296 WaitFailed(#[from] WaitError),
297
298 #[error("Wait-or-terminate operation failed")]
300 WaitOrTerminateFailed(#[from] WaitOrTerminateError),
301
302 #[error("Output collection for process '{process_name}' did not complete within {timeout:?}")]
304 OutputCollectionTimeout {
305 process_name: Cow<'static, str>,
307 timeout: Duration,
309 },
310
311 #[error("Output collection for process '{process_name}' failed")]
313 OutputCollectionFailed {
314 process_name: Cow<'static, str>,
316 #[source]
318 source: ConsumerError,
319 },
320
321 #[error("Output collection for process '{process_name}' could not start")]
323 OutputCollectionStartFailed {
324 process_name: Cow<'static, str>,
326 #[source]
328 source: Box<dyn Error + Send + Sync + 'static>,
329 },
330}
331
332#[derive(Debug, Error)]
334#[non_exhaustive]
335pub enum SpawnError {
336 #[error("Failed to spawn process '{process_name}'")]
338 SpawnFailed {
339 process_name: Cow<'static, str>,
341 #[source]
343 source: io::Error,
344 },
345
346 #[cfg(windows)]
356 #[error("Failed to attach spawned process '{process_name}' to a Windows Job Object")]
357 JobAttachmentFailed {
358 process_name: Cow<'static, str>,
360 #[source]
363 source: io::Error,
364 },
365}
366
367#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
369#[non_exhaustive]
370pub enum StreamConsumerError {
371 #[error("Stream '{stream_name}' already has an active consumer")]
373 ActiveConsumer {
374 stream_name: &'static str,
376 },
377}
378
379impl StreamConsumerError {
380 #[must_use]
382 pub fn stream_name(&self) -> &'static str {
383 match self {
384 Self::ActiveConsumer { stream_name } => stream_name,
385 }
386 }
387}
388
389#[derive(Debug, Clone, Error)]
391#[error("Could not read from stream '{stream_name}'")]
392pub struct StreamReadError {
393 stream_name: &'static str,
394 #[source]
395 source: Arc<io::Error>,
396}
397
398impl StreamReadError {
399 #[must_use]
401 pub fn new(stream_name: &'static str, source: io::Error) -> Self {
402 Self {
403 stream_name,
404 source: Arc::new(source),
405 }
406 }
407
408 #[must_use]
410 pub fn stream_name(&self) -> &'static str {
411 self.stream_name
412 }
413
414 #[must_use]
416 pub fn kind(&self) -> io::ErrorKind {
417 self.source.kind()
418 }
419
420 #[must_use]
422 pub fn source_io_error(&self) -> &io::Error {
423 self.source.as_ref()
424 }
425}
426
427impl PartialEq for StreamReadError {
428 fn eq(&self, other: &Self) -> bool {
429 self.stream_name == other.stream_name && self.kind() == other.kind()
430 }
431}
432
433impl Eq for StreamReadError {}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
440pub enum WaitForLineResult {
441 Matched,
443
444 StreamClosed,
446
447 Timeout,
449}
450
451#[cfg(test)]
452pub(crate) mod tests {
453 use super::*;
454 use assertr::prelude::*;
455
456 pub(crate) fn assert_attempt_error(
457 attempt_error: &TerminationAttemptError,
458 expected_action: TerminationAction,
459 expected_kind: io::ErrorKind,
460 expected_message: &str,
461 ) {
462 assert_that!(attempt_error.action).is_equal_to(expected_action);
463
464 let io_error = attempt_error
465 .source
466 .downcast_ref::<io::Error>()
467 .expect("diagnostic should preserve the original io::Error");
468
469 assert_that!(io_error.kind()).is_equal_to(expected_kind);
470 assert_that!(io_error.to_string().as_str()).contains(expected_message);
471 }
472}