Skip to main content

tokio_process_tools/
error.rs

1//! Error types for process operations.
2
3use 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/// Errors that can occur when terminating a process.
15#[derive(Debug, Error)]
16#[non_exhaustive]
17pub enum TerminationError {
18    /// Failed to manually send a graceful signal to the process.
19    #[error(
20        "Failed to send signal to process '{process_name}'.{}",
21        DisplayAttemptErrors(.attempt_errors.as_slice())
22    )]
23    SignalFailed {
24        /// The name of the process.
25        process_name: Cow<'static, str>,
26        /// Errors recorded while attempting to send the signal, in chronological order.
27        attempt_errors: Vec<TerminationAttemptError>,
28    },
29
30    /// Failed to terminate the process after trying all platform termination signals.
31    #[error(
32        "Failed to terminate process '{process_name}'.{}",
33        DisplayAttemptErrors(.attempt_errors.as_slice())
34    )]
35    TerminationFailed {
36        /// The name of the process.
37        process_name: Cow<'static, str>,
38        /// Errors recorded while attempting process termination, in chronological order.
39        attempt_errors: Vec<TerminationAttemptError>,
40    },
41}
42
43impl TerminationError {
44    /// The name of the process involved in the termination error.
45    #[must_use]
46    pub fn process_name(&self) -> &str {
47        match self {
48            Self::SignalFailed { process_name, .. }
49            | Self::TerminationFailed { process_name, .. } => process_name,
50        }
51    }
52
53    /// Errors recorded while attempting the operation, in chronological order.
54    #[must_use]
55    pub fn attempt_errors(&self) -> &[TerminationAttemptError] {
56        match self {
57            Self::SignalFailed { attempt_errors, .. }
58            | Self::TerminationFailed { attempt_errors, .. } => attempt_errors,
59        }
60    }
61}
62
63struct DisplayAttemptErrors<'a>(&'a [TerminationAttemptError]);
64
65impl fmt::Display for DisplayAttemptErrors<'_> {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        if self.0.is_empty() {
68            return write!(f, " No attempt error was recorded.");
69        }
70
71        write!(f, " Attempt errors:")?;
72        for (index, attempt_error) in self.0.iter().enumerate() {
73            write!(f, " [{}] {attempt_error}", index + 1)?;
74        }
75
76        Ok(())
77    }
78}
79
80struct DisplaySignalNameSuffix(Option<&'static str>);
81
82impl fmt::Display for DisplaySignalNameSuffix {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        let Some(signal_name) = self.0 else {
85            return Ok(());
86        };
87
88        write!(f, " for {signal_name}")
89    }
90}
91
92/// A failed operation recorded while attempting to terminate a process.
93#[derive(Debug, Error)]
94#[error(
95    "{phase} {operation} failed{}: {source}",
96    DisplaySignalNameSuffix(*.signal_name)
97)]
98#[non_exhaustive]
99pub struct TerminationAttemptError {
100    /// Termination phase where the failure happened.
101    pub phase: TerminationAttemptPhase,
102    /// Operation that failed during the phase.
103    pub operation: TerminationAttemptOperation,
104    /// Platform signal involved in the failed operation, when applicable.
105    pub signal_name: Option<&'static str>,
106    /// Original source error.
107    #[source]
108    pub source: Box<dyn Error + Send + Sync + 'static>,
109}
110
111/// Termination phase where an attempt error was recorded.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113#[non_exhaustive]
114pub enum TerminationAttemptPhase {
115    /// Initial process status check before any termination signal is sent.
116    Preflight,
117    /// Graceful interrupt phase.
118    Interrupt,
119    /// Graceful terminate phase.
120    Terminate,
121    /// Forceful kill phase.
122    Kill,
123}
124
125impl fmt::Display for TerminationAttemptPhase {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self {
128            Self::Preflight => f.write_str("preflight"),
129            Self::Interrupt => f.write_str("interrupt"),
130            Self::Terminate => f.write_str("terminate"),
131            Self::Kill => f.write_str("kill"),
132        }
133    }
134}
135
136/// Termination operation that failed.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138#[non_exhaustive]
139pub enum TerminationAttemptOperation {
140    /// Checking whether the process has already exited failed.
141    CheckStatus,
142    /// Sending a graceful or forceful termination signal failed.
143    SendSignal,
144    /// Waiting for the process to exit after a termination signal failed.
145    WaitForExit,
146}
147
148impl fmt::Display for TerminationAttemptOperation {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::CheckStatus => f.write_str("status check"),
152            Self::SendSignal => f.write_str("signal send"),
153            Self::WaitForExit => f.write_str("exit wait"),
154        }
155    }
156}
157
158/// Errors that can occur when waiting for process operations.
159#[derive(Debug, Error)]
160#[non_exhaustive]
161pub enum WaitError {
162    /// A general IO error occurred.
163    #[error("IO error occurred while waiting for process '{process_name}': {source}")]
164    IoError {
165        /// The name of the process.
166        process_name: Cow<'static, str>,
167        /// The underlying IO error.
168        #[source]
169        source: io::Error,
170    },
171}
172
173/// Result of waiting for a process to complete within an explicit timeout.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum WaitForCompletionResult<T = ExitStatus> {
176    /// The process completed before the timeout elapsed.
177    Completed(T),
178
179    /// The timeout elapsed before the process completed.
180    Timeout {
181        /// The timeout duration that was exceeded.
182        timeout: Duration,
183    },
184}
185
186impl<T> WaitForCompletionResult<T> {
187    /// Returns the completed value, or `None` if the wait timed out.
188    #[must_use]
189    pub fn into_completed(self) -> Option<T> {
190        match self {
191            Self::Completed(value) => Some(value),
192            Self::Timeout { .. } => None,
193        }
194    }
195
196    /// Returns the completed value, panicking with `message` if the wait timed out.
197    ///
198    /// # Panics
199    ///
200    /// Panics with `message` if this result is [`WaitForCompletionResult::Timeout`].
201    pub fn expect_completed(self, message: &str) -> T {
202        self.into_completed().expect(message)
203    }
204
205    /// Maps a completed value while preserving timeout outcomes.
206    pub(crate) fn map<U>(self, f: impl FnOnce(T) -> U) -> WaitForCompletionResult<U> {
207        match self {
208            Self::Completed(value) => WaitForCompletionResult::Completed(f(value)),
209            Self::Timeout { timeout } => WaitForCompletionResult::Timeout { timeout },
210        }
211    }
212}
213
214/// Result of waiting for a process to complete, terminating it if the wait times out.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum WaitForCompletionOrTerminateResult<T = ExitStatus> {
217    /// The process completed before the wait timeout elapsed.
218    Completed(T),
219
220    /// The wait timeout elapsed, then cleanup termination completed successfully.
221    TerminatedAfterTimeout {
222        /// The result observed after cleanup termination.
223        result: T,
224        /// The wait timeout duration that was exceeded before cleanup began.
225        timeout: Duration,
226    },
227}
228
229impl<T> WaitForCompletionOrTerminateResult<T> {
230    /// Returns the terminal value, whether completion happened before timeout or after cleanup.
231    #[must_use]
232    pub fn into_result(self) -> T {
233        match self {
234            Self::Completed(value) | Self::TerminatedAfterTimeout { result: value, .. } => value,
235        }
236    }
237
238    /// Returns the completed value, or `None` if cleanup termination was required after timeout.
239    #[must_use]
240    pub fn into_completed(self) -> Option<T> {
241        match self {
242            Self::Completed(value) => Some(value),
243            Self::TerminatedAfterTimeout { .. } => None,
244        }
245    }
246
247    /// Returns the completed value, panicking with `message` if cleanup termination was required.
248    ///
249    /// # Panics
250    ///
251    /// Panics with `message` if this result is
252    /// [`WaitForCompletionOrTerminateResult::TerminatedAfterTimeout`].
253    pub fn expect_completed(self, message: &str) -> T {
254        self.into_completed().expect(message)
255    }
256
257    /// Maps a terminal value while preserving the timeout/cleanup outcome.
258    pub(crate) fn map<U>(self, f: impl FnOnce(T) -> U) -> WaitForCompletionOrTerminateResult<U> {
259        match self {
260            Self::Completed(value) => WaitForCompletionOrTerminateResult::Completed(f(value)),
261            Self::TerminatedAfterTimeout { result, timeout } => {
262                WaitForCompletionOrTerminateResult::TerminatedAfterTimeout {
263                    result: f(result),
264                    timeout,
265                }
266            }
267        }
268    }
269}
270
271/// Errors that can occur when waiting for a process with automatic termination on failure.
272#[derive(Debug, Error)]
273#[non_exhaustive]
274pub enum WaitOrTerminateError {
275    /// Waiting failed, but the subsequent cleanup termination succeeded.
276    #[error(
277        "Waiting for process '{process_name}' failed with '{wait_error}', then cleanup termination completed with status {termination_status}"
278    )]
279    WaitFailed {
280        /// The name of the process.
281        process_name: Cow<'static, str>,
282        /// The original error returned while waiting for the process.
283        #[source]
284        wait_error: Box<WaitError>,
285        /// The status observed after cleanup termination.
286        termination_status: ExitStatus,
287    },
288
289    /// Waiting failed, and the subsequent cleanup termination also failed.
290    #[error(
291        "Waiting for process '{process_name}' failed with '{wait_error}', then cleanup termination also failed: {termination_error}"
292    )]
293    TerminationFailed {
294        /// The name of the process.
295        process_name: Cow<'static, str>,
296        /// The original error returned while waiting for the process.
297        #[source]
298        wait_error: Box<WaitError>,
299        /// The error returned while trying to terminate the process after the wait failure.
300        termination_error: TerminationError,
301    },
302
303    /// Waiting timed out, and the subsequent cleanup termination failed.
304    #[error(
305        "Process '{process_name}' did not complete within {timeout:?}, then cleanup termination failed: {termination_error}"
306    )]
307    TerminationAfterTimeoutFailed {
308        /// The name of the process.
309        process_name: Cow<'static, str>,
310        /// The wait timeout duration that was exceeded before cleanup began.
311        timeout: Duration,
312        /// The error returned while trying to terminate the process after the timeout.
313        #[source]
314        termination_error: TerminationError,
315    },
316}
317
318/// Errors that can occur when waiting for a process while collecting its output, with or
319/// without automatic termination on timeout.
320///
321/// `WaitFailed` is emitted by APIs without automatic termination
322/// (`wait_for_completion_with_output`, `wait_for_completion_with_raw_output`).
323/// `WaitOrTerminateFailed` is emitted by the `*_or_terminate` variants. The remaining variants
324/// can be emitted by either family.
325#[derive(Debug, Error)]
326#[non_exhaustive]
327pub enum WaitWithOutputError {
328    /// Waiting for the process failed.
329    #[error("Waiting for process completion failed: {0}")]
330    WaitFailed(#[from] WaitError),
331
332    /// Waiting with automatic termination failed.
333    #[error("Wait-or-terminate operation failed: {0}")]
334    WaitOrTerminateFailed(#[from] WaitOrTerminateError),
335
336    /// Output collection did not complete before the operation timeout elapsed.
337    #[error("Output collection for process '{process_name}' did not complete within {timeout:?}")]
338    OutputCollectionTimeout {
339        /// The name of the process.
340        process_name: Cow<'static, str>,
341        /// The timeout duration that was exceeded.
342        timeout: Duration,
343    },
344
345    /// Collecting stdout or stderr failed.
346    #[error("Output collection for process '{process_name}' failed: {source}")]
347    OutputCollectionFailed {
348        /// The name of the process.
349        process_name: Cow<'static, str>,
350        /// The collector error that caused output collection to fail.
351        #[source]
352        source: ConsumerError,
353    },
354
355    /// Starting stdout or stderr output collection failed.
356    #[error("Output collection for process '{process_name}' could not start: {source}")]
357    OutputCollectionStartFailed {
358        /// The name of the process.
359        process_name: Cow<'static, str>,
360        /// The stream consumer error that prevented output collection from starting.
361        #[source]
362        source: StreamConsumerError,
363    },
364}
365
366/// Errors that can occur when spawning a process.
367#[derive(Debug, Error)]
368#[non_exhaustive]
369pub enum SpawnError {
370    /// Failed to spawn the process.
371    #[error("Failed to spawn process '{process_name}': {source}")]
372    SpawnFailed {
373        /// The name or description of the process being spawned.
374        process_name: Cow<'static, str>,
375        /// The underlying IO error.
376        #[source]
377        source: io::Error,
378    },
379}
380
381/// Errors that can occur when creating a stream consumer.
382#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
383#[non_exhaustive]
384pub enum StreamConsumerError {
385    /// A single-subscriber stream already has an active consumer.
386    #[error("Stream '{stream_name}' already has an active consumer")]
387    ActiveConsumer {
388        /// The name of the stream that rejected the consumer.
389        stream_name: &'static str,
390    },
391}
392
393impl StreamConsumerError {
394    /// The name of the stream that rejected the consumer.
395    #[must_use]
396    pub fn stream_name(&self) -> &'static str {
397        match self {
398            Self::ActiveConsumer { stream_name } => stream_name,
399        }
400    }
401}
402
403/// Error emitted when an output stream cannot be read to completion.
404#[derive(Debug, Clone, Error)]
405#[error("Could not read from stream '{stream_name}': {source}")]
406pub struct StreamReadError {
407    stream_name: &'static str,
408    #[source]
409    source: Arc<io::Error>,
410}
411
412impl StreamReadError {
413    /// Creates a stream read error from the stream name and underlying IO error.
414    #[must_use]
415    pub fn new(stream_name: &'static str, source: io::Error) -> Self {
416        Self {
417            stream_name,
418            source: Arc::new(source),
419        }
420    }
421
422    /// The name of the stream that failed.
423    #[must_use]
424    pub fn stream_name(&self) -> &'static str {
425        self.stream_name
426    }
427
428    /// The [`io::ErrorKind`] of the underlying read failure.
429    #[must_use]
430    pub fn kind(&self) -> io::ErrorKind {
431        self.source.kind()
432    }
433
434    /// The underlying IO error.
435    #[must_use]
436    pub fn source_io_error(&self) -> &io::Error {
437        self.source.as_ref()
438    }
439}
440
441impl PartialEq for StreamReadError {
442    fn eq(&self, other: &Self) -> bool {
443        self.stream_name == other.stream_name && self.kind() == other.kind()
444    }
445}
446
447impl Eq for StreamReadError {}
448
449/// Result of waiting for an output line matching a predicate.
450///
451/// This enum is returned inside a `Result`; stream read failures are surfaced as
452/// [`StreamReadError`] rather than as a variant of this enum.
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
454pub enum WaitForLineResult {
455    /// A matching line was observed before the stream ended or the timeout elapsed.
456    Matched,
457
458    /// The stream ended before any matching line was observed.
459    StreamClosed,
460
461    /// The timeout elapsed before a matching line was observed or the stream ended.
462    Timeout,
463}