Skip to main content

oseda_cli/cmd/
run.rs

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