Skip to main content

asimov_runner/
executor_error.rs

1// This is free and unencumbered software released into the public domain.
2
3//! Results and error categories for child-process execution.
4//!
5//! [`ExecutorError`] distinguishes failures to start a program from unsuccessful
6//! exits and other I/O failures. Recognized sysexits statuses are exposed as
7//! [`SysexitsError`] values so callers can handle them without parsing messages.
8
9use crate::SysexitsError;
10use alloc::{string::String, vec::Vec};
11use core::fmt;
12use std::{ffi::OsString, io::Cursor};
13
14/// Captured stdout from a successful process, or an execution failure.
15///
16/// The cursor contains raw bytes, is positioned at zero, and is empty when stdout
17/// was not captured. The complete output is buffered before this result is
18/// returned; it is not a live stream from the child.
19pub type ExecutorResult = std::result::Result<Cursor<Vec<u8>>, ExecutorError>;
20
21/// A configuration failure, or a failure to launch, communicate with, or complete a child process.
22///
23/// Conversion from a process output decodes stderr strictly as UTF-8: invalid
24/// UTF-8 yields `None`, while empty stderr yields `Some(String::new())`.
25/// Conversion from an exit status alone has no stderr and always uses `None`.
26#[derive(Debug)]
27pub enum ExecutorError {
28    /// Supplied capability metadata declares a requested option unsupported.
29    /// Contains the long option name (such as `--sort`); no child was spawned.
30    UnsupportedOption(&'static str),
31    /// Spawning returned `NotFound`; contains the selected program name or path.
32    MissingProgram(OsString),
33    /// The process could not be started for a reason other than `NotFound`.
34    SpawnFailure(std::io::Error),
35    /// A recognized sysexits error and optional captured UTF-8 stderr.
36    Failure(SysexitsError, Option<String>),
37    /// An unrecognized exit code and optional captured UTF-8 stderr.
38    ///
39    /// The code is `None` when termination has no numeric exit code, such as
40    /// termination by a signal on Unix.
41    UnexpectedFailure(Option<i32>, Option<String>),
42    /// An I/O failure outside spawning, such as copying stdin, waiting for the
43    /// child, or decoding a prompter's stdout as UTF-8.
44    UnexpectedOther(std::io::Error),
45    /// The child exited successfully before its input feed completed. Use
46    /// [`crate::ExecutionCompletion`] to explicitly handle intentional early exit.
47    IncompleteInput,
48}
49
50impl core::error::Error for ExecutorError {}
51
52impl From<crate::JsonlLineError> for ExecutorError {
53    fn from(error: crate::JsonlLineError) -> Self {
54        std::io::Error::new(std::io::ErrorKind::InvalidData, error).into()
55    }
56}
57
58impl fmt::Display for ExecutorError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::UnsupportedOption(option) => {
62                write!(f, "Program does not support requested option: {}", option)
63            },
64            Self::MissingProgram(program) => {
65                write!(f, "Missing program: {}", program.to_string_lossy())
66            },
67            Self::SpawnFailure(err) => write!(f, "Failed to spawn process: {}", err),
68            Self::Failure(error, stderr) => {
69                write!(
70                    f,
71                    "Command failed with exit code {}",
72                    error.code().unwrap_or(-1),
73                )?;
74                if let Some(stderr) = stderr {
75                    write!(f, "\n{}", stderr)?;
76                }
77                Ok(())
78            },
79            Self::UnexpectedFailure(code, stderr) => {
80                write!(
81                    f,
82                    "Command failed with unexpected exit code: {}",
83                    code.unwrap_or(-1)
84                )?;
85                if let Some(stderr) = stderr {
86                    write!(f, "\n{}", stderr)?;
87                }
88                Ok(())
89            },
90            Self::UnexpectedOther(err) => write!(f, "Unexpected error: {}", err),
91            Self::IncompleteInput => write!(f, "Process exited before input delivery completed"),
92        }
93    }
94}
95
96#[cfg(feature = "std")]
97impl From<std::io::Error> for ExecutorError {
98    fn from(error: std::io::Error) -> Self {
99        Self::UnexpectedOther(error)
100    }
101}
102
103#[cfg(feature = "std")]
104impl From<std::process::ExitStatus> for ExecutorError {
105    fn from(status: std::process::ExitStatus) -> Self {
106        match SysexitsError::try_from(status) {
107            Ok(error) => Self::Failure(error, None),
108            Err(code) => Self::UnexpectedFailure(code, None),
109        }
110    }
111}
112
113#[cfg(feature = "std")]
114impl From<std::process::Output> for ExecutorError {
115    fn from(output: std::process::Output) -> Self {
116        let stderr = String::from_utf8(output.stderr).ok();
117        match SysexitsError::try_from(output.status) {
118            Ok(error) => Self::Failure(error, stderr),
119            Err(code) => Self::UnexpectedFailure(code, stderr),
120        }
121    }
122}