185 lines
6.0 KiB
Rust
185 lines
6.0 KiB
Rust
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<HashMap<i32, bool>>,
|
|
}
|
|
|
|
#[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 {
|
|
let apps: Vec<entity::application::Model> = entity::application::Entity::find()
|
|
.filter(entity::application::Column::EnableHealthcheck.eq(true))
|
|
.all(&st.db)
|
|
.await
|
|
.unwrap();
|
|
|
|
let client = reqwest::Client::builder()
|
|
.danger_accept_invalid_certs(true)
|
|
.build()
|
|
.unwrap();
|
|
|
|
for app in apps {
|
|
match client
|
|
.request(reqwest::Method::GET, app.url)
|
|
.timeout(std::time::Duration::from_secs(5))
|
|
.send()
|
|
.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;
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!("Error performing healthcheck: {}", e);
|
|
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;
|
|
}
|
|
Ok(res) => {
|
|
tracing::warn!("Non 200 status code: {}", res.status());
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
tokio::time::sleep(tokio::time::Duration::from_secs(60)).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()
|
|
.wrap(cors)
|
|
.app_data(state.clone())
|
|
.wrap(tracing_actix_web::TracingLogger::default())
|
|
.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<String>) -> 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<DatabaseConnection> {
|
|
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?)
|
|
}
|