mod atproto; mod camo; mod card; mod config; mod db; mod flare; mod matches; mod png_util; mod proxy; mod routes; mod session; mod share; mod unfurl; mod units; use std::sync::Arc; use axum::serve::{Listener, ListenerExt}; use crate::config::Config; use crate::routes::AppState; #[tokio::main] async fn main() { // Logging discipline: manifest URLs, tokens and authorization codes never // appear in log output at any level — log that a thing failed, not the // values involved. tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), ) .init(); tracing::info!( build = %std::env::var("BUILD_REF").unwrap_or_else(|_| "unknown".into()), "headquarters-api starting" ); let config = match Config::from_env() { Ok(config) => config, Err(e) => { tracing::error!("configuration: {e}"); std::process::exit(1); } }; let db = match db::Db::open(&config.db_path) { Ok(db) => db, Err(e) => { tracing::error!("database: {e}"); std::process::exit(1); } }; let atproto = match atproto::Atproto::new(&config, db.clone()) { Ok(atproto) => atproto, Err(e) => { tracing::error!("atproto: {e}"); std::process::exit(1); } }; tracing::info!( "oauth client: {}", if atproto.is_confidential() { "confidential" } else { "loopback (development)" } ); let matches = matches::Matches::new(&config, db.clone()) .await .map(Arc::new); tracing::info!( "match launching: {}", if matches.is_some() { "configured" } else { "not configured" } ); let lobbies = Arc::new(matches::lobby::Lobbies::new(db.clone())); // Fetched here rather than on the first report, so a deployment says at // startup whether its pages will draw real machines or silhouettes. let units = units::load(config.unit_index_url.as_deref(), &config.web_origin).await; let state = AppState { atproto: Arc::new(atproto), lobbies, db, signer: Arc::new(session::SessionSigner::new( config.session_secret.clone(), config.public_url.is_some(), config.cookie_domain.clone(), )), web_origin: config.web_origin.clone(), share_origin: config.share_origin.clone(), matches, build_ref: config.build_ref.clone(), arena_version: config.arena_version.clone(), flare_space: config.flare_space.clone(), units, }; let listener = tokio::net::TcpListener::bind(config.bind_addr) .await .unwrap_or_else(|e| panic!("cannot bind {}: {e}", config.bind_addr)) .tap_io(|stream| { if let Err(e) = stream.set_nodelay(true) { tracing::debug!("set_nodelay failed: {e}"); } }); // The bound address, not the configured one: BIND_ADDR=127.0.0.1:0 asks // the OS for whichever port is free, which is what scripts/dev-instance.sh // relies on this line to report - logging config.bind_addr back would // always print :0 and never say which port actually opened. let bound = listener .local_addr() .map(|a| a.to_string()) .unwrap_or_else(|_| config.bind_addr.to_string()); tracing::info!("listening on {bound}"); axum::serve(listener, routes::app(state)) .with_graceful_shutdown(shutdown_signal()) .await .expect("server error"); } /// Resolves on SIGINT or SIGTERM, so the container stops promptly instead of /// waiting out docker's kill timeout. async fn shutdown_signal() { let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("cannot install SIGTERM handler"); tokio::select! { _ = tokio::signal::ctrl_c() => {} _ = sigterm.recv() => {} } tracing::info!("shutting down"); }