Skip to main content

oseda_cli/cmd/
check.rs

1use std::{
2    sync::{
3        atomic::{AtomicBool, Ordering},
4        Arc,
5    },
6    time::Duration,
7};
8
9use clap::Args;
10use reqwest::StatusCode;
11
12use crate::cmd::run;
13use crate::config;
14
15use crate::net::{self, kill_port};
16
17/// Options for the `oseda check` command
18#[derive(Args, Debug)]
19pub struct CheckOptions {
20    /// Port to check for the Oseda project on
21    /// This is only useful if you have changed the default port that Oseda projects run on my default (3000)
22    #[arg(long, default_value_t = 3000)]
23    port: u16,
24}
25/// All common error types that could cause `oseda check` to fail
26#[derive(Debug)]
27pub enum OsedaCheckError {
28    MissingConfig(String),
29    BadConfig(String),
30    BadGitCredentials(String),
31    DirectoryNameMismatch(String),
32    CouldNotPingLocalPresentation(String),
33    MissingDescription(String),
34    MissingTags(String),
35}
36
37impl std::error::Error for OsedaCheckError {}
38
39/// Display options with more verbose messagess
40impl std::fmt::Display for OsedaCheckError {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Self::MissingConfig(msg) => write!(f, "Missing config file: {}", msg),
44            Self::BadConfig(msg) => write!(f, "Bad config file: {}", msg),
45            Self::BadGitCredentials(msg) => write!(f, "Missing git credentials: {}", msg),
46            Self::DirectoryNameMismatch(msg) => {
47                write!(f, "Project name does not match directory: {}", msg)
48            }
49            Self::CouldNotPingLocalPresentation(msg) => {
50                write!(f, "Could not ping localhost after project was ran: {}", msg)
51            }
52            Self::MissingDescription(msg) => {
53                write!(f, "Config file is missing description: {}", msg)
54            }
55            Self::MissingTags(msg) => {
56                write!(f, "No tags detected: {}", msg)
57            }
58        }
59    }
60}
61
62/// Checks the Oseda project in the working directory for common oseda errors
63///
64/// # Arguments
65/// * `opts` - options parsed from CLI flags
66///
67/// # Returns
68/// * `Ok(())` if the project passes all checks and is considered as "deployabl"e
69/// * `Err(OsedaCheckError)` a problem was detected that prevents the user from doing a deployment
70pub fn check(opts: CheckOptions) -> Result<(), OsedaCheckError> {
71    // separate abstraction layer here, want the primary subcommand to call this
72    // verify can also be called from deploy (in theory)
73    match verify_project(opts.port) {
74        OsedaProjectStatus::DeployReady => Ok(()),
75        OsedaProjectStatus::NotDeploymentReady(err) => Err(err),
76    }
77}
78
79/// Status of Oseda project, plan to make this more verbose later
80pub enum OsedaProjectStatus {
81    DeployReady,
82    NotDeploymentReady(OsedaCheckError),
83}
84
85/// Verifies a project passes all common checks
86///
87/// # Arguments
88/// * `skip_git` - skips git authorship validation
89/// * `port_num` - the port to check for the running project (defaults to 3000)
90///
91/// # Returns
92/// * `OsedaProjectStatus::DeployReady` if the project passes all checks
93/// * `OsedaProjectStatus::NotDeploymentReady(err)` if something fails that is commonly seen
94fn verify_project(port_num: u16) -> OsedaProjectStatus {
95    let _conf = match config::read_and_validate_config() {
96        Ok(conf) => conf,
97        Err(err) => return OsedaProjectStatus::NotDeploymentReady(err),
98    };
99
100    let shutdown_flag = Arc::new(AtomicBool::new(false));
101    let shutdown_flag_clone = shutdown_flag.clone();
102
103    // use shutdown hook and kill once polled as alive
104    let run_handle = std::thread::spawn(move || {
105        let _ = run::run_with_shutdown(shutdown_flag_clone);
106    });
107
108    let addr = format!("http://localhost:{}", port_num);
109    let mut status = None;
110
111    // poll oseda run process at:
112    let max_polls = 100;
113    let poll_delay = Duration::from_millis(200);
114
115    for i in 0..max_polls {
116        println!("polled {}", i);
117        if let Ok(res_status) = net::get_status(&addr) {
118            if res_status == StatusCode::OK {
119                status = Some(res_status);
120                break;
121            }
122        }
123        std::thread::sleep(poll_delay);
124    }
125
126    let status = match status {
127        Some(status) => status,
128        None => {
129            // if could not get status, ensure process dies
130            shutdown_flag.store(true, Ordering::SeqCst);
131            let _ = run_handle.join();
132            return OsedaProjectStatus::NotDeploymentReady(
133                OsedaCheckError::CouldNotPingLocalPresentation(
134                    "Could not ping presentation".to_owned(),
135                ),
136            );
137        }
138    };
139
140    if status != StatusCode::OK {
141        // send shutdown flag, but happy about it this time
142        shutdown_flag.store(true, Ordering::SeqCst);
143        let _ = run_handle.join();
144        return OsedaProjectStatus::NotDeploymentReady(
145            OsedaCheckError::CouldNotPingLocalPresentation(
146                "Presentation returned non 200 error status code".to_owned(),
147            ),
148        );
149    }
150
151    println!("Project returned status code {:?}", status);
152
153    // due to memory issues, no nice way to kill run_handle
154    // eg -> no run_handle.kill();
155    // so we'll go through the OS instead.
156    // This can also be solved with an atomic boolean in run, this
157    // would also get rid of the mpsc stuff going on in run(), but honestly
158    // im just not that familiar with the mpsc pattern and rust api
159
160    // shutdown other process
161    shutdown_flag.store(true, Ordering::SeqCst);
162    if run_handle.join().is_err() {
163        if kill_port(port_num).is_err() {
164            println!("Warning: could not kill process on port, project could still be running");
165        }
166    } else {
167        println!("Project process sucessfully terminated");
168    }
169
170    OsedaProjectStatus::DeployReady
171}