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#[derive(Args, Debug)]
19pub struct CheckOptions {
20 #[arg(long, default_value_t = 3000)]
23 port: u16,
24}
25#[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
40impl 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
66pub 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 match verify_project(opts.port) {
84 OsedaProjectStatus::DeployReady => Ok(()),
85 OsedaProjectStatus::NotDeploymentReady(err) => Err(err),
86 }
87}
88
89pub enum OsedaProjectStatus {
91 DeployReady,
92 NotDeploymentReady(OsedaCheckError),
93}
94
95fn 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 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 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 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 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 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}