| 1 | //! todo-rust: the CARLOS bakeoff's TodoMVC, in Rust. |
| 2 | //! |
| 3 | //! todo-rust --socket <path> --db <path> (on Carloku) |
| 4 | //! todo-rust --listen 127.0.0.1:<port> --db <path> (locally) |
| 5 | |
| 6 | use todo_rust::{app, db}; |
| 7 | |
| 8 | use std::net::SocketAddr; |
| 9 | use std::path::PathBuf; |
| 10 | use std::process::ExitCode; |
| 11 | |
| 12 | use tokio::signal::unix::{SignalKind, signal}; |
| 13 | |
| 14 | // musl's allocator is slow under many threads; mimalloc is a drop-in fix. |
| 15 | #[global_allocator] |
| 16 | static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; |
| 17 | |
| 18 | /// The git short SHA, stamped in by `make` (see the Makefile). |
| 19 | const VERSION: &str = match option_env!("APP_VERSION") { |
| 20 | Some(v) => v, |
| 21 | None => "dev", |
| 22 | }; |
| 23 | |
| 24 | enum Bind { |
| 25 | Socket(PathBuf), |
| 26 | Listen(SocketAddr), |
| 27 | } |
| 28 | |
| 29 | struct Args { |
| 30 | bind: Bind, |
| 31 | db: PathBuf, |
| 32 | } |
| 33 | |
| 34 | fn parse_args() -> Result<Args, String> { |
| 35 | let mut socket = None; |
| 36 | let mut listen = None; |
| 37 | let mut db = None; |
| 38 | let mut it = std::env::args().skip(1); |
| 39 | while let Some(flag) = it.next() { |
| 40 | let mut value = || it.next().ok_or_else(|| format!("{flag} needs a value")); |
| 41 | match flag.as_str() { |
| 42 | "--socket" => socket = Some(PathBuf::from(value()?)), |
| 43 | "--listen" => { |
| 44 | let addr: SocketAddr = value()?.parse().map_err(|e| format!("--listen: {e}"))?; |
| 45 | if !addr.ip().is_loopback() { |
| 46 | return Err("--listen only binds a loopback address, e.g. 127.0.0.1:8080".into()); |
| 47 | } |
| 48 | listen = Some(addr); |
| 49 | } |
| 50 | "--db" => db = Some(PathBuf::from(value()?)), |
| 51 | "--version" => { |
| 52 | println!("{VERSION}"); |
| 53 | std::process::exit(0); |
| 54 | } |
| 55 | other => return Err(format!("unknown argument {other}")), |
| 56 | } |
| 57 | } |
| 58 | let bind = match (socket, listen) { |
| 59 | (Some(path), None) => Bind::Socket(path), |
| 60 | (None, Some(addr)) => Bind::Listen(addr), |
| 61 | _ => return Err("pass exactly one of --socket <path> or --listen 127.0.0.1:<port>".into()), |
| 62 | }; |
| 63 | let db = db.ok_or("--db <path> is required")?; |
| 64 | Ok(Args { bind, db }) |
| 65 | } |
| 66 | |
| 67 | /// The origin browsers will send with our own form posts. |
| 68 | fn own_origin(bind: &Bind) -> String { |
| 69 | match bind { |
| 70 | Bind::Listen(addr) => format!("http://{addr}"), |
| 71 | Bind::Socket(_) => match std::env::var("CARLOS_HOST") { |
| 72 | Ok(host) if !host.is_empty() => format!("https://{host}"), |
| 73 | _ => { |
| 74 | eprintln!("CARLOS_HOST is not set; every cross-origin POST will be refused"); |
| 75 | "https://localhost".into() |
| 76 | } |
| 77 | }, |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | async fn shutdown() { |
| 82 | let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler"); |
| 83 | let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler"); |
| 84 | tokio::select! { |
| 85 | _ = term.recv() => {} |
| 86 | _ = int.recv() => {} |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | #[tokio::main] |
| 91 | async fn main() -> ExitCode { |
| 92 | let args = match parse_args() { |
| 93 | Ok(a) => a, |
| 94 | Err(e) => { |
| 95 | eprintln!("todo-rust: {e}"); |
| 96 | return ExitCode::from(2); |
| 97 | } |
| 98 | }; |
| 99 | let db = match db::Db::open(&args.db) { |
| 100 | Ok(db) => db, |
| 101 | Err(e) => { |
| 102 | eprintln!("todo-rust: opening {}: {e}", args.db.display()); |
| 103 | return ExitCode::FAILURE; |
| 104 | } |
| 105 | }; |
| 106 | let state = app::AppState { db, origin: own_origin(&args.bind), version: VERSION }; |
| 107 | let router = app::router(state); |
| 108 | |
| 109 | let served = match &args.bind { |
| 110 | Bind::Listen(addr) => match tokio::net::TcpListener::bind(addr).await { |
| 111 | Ok(l) => { |
| 112 | eprintln!("todo-rust {VERSION} listening on http://{addr}"); |
| 113 | axum::serve(l, router).with_graceful_shutdown(shutdown()).await |
| 114 | } |
| 115 | Err(e) => Err(e), |
| 116 | }, |
| 117 | Bind::Socket(path) => { |
| 118 | // A socket left behind by an earlier run would make bind fail. |
| 119 | let _ = std::fs::remove_file(path); |
| 120 | match tokio::net::UnixListener::bind(path) { |
| 121 | Ok(l) => { |
| 122 | eprintln!("todo-rust {VERSION} listening on {}", path.display()); |
| 123 | let r = axum::serve(l, router).with_graceful_shutdown(shutdown()).await; |
| 124 | let _ = std::fs::remove_file(path); |
| 125 | r |
| 126 | } |
| 127 | Err(e) => Err(e), |
| 128 | } |
| 129 | } |
| 130 | }; |
| 131 | match served { |
| 132 | Ok(()) => ExitCode::SUCCESS, |
| 133 | Err(e) => { |
| 134 | eprintln!("todo-rust: {e}"); |
| 135 | ExitCode::FAILURE |
| 136 | } |
| 137 | } |
| 138 | } |
| 139 | |