subtale_cortex/
lib.rs

1/// A wrapper around a process closure that handles crashes by running the
2/// closure as a subprocess and invoking a crash handler closure if the
3/// subprocess fails.
4pub struct CrashHandler {
5    /// Closure that runs a process.
6    process_closure: Box<dyn Fn()>,
7    /// Command line flag that identifies a child process (and prevents infinite
8    /// recursion of spawning subprocesses).
9    child_flag: String,
10    /// Closure that handles crashes, accepting the output of the subprocess.
11    crash_handler_closure:
12        Box<dyn Fn(std::process::Output) -> Result<(), Box<dyn std::error::Error>> + 'static>,
13    /// The value of the `RUST_BACKTRACE` environment variable, to be set in the
14    /// subprocess.
15    backtrace: Option<&'static str>,
16}
17
18impl Default for CrashHandler {
19    /// Create a new crash reporter with default settings (a process that prints
20    /// "Hello, world!", "--cortex-bypass" bypass flag, a crash
21    /// handler that prints the status code and error message using
22    /// `eprintln!`, and `RUST_BACKTRACE=full`).
23    fn default() -> Self {
24        Self {
25            process_closure: Box::new(|| println!("Hello, world!")),
26            child_flag: "--cortex-child".to_string(),
27            crash_handler_closure: Box::new(|output| {
28                let status = output.status.code().unwrap_or(-1);
29                let error = String::from_utf8_lossy(&output.stderr);
30
31                eprintln!("Status: {status}\nError: {error}");
32                Ok(())
33            }),
34            backtrace: None,
35        }
36    }
37}
38
39impl CrashHandler {
40    /// Create a new crash reporter with default settings.
41    pub fn new() -> Self { Self::default() }
42
43    /// Create a new crash reporter from the given closure that runs a process.
44    pub fn with_process(process: impl Fn() + 'static) -> Self {
45        Self {
46            process_closure: Box::new(process),
47            ..Default::default()
48        }
49    }
50
51    /// Sets the command line flag that identifies a child process.
52    pub fn child_flag(mut self, flag: impl Into<String>) -> Self {
53        self.child_flag = flag.into();
54        self
55    }
56
57    /// Sets the crash handler that is called when the process fails.
58    pub fn crash_handler(
59        mut self,
60        crash_handler: impl Fn(std::process::Output) -> Result<(), Box<dyn std::error::Error>> + 'static,
61    ) -> Self {
62        self.crash_handler_closure = Box::new(crash_handler);
63        self
64    }
65
66    /// Sets the value of the `RUST_BACKTRACE` environment variable in the
67    /// subprocess to `1`.
68    pub fn backtrace(mut self) -> Self {
69        self.backtrace = Some("1");
70        self
71    }
72
73    /// Sets the value of the `RUST_BACKTRACE` environment variable in the
74    /// subprocess to `full`.
75    pub fn full_backtrace(mut self) -> Self {
76        self.backtrace = Some("full");
77        self
78    }
79
80    /// Runs the configured process as a subprocess and handle crashes if the
81    /// child flag is not present, otherwise run the process normally.
82    ///
83    /// # Returns
84    /// - `Ok(true)` if the process ran successfully without errors.
85    /// - `Ok(false)` if there was an error in the process (that was handled).
86    /// - `Err` if there was an error spawning the process or handling an error
87    ///   in the process.
88    pub fn run(&self) -> Result<bool, Box<dyn std::error::Error>> {
89        // Capture the current CLI arguments
90        let mut args = std::env::args().collect::<Vec<_>>();
91
92        // If the child flag is present, run the process normally
93        if args.contains(&self.child_flag) {
94            (self.process_closure)();
95        } else {
96            // Remove the first argument (path to executable) and add the child flag
97            args.remove(0);
98            args.push(self.child_flag.clone());
99
100            // Spawn current exe as subprocess and read process output
101            let output = std::process::Command::new(std::env::current_exe()?)
102                // Passthrough the current arguments
103                .args(args)
104                // Passthrough the current environment
105                .envs(std::env::vars())
106                // Set the RUST_BACKTRACE environment variable if configured
107                .env("RUST_BACKTRACE", self.backtrace.unwrap_or("0"))
108                // Spawn the subprocess and capture its output
109                .output()?;
110
111            // If the subprocess failed, call the crash handler closure
112            if !output.status.success() {
113                (self.crash_handler_closure)(output)?;
114                return Ok(false);
115            }
116        }
117
118        Ok(true)
119    }
120}