//! todo-rust: the CARLOS bakeoff's TodoMVC, in Rust. //! //! todo-rust --socket --db (on Carloku) //! todo-rust --listen 127.0.0.1: --db (locally) use todo_rust::{app, db}; use std::net::SocketAddr; use std::path::PathBuf; use std::process::ExitCode; use tokio::signal::unix::{SignalKind, signal}; // musl's allocator is slow under many threads; mimalloc is a drop-in fix. #[global_allocator] static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; /// The git short SHA, stamped in by `make` (see the Makefile). const VERSION: &str = match option_env!("APP_VERSION") { Some(v) => v, None => "dev", }; enum Bind { Socket(PathBuf), Listen(SocketAddr), } struct Args { bind: Bind, db: PathBuf, } fn parse_args() -> Result { let mut socket = None; let mut listen = None; let mut db = None; let mut it = std::env::args().skip(1); while let Some(flag) = it.next() { let mut value = || it.next().ok_or_else(|| format!("{flag} needs a value")); match flag.as_str() { "--socket" => socket = Some(PathBuf::from(value()?)), "--listen" => { let addr: SocketAddr = value()?.parse().map_err(|e| format!("--listen: {e}"))?; if !addr.ip().is_loopback() { return Err("--listen only binds a loopback address, e.g. 127.0.0.1:8080".into()); } listen = Some(addr); } "--db" => db = Some(PathBuf::from(value()?)), "--version" => { println!("{VERSION}"); std::process::exit(0); } other => return Err(format!("unknown argument {other}")), } } let bind = match (socket, listen) { (Some(path), None) => Bind::Socket(path), (None, Some(addr)) => Bind::Listen(addr), _ => return Err("pass exactly one of --socket or --listen 127.0.0.1:".into()), }; let db = db.ok_or("--db is required")?; Ok(Args { bind, db }) } /// The origin browsers will send with our own form posts. fn own_origin(bind: &Bind) -> String { match bind { Bind::Listen(addr) => format!("http://{addr}"), Bind::Socket(_) => match std::env::var("CARLOS_HOST") { Ok(host) if !host.is_empty() => format!("https://{host}"), _ => { eprintln!("CARLOS_HOST is not set; every cross-origin POST will be refused"); "https://localhost".into() } }, } } async fn shutdown() { let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler"); let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler"); tokio::select! { _ = term.recv() => {} _ = int.recv() => {} } } #[tokio::main] async fn main() -> ExitCode { let args = match parse_args() { Ok(a) => a, Err(e) => { eprintln!("todo-rust: {e}"); return ExitCode::from(2); } }; let db = match db::Db::open(&args.db) { Ok(db) => db, Err(e) => { eprintln!("todo-rust: opening {}: {e}", args.db.display()); return ExitCode::FAILURE; } }; let state = app::AppState { db, origin: own_origin(&args.bind), version: VERSION }; let router = app::router(state); let served = match &args.bind { Bind::Listen(addr) => match tokio::net::TcpListener::bind(addr).await { Ok(l) => { eprintln!("todo-rust {VERSION} listening on http://{addr}"); axum::serve(l, router).with_graceful_shutdown(shutdown()).await } Err(e) => Err(e), }, Bind::Socket(path) => { // A socket left behind by an earlier run would make bind fail. let _ = std::fs::remove_file(path); match tokio::net::UnixListener::bind(path) { Ok(l) => { eprintln!("todo-rust {VERSION} listening on {}", path.display()); let r = axum::serve(l, router).with_graceful_shutdown(shutdown()).await; let _ = std::fs::remove_file(path); r } Err(e) => Err(e), } } }; match served { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("todo-rust: {e}"); ExitCode::FAILURE } } }