Skip to main content

quorum_public/
main.rs

1//! This is the main entry point for the Quorum Public server.
2//!
3//! To build/run the server use `cargo run -p quorum-public`
4
5mod cli;
6mod db;
7mod models;
8mod routes;
9mod tests;
10mod utility;
11
12use crate::db::schema;
13use colored::Colorize;
14use quorum_core::db as core_db;
15use quorum_core::startup;
16use reqwest::Client;
17use routes::route::create_router;
18use std::net::SocketAddr;
19use std::time::Duration;
20use std::time::Instant;
21
22#[tokio::main]
23async fn main() {
24    // Clear the terminal and move the cursor to top left corner
25    print!("\x1B[2J\x1B[1;1H");
26
27    startup::print_banner();
28    startup::print_initializing();
29
30    let timer = startup::create_timer();
31    match quorum_core::utility::config::Config::load() {
32        Ok(_) => startup::print_step("Loading config", true, startup::elapsed(timer)),
33        Err(e) => {
34            startup::print_step("Loading config", false, startup::elapsed(timer));
35            eprintln!("{}", format!("  Error: {}", e).red());
36            std::process::exit(1);
37        }
38    };
39
40    // Load the config once and reuse it
41    let config = quorum_core::utility::config::Config::get();
42
43    // Load up and connect to the database
44    let timer = startup::create_timer();
45    let _db = match core_db::init().await {
46        Ok(db) => {
47            startup::print_step("Opening database", true, startup::elapsed(timer));
48            db
49        }
50        Err(e) => {
51            startup::print_step("Opening database", false, startup::elapsed(timer));
52            eprintln!("{}", format!("  Error: {}", e).red());
53            std::process::exit(1);
54        }
55    };
56
57    // Write the tables to the database if they don't exist
58    let timer = startup::create_timer();
59    match schema::init(&_db).await {
60        Ok(_) => {
61            startup::print_step("Initializing schema", true, startup::elapsed(timer));
62            let _ = core_db::queries::server_logs::log_startup(
63                &_db,
64                startup::elapsed(timer).as_millis() as i64,
65            )
66            .await;
67        }
68        Err(e) => {
69            startup::print_step("Initializing schema", false, startup::elapsed(timer));
70            eprintln!("{}", format!("  Error: {}", e).red());
71            let _ = core_db::queries::server_logs::log_error(&_db, e.to_string(), 0).await;
72            std::process::exit(1);
73        }
74    }
75
76    startup::print_ready(config.server_port);
77
78    // Tracks true server uptime from this point forward, used by server:status and server:shutdown.
79    // Intentionally created after all startup steps have completed.
80    let server_start = Instant::now();
81
82    // Shutdown channel — CLI sends true on server:shutdown, main select! receives it
83    let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
84
85    // Start the router
86    let app = create_router(_db.clone());
87    let host = config
88        .server_host
89        .parse::<std::net::IpAddr>()
90        .expect("Invalid SERVER_HOST IP address");
91    let addr = SocketAddr::from((host, config.server_port));
92    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
93    let server = axum::serve(
94        listener,
95        app.into_make_service_with_connect_info::<SocketAddr>(),
96    );
97
98    let mut server_task = tokio::spawn(async move {
99        server
100            .with_graceful_shutdown(async move {
101                let _ = shutdown_rx.changed().await;
102            })
103            .await
104    });
105
106    let health_url = format!(
107        "http://{}:{}/health",
108        config.server_host, config.server_port
109    );
110    let client = Client::new();
111    let mut is_ready = false;
112    let max_retries = 20;
113    let retry_delay = Duration::from_millis(50);
114
115    for _ in 0..max_retries {
116        match client.get(&health_url).send().await {
117            Ok(response) if response.status().is_success() => {
118                is_ready = true;
119                break;
120            }
121            Ok(_) => {
122                tokio::time::sleep(retry_delay).await;
123                continue;
124            }
125            Err(_) => {
126                tokio::time::sleep(retry_delay).await;
127                continue;
128            }
129        }
130    }
131
132    if !is_ready {
133        eprintln!("{}", "Server failed to start in time.".red());
134        std::process::exit(1);
135    }
136
137    // Run the tests if enabled
138    if config.enable_testing {
139        tests::run_all_tests().await;
140    }
141
142    // Start the CLI input loop in a background task
143    cli::spawn_cli(_db.clone(), server_start, shutdown_tx).await;
144
145    #[cfg(not(debug_assertions))]
146    {
147        loop {
148            tokio::select! {
149                _ = &mut server_task => {
150                    break;
151            }
152
153               result = tokio::signal::ctrl_c() => {
154                    match result {
155                        Ok(()) => {
156                            println!(
157                                "\nYou cannot shut down the server with Ctrl+C. Use server:shutdown."
158                            );
159                        },
160                        Err(_) => {
161                            println!("{}", "Failed to listen for Ctrl+C".red());
162                        }
163                    }
164                }
165            }
166        }
167    }
168
169    #[cfg(debug_assertions)]
170    {
171        let shutdown = async {
172            let _ = tokio::signal::ctrl_c().await;
173        };
174
175        tokio::select! {
176            _ = &mut server_task => {},
177            _ = shutdown => {
178                println!("\nShutting down...");
179                let _ = quorum_core::db::queries::server_logs::log_shutdown(&_db, server_start.elapsed().as_millis() as i64).await;
180            }
181        }
182    }
183}