jacquardSnapshot

← snapshot

5853 bytes
//! The jac-serve binary: an in-memory jacquard host on one port.
//!
//! ```text
//! cargo run -p jac-serve                 # empty state, port 8787
//! cargo run -p jac-serve -- --seed-demo  # start with the walkthrough seeded
//! JAC_SERVE_PORT=9000 cargo run -p jac-serve
//!
//! # Import a shelf of git repositories and write them to disk, then exit.
//! cargo run -p jac-serve -- --import-dir ~/github/one --out ~/jack/one
//! cargo run -p jac-serve -- --import-dir ~/github/one --out ~/jack/one \
//!     --commits 25 --paths 400
//!
//! # Serve the repos that live in a directory.
//! cargo run -p jac-serve -- --host-dir ~/jack/one
//! ```
//!
//! ## On hosting, honestly
//!
//! `--host-dir` reads repositories off disk at boot and serves them from
//! memory. It does not write back. Anything created while the process runs —
//! an attestation, a remark — lives as long as the process does and no longer.
//!
//! That is a real constraint when this is deployed rather than run locally: a
//! container restart returns every repo to exactly what is on disk, and two
//! replicas serve identical histories but diverge the moment somebody writes
//! to one. The surface says so rather than implying durability it does not
//! have. Persistent stores remain the named unblock condition.

use std::sync::Arc;

use jac_serve::state::AppState;
use jac_telemetry::{S10Config, Telemetry};

/// `--name value`, absent when the flag is not given.
fn flag(args: &[String], name: &str) -> Option<String> {
    args.iter()
        .position(|a| a == name)
        .and_then(|i| args.get(i + 1))
        .cloned()
}

/// A leading `~` is the shell's job, and this may not run under one.
fn expand(path: &str) -> String {
    match path.strip_prefix("~/") {
        Some(rest) => {
            std::env::var("HOME").map_or_else(|_| path.to_owned(), |home| format!("{home}/{rest}"))
        }
        None => path.to_owned(),
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let telemetry = Telemetry::start(S10Config::from_env(), "jac-serve");
    if telemetry.enabled() {
        println!("telemetry: reporting to s10");
    } else {
        println!("telemetry: disabled (set S10_INGEST_URL and S10_INGEST_KEY to enable)");
    }
    let state = Arc::new(AppState::with_telemetry(telemetry));

    // Batch import: does its work and exits without ever binding a port.
    let args: Vec<String> = std::env::args().collect();
    if let Some(root) = flag(&args, "--import-dir") {
        let out = flag(&args, "--out").unwrap_or_else(|| "./jack".to_owned());
        let bounds = jac_serve::gitimport::Bounds {
            commits: flag(&args, "--commits")
                .and_then(|v| v.parse().ok())
                .unwrap_or(20),
            paths: flag(&args, "--paths")
                .and_then(|v| v.parse().ok())
                .unwrap_or(300),
        };
        let root = expand(&root);
        let out = expand(&out);
        println!("importing every git repo under {root}");
        println!(
            "  bounds: {} commits, {} paths each",
            bounds.commits, bounds.paths
        );
        let index = jac_serve::batch::shelf(
            std::path::Path::new(&root),
            std::path::Path::new(&out),
            &bounds,
        )?;
        println!("{}", serde_json::to_string_pretty(&index["provenance"])?);
        println!(
            "imported {} repos ({} failed) -> {out}",
            index["imported"], index["failed"]
        );
        return Ok(());
    }

    if std::env::args().any(|a| a == "--seed-demo") {
        jac_serve::demo::seed(&state).map_err(|e| anyhow::anyhow!("demo seed failed: {e}"))?;
        println!("seeded the demo arc: meridian-systems + halcyon-works");
    }

    // Repos that live on disk. Read once at boot; see the module docs for what
    // that does and does not promise.
    if let Some(root) = flag(&args, "--host-dir").or_else(|| std::env::var("JAC_HOST_DIR").ok()) {
        let root = expand(&root);
        let dirs = jac_serve::store::discover(std::path::Path::new(&root));
        if dirs.is_empty() {
            println!("no jacquard repos under {root} — serving empty");
        } else {
            let mut loaded = 0usize;
            let mut failed = 0usize;
            let mut mismatched = 0usize;
            for dir in &dirs {
                match jac_serve::store::load(dir) {
                    Ok(repo) => {
                        mismatched += repo.mismatched.len();
                        if jac_serve::store::host(&state, repo).is_ok() {
                            loaded += 1;
                        } else {
                            failed += 1;
                        }
                    }
                    Err(_) => failed += 1,
                }
            }
            println!("hosting {loaded} repos from {root} ({failed} unreadable)");
            if mismatched > 0 {
                // Loud on purpose: the file and the engine disagree about what
                // that history is, which is not a rounding error.
                println!(
                    "WARNING: {mismatched} snapshots did not replay to their recorded address"
                );
            }
        }
    }

    let port = std::env::var("JAC_SERVE_PORT")
        .or_else(|_| std::env::var("PORT"))
        .ok()
        .and_then(|p| p.parse::<u16>().ok())
        .unwrap_or(8787);

    let app = jac_serve::app(state);
    let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
    println!("jac-serve listening on http://localhost:{port}");
    println!("honest scope: identities are declared, not authenticated; nothing persists");

    axum::serve(listener, app)
        .with_graceful_shutdown(async {
            let _ = tokio::signal::ctrl_c().await;
        })
        .await?;
    Ok(())
}