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