use app::state::AppState; use anyhow::{Context, Result}; use axum::Router; use tokio::signal; use tokio::task::{AbortHandle, JoinHandle}; use tower_http::{ trace::TraceLayer, }; use tracing::info; use tracing_subscriber::EnvFilter; mod app_state; mod db; mod error; mod session; mod auth; mod static_routes; mod app; //NOTE TO FUTURE ME: I'm leaving a bunch of notes about these things as part of the learning // process. There is a lot of implementation details that are obscured by all these pieces, and it // can be hard to tell how heavy a line is. Lots of comment in this file and some of the kind of // "first breadcrumb" type files so you have a place to pick up the threads. Hopefully you remember // this stuff, but in case you don't here's some notes #[tokio::main] async fn main() -> Result<()>{ // Load local environment variables from a .env file // Use the regular std::env::var to use let env_status = match dotenvy::from_filename(".env") { Ok(_) => { "found local .env file" } Err(_) => { "no local .env file" } }; // Create a subscriber that will turn tracing events into // console logs. Use macros (tracing::info!, tracing::error!) // to create events in the code // Set default environment variable to set the level // Example "RUST_LOG=debug,tower_http=warn" tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .compact() .init(); info!("{}", env_status); // Application database. What you would expect. Access // through the application state let db_file = std::env::var("DATABASE_URI") .context("DATABASE_URI not set")?; let db = db::init(&db_file).await?; // OAUTH2 Client let oauth_client = auth::init_client()?; // Application state let app_state = AppState { db, oauth_client }; // Session let (session_layer, session_task) = session::init().await?; // Long-running tasks let mut tasks = vec![]; tasks.push(session_task); // Assemble all the routes to the various handlers let auth_routes = auth::routes(); let static_routes = static_routes::routes(); let error_routes = error::routes(); let app_routes = app::routes(); // Top level router let router = Router::new() .merge(auth_routes) .merge(error_routes) .merge(app_routes) .merge(static_routes) .layer(session_layer) .layer(TraceLayer::new_for_http()) .fallback(error::not_found) .with_state(app_state); // Serve let address = "0.0.0.0:4206"; let listener = tokio::net::TcpListener::bind(address) .await .context("failed to bind")?; info!("listening on {}", address); axum::serve(listener, router.into_make_service()) .with_graceful_shutdown(shutdown_signal(tasks)) .await.context("unable to serve")?; Ok(()) } /// This is needed to handle the shutdown of any long-running tasks /// such as the one that clears expired sessions. This just /// functions by listening for the termination signal--either /// ctrl-c or SIGTERM--triggering the abort handle for each /// task and then joining (awaiting) each handle async fn shutdown_signal(tasks: Vec>>) { let abort_handles: Vec = tasks.iter().map(|h| h.abort_handle()).collect(); let ctrl_c = async { signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, }; info!("Shutdown signalled"); for handle in abort_handles { handle.abort(); } for handle in tasks { let _ = handle.await; } info!("All processes finished"); }