#[macro_use] extern crate dotenv_codegen; extern crate dotenv; use dotenv::dotenv; use tokio::time::sleep; use std::{env, error::Error, fs, net::Ipv4Addr, net::Ipv6Addr, thread, time::Duration, sync::{Mutex, Arc, atomic::{AtomicBool, Ordering}}}; use chrono::Local; use actix_web::{middleware::Logger, web, App, HttpServer, Result}; use serde::{Deserialize, Serialize}; use sqlx::{PgPool, Pool, Postgres, Connection}; use utoipa::OpenApi; use utoipa_swagger_ui::{Config, SwaggerUi, Url}; mod db; mod v2; mod v3; macro_rules! api_base { () => { "/api" }; } macro_rules! api_base_2 { () => { "/api/v2" }; } macro_rules! api_base_3 { () => { "/api/v3" }; } #[derive(Serialize, Deserialize, Debug, Clone)] pub(crate) struct ConfigToml { auth: AuthenticationTokens, } #[derive(Serialize, Deserialize, Debug, Clone)] pub(crate) struct AuthenticationTokens { moderator: String, admin: String } pub struct AppState { db: Pool<Postgres>, auth_tokens: AuthenticationTokens, } async fn postgres_watchdog(pool: PgPool, is_alive: Arc<AtomicBool>, shutdown: Arc<AtomicBool>) { loop { if shutdown.load(Ordering::Relaxed) { break; } let start = Local::now(); let mut conn = match pool.acquire().await { Ok(data) => data, Err(_) => break, }; match conn.ping().await { Ok(_) => {println!("Pinged DB Server at {}", Local::now().format("%H:%M:%S"))}, Err(_) => {println!("Error pinging Server"); break;}, }; let passed = (Local::now() - start).to_std().expect(&format!("Unable to get Time Difference for '{}' and '{}'", start, Local::now())); sleep(Duration::from_secs(15) - passed).await; } is_alive.store(false, Ordering::Relaxed); } #[actix_web::main] async fn main() -> Result<()> { env_logger::init(); dotenv().ok(); let shutdown: Arc<AtomicBool> = Arc::new(AtomicBool::new(false)); let shutdown_clone = Arc::clone(&shutdown); ctrlc::set_handler(move || { eprintln!("Ctrl-C received"); shutdown_clone.store(true, Ordering::Relaxed) }) .expect("Error setting Ctrl-C handler"); let toml_str = fs::read_to_string("config.toml").expect("Failed to read config.toml"); let config: ConfigToml = toml::from_str(&toml_str).expect("Failed to parse config.toml"); println!("DATABASE_URL: {}", env::var("DATABASE_URL").unwrap()); // DBEUG #[derive(OpenApi)] #[openapi( paths( v2::empire_ethics, v2::empires, v2::ethics, v2::game_groups, v2::games, v2::portrait_groups, v2::portraits ), components(schemas( v2::schemas::PortraitGroup, v2::schemas::Portrait, v2::schemas::Ethic, v2::schemas::Empire, v2::schemas::EmpireEthic, v2::schemas::Game, v2::schemas::GameGroup )) )] struct ApiDocV2; #[derive(OpenApi)] #[openapi( paths( v3::full_view_data, v3::auth, v3::list_games, v3::get_game_data, v3::create_game, v3::delete_game ), components(schemas( v3::schemas::AuthParamsOptional, v3::schemas::AuthParams, v3::schemas::AuthReturn, v3::schemas::GetGameParam, v3::schemas::PostGameParams, v3::schemas::DeleteGameParam, v3::schemas::FullViewData, v3::schemas::Ethic, v3::schemas::EmpireEthic, v3::schemas::ChellarisGame, v3::schemas::ChellarisGameLite, v3::schemas::Species, v3::schemas::ChellarisGameGroup, v3::schemas::Portrait, v3::schemas::ChellarisEmpire )) )] struct ApiDocV3; let openapi_urls = vec![ Url::new("v2", concat!(api_base_2!(), "/openapi.json")), Url::new("v3", concat!(api_base_3!(), "/openapi.json")), ]; let is_alive: Arc<AtomicBool> = Arc::new(AtomicBool::new(true)); loop { let db_auth_tokens = config.auth.clone(); let pool = PgPool::connect(dotenv!("DATABASE_URL")).await.unwrap(); let pool_copy = pool.clone(); let swagger_config = Config::new(openapi_urls.clone()); let mut openapi_v2 = ApiDocV2::openapi(); openapi_v2.info.title = "Chellaris Rust API".to_string(); let mut openapi_v3 = ApiDocV3::openapi(); openapi_v3.info.title = "Chellaris Rust API".to_string(); let server = HttpServer::new(move || { App::new() .app_data(web::Data::new(AppState { db: pool.clone(), auth_tokens: db_auth_tokens.clone() })) .wrap(Logger::default()) // API v2 Endpoints .service(v2::empire_ethics) .service(v2::empires) .service(v2::ethics) .service(v2::game_groups) .service(v2::games) .service(v2::portrait_groups) .service(v2::portraits) // API v3 Endpoints .service(v3::full_view_data) .service(v3::auth) .service(v3::list_games) .service(v3::get_game_data) .service(v3::create_game) .service(v3::delete_game) // Swagger UI .service( SwaggerUi::new(concat!(api_base!(), "/swagger/{_:.*}")) .urls(vec![ ( Url::new("v2", concat!(api_base_2!(), "/openapi.json")), openapi_v2.clone(), ), ( Url::new("v3", concat!(api_base_3!(), "/openapi.json")), openapi_v3.clone(), ), ]) .config(swagger_config.clone()), ) }) .bind((Ipv6Addr::UNSPECIFIED, 8080)).expect("Port or IP already occupied") .run(); let server_thread = tokio::spawn(async { println!("Awaiting server"); let _ = server.await; println!("Stopped awaiting server"); }); println!("Started Serving API on: "); println!(" -> http://[{}]:{}", Ipv6Addr::UNSPECIFIED, 8080); println!(" -> http://{}:{}", Ipv4Addr::UNSPECIFIED, 8080); let is_alive_clone = Arc::clone(&is_alive); let shutdown_clone = Arc::clone(&shutdown); let _ = tokio::spawn(async move { postgres_watchdog(pool_copy, is_alive_clone, shutdown_clone).await }); //watchdog_thread.await; while is_alive.load(Ordering::Relaxed) { let thread = tokio::spawn(async { thread::sleep(Duration::from_millis(100)); }); let _ = thread.await; } if shutdown.load(Ordering::Relaxed) { break; } eprintln!("Connection died, restarting Server"); server_thread.abort(); } Ok(()) }