use scopenet_panel::{app, bootstrap_admin, build_state, config::Config, db}; use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() -> anyhow::Result<()> { // `scopenet-panel healthcheck` — used by the Docker HEALTHCHECK so the // image doesn't need curl. if std::env::args().nth(1).as_deref() == Some("healthcheck") { return healthcheck().await; } tracing_subscriber::fmt() .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,tower_http=warn,sqlx=warn"))) .compact() .init(); let cfg = Config::from_env(); let pool = db::connect(&cfg.data_dir).await?; if !cfg.web_dir.join("index.html").exists() { tracing::warn!("web UI not found at {} (build panel/web or set SCOPENET_WEB_DIR)", cfg.web_dir.display()); } let bind = cfg.bind.clone(); let state = build_state(cfg, pool).await?; bootstrap_admin(&state).await?; scopenet_panel::routes::discord::spawn_worker(state.clone()); // Earned rewards are applied within seconds even when no game server happens to be polling. { let state = state.clone(); tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_secs(5)).await; if let Err(e) = scopenet_panel::rewards::process_queue(&state).await { tracing::warn!("reward queue failed: {:?}", e.message); } } }); } let listener = tokio::net::TcpListener::bind(&bind).await?; tracing::info!("SCOPENET panel v{} listening on http://{bind}", env!("CARGO_PKG_VERSION")); axum::serve(listener, app(state).into_make_service_with_connect_info::()) .with_graceful_shutdown(shutdown()) .await?; Ok(()) } async fn shutdown() { let ctrl_c = async { tokio::signal::ctrl_c().await.ok(); }; #[cfg(unix)] let term = async { if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { s.recv().await; } }; #[cfg(not(unix))] let term = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = term => {} } tracing::info!("shutting down"); } async fn healthcheck() -> anyhow::Result<()> { use tokio::io::{AsyncReadExt, AsyncWriteExt}; let bind = std::env::var("SCOPENET_BIND").unwrap_or_else(|_| "0.0.0.0:8080".into()); let port = bind.rsplit(':').next().unwrap_or("8080"); let mut s = tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")).await?; s.write_all(b"GET /healthz HTTP/1.0\r\nHost: localhost\r\n\r\n").await?; let mut buf = String::new(); s.read_to_string(&mut buf).await?; if buf.starts_with("HTTP/1.1 200") || buf.starts_with("HTTP/1.0 200") { Ok(()) } else { anyhow::bail!("unhealthy: {}", buf.lines().next().unwrap_or("")) } }