Skip to main content

brush_core/
results.rs

1//! Encapsulation of execution results.
2
3#[cfg(unix)]
4use std::os::unix::process::ExitStatusExt;
5
6use crate::{error, processes};
7
8/// Represents the result of executing a command or similar item.
9#[derive(Default)]
10pub struct ExecutionResult {
11    /// The control flow transition to apply after execution.
12    pub next_control_flow: ExecutionControlFlow,
13    /// The exit code resulting from execution.
14    pub exit_code: ExecutionExitCode,
15}
16
17impl ExecutionResult {
18    /// Returns a new `ExecutionResult` with the given exit code.
19    ///
20    /// # Arguments
21    ///
22    /// * `exit_code` - The exit code of the command.
23    pub fn new(exit_code: u8) -> Self {
24        Self {
25            exit_code: exit_code.into(),
26            ..Self::default()
27        }
28    }
29
30    /// Returns a new `ExecutionResult` reflecting a process that was stopped.
31    pub fn stopped() -> Self {
32        // TODO(jobs): Decide how to sort this out in a platform-independent way.
33        const SIGTSTP: std::os::raw::c_int = 20;
34
35        #[expect(clippy::cast_possible_truncation)]
36        Self::new(128 + SIGTSTP as u8)
37    }
38
39    /// Returns a new `ExecutionResult` with an exit code of 0.
40    pub const fn success() -> Self {
41        Self {
42            next_control_flow: ExecutionControlFlow::Normal,
43            exit_code: ExecutionExitCode::Success,
44        }
45    }
46
47    /// Returns a new `ExecutionResult` with a general error exit code.
48    pub const fn general_error() -> Self {
49        Self {
50            next_control_flow: ExecutionControlFlow::Normal,
51            exit_code: ExecutionExitCode::GeneralError,
52        }
53    }
54
55    /// Returns whether the command was successful.
56    pub const fn is_success(&self) -> bool {
57        self.exit_code.is_success()
58    }
59
60    /// Returns whether the execution result indicates normal control flow.
61    /// Returns `false` if there is any control flow transition requested.
62    pub const fn is_normal_flow(&self) -> bool {
63        matches!(self.next_control_flow, ExecutionControlFlow::Normal)
64    }
65
66    /// Returns whether the execution result indicates a loop break.
67    pub const fn is_break(&self) -> bool {
68        matches!(
69            self.next_control_flow,
70            ExecutionControlFlow::BreakLoop { .. }
71        )
72    }
73
74    /// Returns whether the execution result indicates a loop continue.
75    pub const fn is_continue(&self) -> bool {
76        matches!(
77            self.next_control_flow,
78            ExecutionControlFlow::ContinueLoop { .. }
79        )
80    }
81
82    /// Returns whether the execution result indicates an early return
83    /// from a function or script, or an exit from the shell. Returns `false`
84    /// otherwise, including loop breaks or continues.
85    pub const fn is_return_or_exit(&self) -> bool {
86        matches!(
87            self.next_control_flow,
88            ExecutionControlFlow::ReturnFromFunctionOrScript | ExecutionControlFlow::ExitShell
89        )
90    }
91}
92
93impl From<ExecutionExitCode> for ExecutionResult {
94    fn from(exit_code: ExecutionExitCode) -> Self {
95        Self {
96            next_control_flow: ExecutionControlFlow::Normal,
97            exit_code,
98        }
99    }
100}
101
102impl From<ExecutionWaitResult> for ExecutionResult {
103    fn from(wait_result: ExecutionWaitResult) -> Self {
104        match wait_result {
105            ExecutionWaitResult::Completed(result) => result,
106            // TODO(jobs): We need to job-manage the stopped process.
107            ExecutionWaitResult::Stopped(..) => Self::stopped(),
108        }
109    }
110}
111
112impl From<std::process::Output> for ExecutionResult {
113    fn from(output: std::process::Output) -> Self {
114        if let Some(code) = output.status.code() {
115            #[expect(clippy::cast_sign_loss)]
116            return Self::new((code & 0xFF) as u8);
117        }
118
119        #[cfg(unix)]
120        if let Some(signal) = output.status.signal() {
121            #[expect(clippy::cast_sign_loss)]
122            return Self::new((signal & 0xFF) as u8 + 128);
123        }
124
125        tracing::error!("unhandled process exit");
126        Self::new(127)
127    }
128}
129
130/// Represents an exit code from execution.
131#[derive(Clone, Copy, Default)]
132pub enum ExecutionExitCode {
133    /// Indicates successful execution.
134    #[default]
135    Success,
136    /// Indicates a general error.
137    GeneralError,
138    /// Indicates invalid usage.
139    InvalidUsage,
140    /// Cannot execute the command.
141    CannotExecute,
142    /// Indicates a command or similar item was not found.
143    NotFound,
144    /// Indicates execution was interrupted.
145    Interrupted,
146    /// Indicates a broken pipe (SIGPIPE) was encountered.
147    BrokenPipe,
148    /// Indicates unimplemented functionality was encountered.
149    Unimplemented,
150    /// A custom exit code.
151    Custom(u8),
152}
153
154impl ExecutionExitCode {
155    /// Returns whether the exit code indicates success.
156    pub const fn is_success(&self) -> bool {
157        matches!(self, Self::Success)
158    }
159}
160
161impl From<u8> for ExecutionExitCode {
162    fn from(code: u8) -> Self {
163        match code {
164            0 => Self::Success,
165            1 => Self::GeneralError,
166            2 => Self::InvalidUsage,
167            99 => Self::Unimplemented,
168            126 => Self::CannotExecute,
169            127 => Self::NotFound,
170            130 => Self::Interrupted,
171            141 => Self::BrokenPipe,
172            code => Self::Custom(code),
173        }
174    }
175}
176
177impl From<ExecutionExitCode> for u8 {
178    fn from(code: ExecutionExitCode) -> Self {
179        Self::from(&code)
180    }
181}
182
183impl From<&ExecutionExitCode> for u8 {
184    fn from(code: &ExecutionExitCode) -> Self {
185        match code {
186            ExecutionExitCode::Success => 0,
187            ExecutionExitCode::GeneralError => 1,
188            ExecutionExitCode::InvalidUsage => 2,
189            ExecutionExitCode::Unimplemented => 99,
190            ExecutionExitCode::CannotExecute => 126,
191            ExecutionExitCode::NotFound => 127,
192            ExecutionExitCode::Interrupted => 130,
193            ExecutionExitCode::BrokenPipe => 141,
194            ExecutionExitCode::Custom(code) => *code,
195        }
196    }
197}
198
199/// Represents a control flow transition to apply.
200#[derive(Clone, Copy, Default)]
201pub enum ExecutionControlFlow {
202    /// Continue normal execution.
203    #[default]
204    Normal,
205    /// Break out of an enclosing loop.
206    BreakLoop {
207        /// Identifies which level of nested loops to break out of. 0 indicates the innermost loop,
208        /// 1 indicates the next outer loop, and so on.
209        levels: usize,
210    },
211    /// Continue to the next iteration of an enclosing loop.
212    ContinueLoop {
213        /// Identifies which level of nested loops to continue. 0 indicates the innermost loop,
214        /// 1 indicates the next outer loop, and so on.
215        levels: usize,
216    },
217    /// Return from the current function or script.
218    ReturnFromFunctionOrScript,
219    /// Exit the shell.
220    ExitShell,
221}
222
223impl ExecutionControlFlow {
224    /// Attempts to decrement the loop levels for `BreakLoop` or `ContinueLoop`.
225    /// If the levels reach zero, transitions to `Normal`. If the control flow is not
226    /// a loop break or continue, no changes are made.
227    #[must_use]
228    pub const fn try_decrement_loop_levels(&self) -> Self {
229        match self {
230            Self::BreakLoop { levels: 0 } | Self::ContinueLoop { levels: 0 } => Self::Normal,
231            Self::BreakLoop { levels } => Self::BreakLoop {
232                levels: *levels - 1,
233            },
234            Self::ContinueLoop { levels } => Self::ContinueLoop {
235                levels: *levels - 1,
236            },
237            control_flow => *control_flow,
238        }
239    }
240}
241
242/// Represents the result of spawning an execution; captures both execution
243/// that immediately returns as well as execution that starts a process
244/// asynchronously.
245pub enum ExecutionSpawnResult {
246    /// Indicates that the execution completed.
247    Completed(ExecutionResult),
248    /// Indicates that a process was started and had not yet completed.
249    StartedProcess(processes::ChildProcess),
250    /// Indicates that a task was started to handle the execution asynchronously.
251    StartedTask(tokio::task::JoinHandle<Result<ExecutionResult, error::Error>>),
252}
253
254impl From<ExecutionResult> for ExecutionSpawnResult {
255    fn from(result: ExecutionResult) -> Self {
256        Self::Completed(result)
257    }
258}
259
260impl ExecutionSpawnResult {
261    /// Waits for the command to complete.
262    pub async fn wait(self) -> Result<ExecutionWaitResult, error::Error> {
263        let result = match self {
264            Self::StartedProcess(mut child) => {
265                // Wait for the process to exit or for a relevant signal, whichever happens
266                // first.
267                match child.wait().await? {
268                    processes::ProcessWaitResult::Completed(output) => {
269                        ExecutionWaitResult::Completed(ExecutionResult::from(output))
270                    }
271                    processes::ProcessWaitResult::Stopped => ExecutionWaitResult::Stopped(child),
272                }
273            }
274            Self::Completed(result) => ExecutionWaitResult::Completed(result),
275            Self::StartedTask(join_handle) => {
276                let result = join_handle.await?;
277                ExecutionWaitResult::Completed(result?)
278            }
279        };
280
281        Ok(result)
282    }
283
284    pub(crate) async fn poll(self) -> Result<ExecutionWaitResult, error::Error> {
285        let result = match self {
286            Self::StartedProcess(child) => ExecutionWaitResult::Stopped(child),
287            Self::Completed(result) => ExecutionWaitResult::Completed(result),
288            Self::StartedTask(join_handle) => {
289                // TODO(jobs): This isn't right.
290                let result = join_handle.await?;
291                ExecutionWaitResult::Completed(result?)
292            }
293        };
294
295        Ok(result)
296    }
297}
298
299/// Represents the result of waiting for an execution to complete.
300pub enum ExecutionWaitResult {
301    /// Indicates that the execution completed.
302    Completed(ExecutionResult),
303    /// Indicates that the execution was stopped.
304    Stopped(processes::ChildProcess),
305}