Skip to main content

oseda_cli/cmd/
run.rs

1use std::{
2    process::Command,
3    sync::{
4        atomic::{AtomicBool, Ordering},
5        Arc,
6    },
7    time::Duration,
8};
9
10/// More in depth errors that could cause a project not to run
11#[derive(Debug)]
12pub enum OsedaRunError {
13    BuildError(String),
14    ServeError(String),
15}
16
17impl std::error::Error for OsedaRunError {}
18impl std::fmt::Display for OsedaRunError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            Self::BuildError(msg) => write!(f, "Oseda Build Error: {}", msg),
22            Self::ServeError(msg) => write!(f, "Oseda Serve Error: {}", msg),
23        }
24    }
25}
26
27/// Runs an Oseda project in the working directory
28///
29/// This will:
30/// - Run `npx vite build`
31/// - Start a static file server (`serve dist`)
32/// - Gracefully listen for Ctrl+C to shut down the server
33///     - This gracefull-ness here is important, this runs on a separate thread, do not attempt to orphan this process
34/// # Returns
35/// * `Ok(())` if both the build and serve steps succeed
36/// * `Err(OsedaRunError)` if any step fails (missing vite isn't installed, or `serve` fails to start)
37pub fn run() -> Result<(), OsedaRunError> {
38    // todo refactor the other check command to use this
39    run_with_shutdown(Arc::new(AtomicBool::new(false)))
40}
41
42pub fn run_with_shutdown(shutdown_flag: Arc<AtomicBool>) -> Result<(), OsedaRunError> {
43    // command run failure and command status are considered different, handled accordingly
44    match Command::new("npx").arg("vite").arg("build").status() {
45        Ok(status) => {
46            if !status.success() {
47                println!("Error: `npx vite build` exited with a failure.");
48                println!("Please ensure that npx and vite are installed properly.");
49                return Err(OsedaRunError::BuildError(
50                    "could not 'npx vite build'".to_string(),
51                ));
52            }
53        }
54        Err(e) => {
55            println!("Error: failed to execute `npx vite build`: {e}");
56            println!("Please ensure that `npx` and `vite` are installed and in your PATH.");
57            return Err(OsedaRunError::BuildError(
58                "could not 'npx vite build'".to_string(),
59            ));
60        }
61    }
62
63    let mut child = Command::new("npx")
64        .arg("serve")
65        .arg("dist")
66        .spawn()
67        .map_err(|e| {
68            println!("Error starting `serve dist`: {e}");
69            OsedaRunError::ServeError("failed to start serve".into())
70        })?;
71    // spawn will leave child running the background. Need to listen for ctrl+c, snatch it. Then kill subprocess
72
73    // https://github.com/Detegr/rust-ctrlc
74    // let (tx, rx) = mpsc::channel();
75    let ctrlc_flag = shutdown_flag.clone();
76    ctrlc::set_handler(move || {
77        println!("\nSIGINT received. Attempting graceful shutdown...");
78        ctrlc_flag.store(true, Ordering::SeqCst);
79    })
80    .map_err(|e| {
81        println!("Error setting ctrl+c handler: {e}");
82        OsedaRunError::ServeError("failed to set handler".into())
83    })?;
84
85    // block until ctrl+c or sigkill or flag set otherwise (e.g. via export)
86    while !shutdown_flag.load(Ordering::SeqCst) {
87        std::thread::sleep(Duration::from_millis(100));
88    }
89
90    // attempt to kill the child process
91    if let Err(e) = child.kill() {
92        println!("Failed to kill `serve`: {e}");
93    } else {
94        println!("`serve` process terminated.");
95    }
96
97    let _ = child.wait();
98
99    Ok(())
100}