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
118    /// Graceful interrupt phase.
119    ///
120    /// Only emitted on Unix, where this is the `SIGINT` step that begins the graceful escalation.
121    /// Windows has no targetable `SIGINT` analogue: `GenerateConsoleCtrlEvent` cannot deliver
122    /// `CTRL_C_EVENT` to a single child group, and `CTRL_BREAK_EVENT` is harsher than a Unix
123    /// interrupt, so Windows skips this phase and goes straight to [`Self::Terminate`].
124    Interrupt,
125
126    /// Graceful terminate phase.
127    ///
128    /// On Unix this is the `SIGTERM` step that follows the `SIGINT` phase. On Windows this
129    /// represents the single `CTRL_BREAK_EVENT` graceful step (the only console control event
130    /// `GenerateConsoleCtrlEvent` can target at a nonzero process group); it sits in this phase
131    /// rather than [`Self::Interrupt`] because `CTRL_BREAK_EVENT` is closer in severity to
132    /// `SIGTERM` than to `SIGINT`.
133    Terminate,
134
135    /// Forceful kill phase.
136    Kill,
137}
138
139impl fmt::Display for TerminationAttemptPhase {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            Self::Preflight => f.write_str("preflight"),
143            Self::Interrupt => f.write_str("interrupt"),
144            Self::Terminate => f.write_str("terminate"),
145            Self::Kill => f.write_str("kill"),
146        }
147    }
148}
149
150/// Termination operation that failed.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152#[non_exhaustive]
153pub enum TerminationAttemptOperation {
154    /// Checking whether the process has already exited failed.
155    CheckStatus,
156    /// Sending a graceful or forceful termination signal failed.
157    SendSignal,
158    /// Waiting for the process to exit after a termination signal failed.
159    WaitForExit,
160}
161
162impl fmt::Display for TerminationAttemptOperation {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            Self::CheckStatus => f.write_str("status check"),
166            Self::SendSignal => f.write_str("signal send"),
167            Self::WaitForExit => f.write_str("exit wait"),
168        }
169    }
170}
171
172/// Errors that can occur when waiting for process operations.
173#[derive(Debug, Error)]
174#[non_exhaustive]
175pub enum WaitError {
176    /// A general IO error occurred.
177    #[error("IO error occurred while waiting for process '{process_name}': {source}")]
178    IoError {
179        /// The name of the process.
180        process_name: Cow<'static, str>,
181        /// The underlying IO error.
182        #[source]
183        source: io::Error,
184    },
185}
186
187/// Result of waiting for a process to complete within an explicit timeout.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum WaitForCompletionResult<T = ExitStatus> {
190    /// The process completed before the timeout elapsed.
191    Completed(T),
192
193    /// The timeout elapsed before the process completed.
194    Timeout {
195        /// The timeout duration that was exceeded.
196        timeout: Duration,
197    },
198}
199
200impl<T> WaitForCompletionResult<T> {
201    /// Returns the completed value, or `None` if the wait timed out.
202    #[must_use]
203    pub fn into_completed(self) -> Option<T> {
204        match self {
205            Self::Completed(value) => Some(value),
206            Self::Timeout { .. } => None,
207        }
208    }
209
210    /// Returns the completed value, panicking with `message` if the wait timed out.
211    ///
212    /// # Panics
213    ///
214    /// Panics with `message` if this result is [`WaitForCompletionResult::Timeout`].
215    pub fn expect_completed(self, message: &str) -> T {
216        self.into_completed().expect(message)
217    }
218
219    /// Maps a completed value while preserving timeout outcomes.
220    pub(crate) fn map<U>(self, f: impl FnOnce(T) -> U) -> WaitForCompletionResult<U> {
221        match self {
222            Self::Completed(value) => WaitForCompletionResult::Completed(f(value)),
223            Self::Timeout { timeout } => WaitForCompletionResult::Timeout { timeout },
224        }
225    }
226}
227
228/// Result of waiting for a process to complete, terminating it if the wait times out.
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum WaitForCompletionOrTerminateResult<T = ExitStatus> {
231    /// The process completed before the wait timeout elapsed.
232    Completed(T),
233
234    /// The wait timeout elapsed, then cleanup termination completed successfully.
235    TerminatedAfterTimeout {
236        /// The result observed after cleanup termination.
237        result: T,
238        /// The wait timeout duration that was exceeded before cleanup began.
239        timeout: Duration,
240    },
241}
242
243impl<T> WaitForCompletionOrTerminateResult<T> {
244    /// Returns the terminal value, whether completion happened before timeout or after cleanup.
245    #[must_use]
246    pub fn into_result(self) -> T {
247        match self {
248            Self::Completed(value) | Self::TerminatedAfterTimeout { result: value, .. } => value,
249        }
250    }
251
252    /// Returns the completed value, or `None` if cleanup termination was required after timeout.
253    #[must_use]
254    pub fn into_completed(self) -> Option<T> {
255        match self {
256            Self::Completed(value) => Some(value),
257            Self::TerminatedAfterTimeout { .. } => None,
258        }
259    }
260
261    /// Returns the completed value, panicking with `message` if cleanup termination was required.
262    ///
263    /// # Panics
264    ///
265    /// Panics with `message` if this result is
266    /// [`WaitForCompletionOrTerminateResult::TerminatedAfterTimeout`].
267    pub fn expect_completed(self, message: &str) -> T {
268        self.into_completed().expect(message)
269    }
270
271    /// Maps a terminal value while preserving the timeout/cleanup outcome.
272    pub(crate) fn map<U>(self, f: impl FnOnce(T) -> U) -> WaitForCompletionOrTerminateResult<U> {
273        match self {
274            Self::Completed(value) => WaitForCompletionOrTerminateResult::Completed(f(value)),
275            Self::TerminatedAfterTimeout { result, timeout } => {
276                WaitForCompletionOrTerminateResult::TerminatedAfterTimeout {
277                    result: f(result),
278                    timeout,
279                }
280            }
281        }
282    }
283}
284
285/// Errors that can occur when waiting for a process with automatic termination on failure.
286#[derive(Debug, Error)]
287#[non_exhaustive]
288pub enum WaitOrTerminateError {
289    /// Waiting failed, but the subsequent cleanup termination succeeded.
290    #[error(
291        "Waiting for process '{process_name}' failed with '{wait_error}', then cleanup termination completed with status {termination_status}"
292    )]
293    WaitFailed {
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 status observed after cleanup termination.
300        termination_status: ExitStatus,
301    },
302
303    /// Waiting failed, and the subsequent cleanup termination also failed.
304    #[error(
305        "Waiting for process '{process_name}' failed with '{wait_error}', then cleanup termination also failed: {termination_error}"
306    )]
307    TerminationFailed {
308        /// The name of the process.
309        process_name: Cow<'static, str>,
310        /// The original error returned while waiting for the process.
311        #[source]
312        wait_error: Box<WaitError>,
313        /// The error returned while trying to terminate the process after the wait failure.
314        termination_error: TerminationError,
315    },
316
317    /// Waiting timed out, and the subsequent cleanup termination failed.
318    #[error(
319        "Process '{process_name}' did not complete within {timeout:?}, then cleanup termination failed: {termination_error}"
320    )]
321    TerminationAfterTimeoutFailed {
322        /// The name of the process.
323        process_name: Cow<'static, str>,
324        /// The wait timeout duration that was exceeded before cleanup began.
325        timeout: Duration,
326        /// The error returned while trying to terminate the process after the timeout.
327        #[source]
328        termination_error: TerminationError,
329    },
330}
331
332/// Errors that can occur when waiting for a process while collecting its output, with or
333/// without automatic termination on timeout.
334///
335/// `WaitFailed` is emitted by APIs without automatic termination
336/// (`wait_for_completion_with_output`, `wait_for_completion_with_raw_output`).
337/// `WaitOrTerminateFailed` is emitted by the `*_or_terminate` variants. The remaining variants
338/// can be emitted by either family.
339#[derive(Debug, Error)]
340#[non_exhaustive]
341pub enum WaitWithOutputError {
342    /// Waiting for the process failed.
343    #[error("Waiting for process completion failed: {0}")]
344    WaitFailed(#[from] WaitError),
345
346    /// Waiting with automatic termination failed.
347    #[error("Wait-or-terminate operation failed: {0}")]
348    WaitOrTerminateFailed(#[from] WaitOrTerminateError),
349
350    /// Output collection did not complete before the operation timeout elapsed.
351    #[error("Output collection for process '{process_name}' did not complete within {timeout:?}")]
352    OutputCollectionTimeout {
353        /// The name of the process.
354        process_name: Cow<'static, str>,
355        /// The timeout duration that was exceeded.
356        timeout: Duration,
357    },
358
359    /// Collecting stdout or stderr failed.
360    #[error("Output collection for process '{process_name}' failed: {source}")]
361    OutputCollectionFailed {
362        /// The name of the process.
363        process_name: Cow<'static, str>,
364        /// The collector error that caused output collection to fail.
365        #[source]
366        source: ConsumerError,
367    },
368
369    /// Starting stdout or stderr output collection failed.
370    #[error("Output collection for process '{process_name}' could not start: {source}")]
371    OutputCollectionStartFailed {
372        /// The name of the process.
373        process_name: Cow<'static, str>,
374        /// The stream consumer error that prevented output collection from starting.
375        #[source]
376        source: StreamConsumerError,
377    },
378}
379
380/// Errors that can occur when spawning a process.
381#[derive(Debug, Error)]
382#[non_exhaustive]
383pub enum SpawnError {
384    /// Failed to spawn the process.
385    #[error("Failed to spawn process '{process_name}': {source}")]
386    SpawnFailed {
387        /// The name or description of the process being spawned.
388        process_name: Cow<'static, str>,
389        /// The underlying IO error.
390        #[source]
391        source: io::Error,
392    },
393}
394
395/// Errors that can occur when creating a stream consumer.
396#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
397#[non_exhaustive]
398pub enum StreamConsumerError {
399    /// A single-subscriber stream already has an active consumer.
400    #[error("Stream '{stream_name}' already has an active consumer")]
401    ActiveConsumer {
402        /// The name of the stream that rejected the consumer.
403        stream_name: &'static str,
404    },
405}
406
407impl StreamConsumerError {
408    /// The name of the stream that rejected the consumer.
409    #[must_use]
410    pub fn stream_name(&self) -> &'static str {
411        match self {
412            Self::ActiveConsumer { stream_name } => stream_name,
413        }
414    }
415}
416
417/// Error emitted when an output stream cannot be read to completion.
418#[derive(Debug, Clone, Error)]
419#[error("Could not read from stream '{stream_name}': {source}")]
420pub struct StreamReadError {
421    stream_name: &'static str,
422    #[source]
423    source: Arc<io::Error>,
424}
425
426impl StreamReadError {
427    /// Creates a stream read error from the stream name and underlying IO error.
428    #[must_use]
429    pub fn new(stream_name: &'static str, source: io::Error) -> Self {
430        Self {
431            stream_name,
432            source: Arc::new(source),
433        }
434    }
435
436    /// The name of the stream that failed.
437    #[must_use]
438    pub fn stream_name(&self) -> &'static str {
439        self.stream_name
440    }
441
442    /// The [`io::ErrorKind`] of the underlying read failure.
443    #[must_use]
444    pub fn kind(&self) -> io::ErrorKind {
445        self.source.kind()
446    }
447
448    /// The underlying IO error.
449    #[must_use]
450    pub fn source_io_error(&self) -> &io::Error {
451        self.source.as_ref()
452    }
453}
454
455impl PartialEq for StreamReadError {
456    fn eq(&self, other: &Self) -> bool {
457        self.stream_name == other.stream_name && self.kind() == other.kind()
458    }
459}
460
461impl Eq for StreamReadError {}
462
463/// Result of waiting for an output line matching a predicate.
464///
465/// This enum is returned inside a `Result`; stream read failures are surfaced as
466/// [`StreamReadError`] rather than as a variant of this enum.
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub enum WaitForLineResult {
469    /// A matching line was observed before the stream ended or the timeout elapsed.
470    Matched,
471
472    /// The stream ended before any matching line was observed.
473    StreamClosed,
474
475    /// The timeout elapsed before a matching line was observed or the stream ended.
476    Timeout,
477}