use std::{collections::HashMap, path::Path}; use actix::SystemService; use actix_web::{get, web, App, HttpResponse, HttpServer}; use clap::crate_version; use rust_embed::RustEmbed; use sea_orm::{prelude::*, Database}; use tokio::sync::Mutex; use tracing::{info, instrument}; use tracing_subscriber::prelude::*; mod api; mod auth; mod entity; mod error; mod events; mod socket_session; #[cfg(all(target_env = "musl", target_pointer_width = "64"))] #[global_allocator] static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; #[derive(Debug)] pub struct AppState { pub db: DatabaseConnection, pub healthcheck_status: Mutex>, } #[actix_rt::main] async fn main() { let opts = clap::App::new("Vade Mecum") .version(crate_version!()) .arg( clap::Arg::new("port") .short('p') .env("VADE_PORT") .long("port") .value_name("number") .default_value("8089") .help("Set the port for the HTTP server") .takes_value(true), ) .arg( clap::Arg::new("db") .env("VADE_DB") .long("db") .value_name("path") .default_value("./") .help("Sets the path to the database location") .takes_value(true), ) .get_matches(); let subscriber = tracing_subscriber::registry().with( tracing_subscriber::fmt::Layer::new() .pretty() .with_writer(std::io::stdout) .with_ansi(true) .with_filter(tracing_subscriber::filter::LevelFilter::INFO), ); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector"); let db = setup_database(opts.value_of("db").unwrap_or_default()) .await .unwrap(); let state = web::Data::new(AppState { db, healthcheck_status: Mutex::new(HashMap::new()), }); info!( "Starting http server on {}", opts.value_of("port").unwrap_or_default() ); let st = state.clone(); actix_rt::spawn(async move { loop { tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; let apps: Vec = entity::application::Entity::find() .filter(entity::application::Column::EnableHealthcheck.eq(true)) .all(&st.db) .await .unwrap(); for app in apps { match reqwest::get(app.url).await { Ok(res) if res.status() == 200 => { st.healthcheck_status.lock().await.insert(app.id, true); let _ = events::EventBroker::from_registry() .send(events::Event::HealthcheckChange { app_id: app.id, alive: true, }) .await; } _ => { st.healthcheck_status.lock().await.insert(app.id, false); let _ = events::EventBroker::from_registry() .send(events::Event::HealthcheckChange { app_id: app.id, alive: false, }) .await; } } } } }); let listen_host = format!("0.0.0.0:{}", opts.value_of("port").unwrap_or_default()); HttpServer::new(move || { let cors = actix_cors::Cors::permissive(); App::new() .app_data(state.clone()) .wrap(tracing_actix_web::TracingLogger::default()) .wrap(cors) .service(web::resource("/events/{token}").to(socket_session::event_session_index)) .service(api::routes()) .service(dist) }) .bind(listen_host) .unwrap() .run() .await .expect("Couldnt launch server"); } #[derive(RustEmbed)] #[folder = "dist"] struct UIAssets; #[get("/{filename:.*}")] async fn dist(path: web::Path) -> HttpResponse { let path = if UIAssets::get(&*path).is_some() { &*path } else { "index.html" }; let content = UIAssets::get(path).unwrap(); let body: actix_web::body::BoxBody = match content { std::borrow::Cow::Borrowed(bytes) => actix_web::body::BoxBody::new(bytes), std::borrow::Cow::Owned(bytes) => actix_web::body::BoxBody::new(bytes), }; HttpResponse::Ok() .content_type(mime_guess::from_path(path).first_or_octet_stream().as_ref()) .body(body) } #[instrument] async fn setup_database(db_path: &str) -> error::Result { let db_fname = "data.db"; if !Path::new(db_path).join(db_fname).exists() { std::fs::File::create(db_fname)?; } let pool = sqlx::SqlitePool::connect("sqlite://data.db").await?; sqlx::migrate!("./migrations").run(&pool).await?; tracing::info!("Database migrated"); Ok(Database::connect("sqlite://data.db").await?) }