rns-server 0.1.0

Batteries-included Reticulum node server
use std::thread;
use std::time::Duration;

use rns_cli::args::Args as CliArgs;
use rns_ctl::cmd::http::{prepare_embedded_with_state, HttpRunOptions};
use rns_ctl::state::SharedState;
use rns_server::args::Args;
use rns_server::config::ServerConfig;
use rns_server::control_plane::{install_config_bridge, new_supervised_state};
use rns_server::supervisor::Supervisor;

fn main() {
    let args = Args::parse();

    if let Some(role) = args.get("internal-role") {
        run_internal_role(role);
    }

    if args.has("version") {
        println!("rns-server {}", env!("FULL_VERSION"));
        return;
    }

    if args.has("help") || args.positional.is_empty() {
        print_help();
        return;
    }

    init_logging(&args);

    match args.positional[0].as_str() {
        "start" => run_start(args),
        other => {
            eprintln!("Unknown subcommand: {}", other);
            print_help();
            std::process::exit(1);
        }
    }
}

fn run_internal_role(role: &str) -> ! {
    let child_args = CliArgs::parse_from(sanitized_internal_argv());
    match role {
        "rnsd" => rns_cli::rnsd::main_entry_from(child_args),
        "rns-sentineld" => rns_cli::sentineld::main_entry_from(child_args),
        "rns-statsd" => rns_cli::statsd::main_entry_from(child_args),
        other => {
            eprintln!("rns-server: unknown internal role '{}'", other);
            std::process::exit(1);
        }
    }
    std::process::exit(0);
}

fn sanitized_internal_argv() -> Vec<String> {
    let mut args = std::env::args().skip(1);
    let mut sanitized = Vec::new();
    while let Some(arg) = args.next() {
        if arg == "--internal-role" {
            let _ = args.next();
            continue;
        }
        sanitized.push(arg);
    }
    sanitized
}

fn run_start(args: Args) {
    let (shared_state, _control_tx, control_rx) = new_supervised_state();
    let config = ServerConfig::from_args(&args);
    if let Err(err) = config.ensure_runtime_bootstrap() {
        eprintln!("rns-server: {}", err);
        std::process::exit(1);
    }
    install_config_bridge(&shared_state, &args, &config);
    let dry_run = args.has("dry-run");

    if dry_run {
        let supervisor = Supervisor::new(config.supervisor_config(None, None));
        for spec in supervisor.specs() {
            println!("{}", spec.command_line());
        }
        if config.http_enabled() {
            println!("{}", config.control_http_command_line());
        }
        return;
    }

    let supervisor =
        Supervisor::new(config.supervisor_config(Some(shared_state.clone()), Some(control_rx)));

    match supervisor.run_with_started_hook(|| {
        if config.http_enabled() {
            start_control_http(&config, args.verbosity, shared_state.clone())?;
        }
        Ok(())
    }) {
        Ok(code) => std::process::exit(code),
        Err(err) => {
            eprintln!("rns-server: {}", err);
            std::process::exit(1);
        }
    }
}

fn init_logging(args: &Args) {
    let log_level = if args.quiet > 0 {
        match args.quiet {
            1 => log::LevelFilter::Warn,
            _ => log::LevelFilter::Error,
        }
    } else {
        match args.verbosity {
            0 => log::LevelFilter::Info,
            1 => log::LevelFilter::Debug,
            _ => log::LevelFilter::Trace,
        }
    };

    env_logger::Builder::new()
        .filter_level(log_level)
        .format_timestamp_secs()
        .init();
}

fn start_control_http(
    config: &ServerConfig,
    verbosity: u8,
    shared_state: SharedState,
) -> Result<(), String> {
    let config = config.clone();
    log::info!("starting embedded control plane");
    thread::Builder::new()
        .name("rns-server-http".into())
        .spawn(move || {
            for attempt in 1..=50 {
                match prepare_embedded_with_state(
                    config.ctl_args(verbosity),
                    HttpRunOptions::embedded(),
                    Some(shared_state.clone()),
                ) {
                    Ok(prepared) => {
                        if let Err(err) = rns_ctl::server::run_server(prepared.addr, prepared.ctx) {
                            log::error!("embedded control plane failed: {}", err);
                        }
                        return;
                    }
                    Err(err) => {
                        if attempt == 50 {
                            log::error!("embedded control plane failed: {}", err);
                            return;
                        }
                        log::debug!(
                            "embedded control plane not ready yet (attempt {}): {}",
                            attempt,
                            err
                        );
                        thread::sleep(Duration::from_millis(200));
                    }
                }
            }
        })
        .map_err(|e| format!("failed to spawn control plane thread: {}", e))?;
    Ok(())
}

fn print_help() {
    println!(
        "rns-server - batteries-included Reticulum node server

USAGE:
    rns-server start [OPTIONS]

OPTIONS:
    -c, --config PATH        Path to config directory
        --stats-db PATH      Path to stats SQLite database
        --rnsd-bin PATH      Advanced override for rnsd executable
        --sentineld-bin PATH Advanced override for rns-sentineld executable
        --statsd-bin PATH    Advanced override for rns-statsd executable
        --http-host HOST     Host for embedded control HTTP server
        --http-port PORT     Port for embedded control HTTP server
        --http-token TOKEN   Auth token for embedded control HTTP server
        --disable-auth       Disable auth on embedded control HTTP server
        --no-http            Disable the embedded control HTTP server
        --dry-run            Print the child process plan and exit
    -v                       Increase verbosity (repeat for more)
    -q                       Decrease verbosity (repeat for more)
    -h, --help               Show this help
        --version            Show version"
    );
}