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