//! Spike 22 command-line harness.
//!
//! The VM implementation lives in the library crate. This binary parses CLI
//! flags, applies process-wide harness hooks, and calls `vmm::runner`.

use supermachine as vmm;

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn arg_value(args: &[String], i: usize, flag: &str) -> String {
    args.get(i + 1).cloned().unwrap_or_else(|| {
        eprintln!("{flag}: missing value");
        std::process::exit(2);
    })
}

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn parse_arg<T>(args: &[String], i: usize, flag: &str) -> T
where
    T: std::str::FromStr,
    T::Err: std::fmt::Display,
{
    let value = arg_value(args, i, flag);
    value.parse().unwrap_or_else(|e| {
        eprintln!("{flag}: invalid value {value:?}: {e}");
        std::process::exit(2);
    })
}

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn main() {
    env_logger::init();

    use vmm::vmm::resources::{VmProfile, VmResources, DEFAULT_CMDLINE, DEFAULT_MEMORY_MIB};
    use vmm::vmm::runner::{self, RunOptions};

    // P-core hint for vCPU 0 (the main thread enters dispatch_vcpu).
    // Secondary vCPU threads call this themselves in run_secondary.
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    vmm::vmm::worker::pin_vcpu_thread_to_pcore();

    let args: Vec<String> = std::env::args().collect();

    // Version probe — must run BEFORE any other arg handling. The
    // library spawns this binary with `--version` once on first
    // use to verify protocol compatibility (the worker and the
    // library MUST be the same crate version because the
    // supervisor protocol evolves between releases — e.g. 0.4.6
    // added BAKE_READY / SNAPSHOT_ASYNC, missing in older workers,
    // and a stale `~/.cargo/bin/supermachine-worker` deadlocks
    // pipelined-bake otherwise). Output is parsed by the lib;
    // format is stable: exactly `supermachine-worker <semver>\n`.
    if args.iter().any(|a| a == "--version") {
        println!("supermachine-worker {}", env!("CARGO_PKG_VERSION"));
        return;
    }

    // Parent-death watchdog. If the library process that spawned us
    // dies (clean exit, SIGKILL, crash, whatever), the OS reparents
    // us to init (PID 1). Poll getppid() every second; on the first
    // observation that PPID has changed away from our initial parent,
    // self-exit.
    //
    // This is the macOS-equivalent of Linux's
    // `prctl(PR_SET_PDEATHSIG)`. Disabled via
    // `SUPERMACHINE_WORKER_NO_WATCHDOG=1` (used by tests that
    // intentionally daemonize / detach).
    if std::env::var_os("SUPERMACHINE_WORKER_NO_WATCHDOG").is_none() {
        let initial_ppid = unsafe { libc::getppid() };
        // The lib often spawns via `posix_spawn` which doesn't
        // change PPID, so the value we see here matches the
        // library process. If we were started directly from a
        // shell our initial PPID is the shell — and if THAT shell
        // dies, we should also clean up.
        std::thread::Builder::new()
            .name("supermachine-watchdog".into())
            .spawn(move || loop {
                std::thread::sleep(std::time::Duration::from_secs(1));
                let current = unsafe { libc::getppid() };
                if current != initial_ppid {
                    // Parent gone (reparented, usually to init=1).
                    // Exit cleanly — the lib's worker control socket
                    // already saw us go away.
                    eprintln!(
                        "[supermachine-worker] parent {initial_ppid} exited (now reparented to {current}); self-exiting"
                    );
                    std::process::exit(0);
                }
            })
            .ok();
    }

    let mut kernel_path: Option<String> = None;
    let mut initrd_path: Option<String> = None;
    let mut cmdline = String::from(DEFAULT_CMDLINE);
    let mut memory_mib: usize = DEFAULT_MEMORY_MIB;
    let mut blk_paths: Vec<String> = Vec::new();
    // `--volume HOST_FILE:GUEST_PATH` — writable attachments,
    // opened RW and ordered after RO layers in /dev/vd*. The
    // matching guest paths are written to /.supermachine-volumes
    // by the bake pipeline; init-oci consumes them post-pivot.
    let mut volumes: Vec<vmm::vmm::resources::VolumeSpec> = Vec::new();
    let mut mounts: Vec<vmm::vmm::resources::MountSpec> = Vec::new();
    let mut snapshot_after_ms: Option<u64> = None;
    let mut snapshot_at: Option<u64> = None;
    let mut snapshot_on_listener: bool = false;
    let mut snapshot_on_pre_exec: bool = false;
    let mut snapshot_out: Option<String> = None;
    let mut restore_from: Option<String> = None;
    let mut cow_restore: bool = false;
    let mut quiesce_ms: u64 = 0;
    let mut balloon_target_pages: Option<u32> = None;
    let mut log_sink: Option<String> = None;
    let mut n_vcpus: u32 = 1;
    let mut profile: Option<VmProfile> = None;
    let mut vcpus_explicit = false;
    let mut tls_listen: Option<String> = None;
    let mut tls_vm_port: Option<u32> = None;
    let mut tls_cert: Option<String> = None;
    let mut tls_key: Option<String> = None;
    let mut env_pairs: Vec<(String, String)> = Vec::new();
    let mut env_file: Option<String> = None;
    let mut egress_policy: Option<String> = None;
    let mut vsock_mux: Option<String> = None;
    let mut vsock_mux_handoff: Option<String> = None;
    let mut vsock_exec: Option<String> = None;
    let mut vsock_exec_guest_port: Option<u32> = None;
    let mut http_port: Option<String> = None;
    let mut pool_worker: Option<String> = None;
    // TSI control-channel auth token (64 lowercase hex chars =
    // 32 bytes). When present we (a) append
    // `supermachine.tsi_token=<hex>` to the kernel cmdline so the
    // guest's af_tsi driver prepends the token to every control
    // DGRAM, and (b) hand the decoded 32 bytes to the vsock muxer
    // so it rejects any guest-userspace-forged control message.
    // See `kernel-build/patches/af-tsi/0014-*.patch`.
    let mut tsi_token_hex: Option<String> = None;

    let mut i = 1;
    while i < args.len() {
        match args[i].as_str() {
            "--kernel" => {
                kernel_path = Some(arg_value(&args, i, "--kernel"));
                i += 2;
            }
            "--initramfs" => {
                initrd_path = Some(arg_value(&args, i, "--initramfs"));
                i += 2;
            }
            "--cmdline" => {
                cmdline = arg_value(&args, i, "--cmdline");
                i += 2;
            }
            "--memory" => {
                memory_mib = parse_arg(&args, i, "--memory");
                i += 2;
            }
            "--virtio-blk" => {
                blk_paths.push(arg_value(&args, i, "--virtio-blk"));
                i += 2;
            }
            "--volume" => {
                let raw = arg_value(&args, i, "--volume");
                // Encoding: HOST:GUEST (legacy) or HOST:GUEST:SIZE_BYTES
                // (0.7.0+, size_bytes optional with default 1 GiB).
                let parts: Vec<&str> = raw.splitn(3, ':').collect();
                if parts.len() < 2 {
                    eprintln!("--volume expects HOST:GUEST[:SIZE_BYTES], got {raw:?}");
                    std::process::exit(2);
                }
                let mut spec = vmm::vmm::resources::VolumeSpec::new(parts[0], parts[1]);
                if let Some(s) = parts.get(2) {
                    match s.parse::<u64>() {
                        Ok(sz) => spec = spec.with_size_bytes(sz),
                        Err(_) => {
                            eprintln!("--volume SIZE_BYTES not a u64: {s:?}");
                            std::process::exit(2);
                        }
                    }
                }
                volumes.push(spec);
                i += 2;
            }
            "--mount" => {
                let raw = arg_value(&args, i, "--mount");
                // Encoding: `HOST:TAG:GUEST_PATH[:POLICY]`. POLICY is
                // optional; defaults to Opaque. Splits left-to-right
                // on `:` — host paths on macOS never contain `:`.
                let parts: Vec<&str> = raw.splitn(4, ':').collect();
                let (host, tag, guest_path, policy_str): (
                    &str,
                    &str,
                    &str,
                    Option<&str>,
                ) = match parts.len() {
                    3 => (parts[0], parts[1], parts[2], None),
                    4 => (parts[0], parts[1], parts[2], Some(parts[3])),
                    _ => {
                        eprintln!(
                            "--mount expects HOST:TAG:GUEST_PATH[:POLICY], got {raw:?}"
                        );
                        std::process::exit(2);
                    }
                };
                if tag.is_empty() {
                    eprintln!("--mount tag is empty: {raw:?}");
                    std::process::exit(2);
                }
                if tag.len() > 35 {
                    eprintln!(
                        "--mount tag too long (max 35 bytes, got {}): {raw:?}",
                        tag.len()
                    );
                    std::process::exit(2);
                }
                if guest_path.is_empty() {
                    eprintln!("--mount guest_path is empty: {raw:?}");
                    std::process::exit(2);
                }
                if !guest_path.starts_with('/') {
                    eprintln!(
                        "--mount guest_path must be absolute (start with `/`), got {guest_path:?}"
                    );
                    std::process::exit(2);
                }
                let policy = match policy_str {
                    None => vmm::vmm::resources::SymlinkPolicy::default(),
                    Some("deny") => vmm::vmm::resources::SymlinkPolicy::Deny,
                    Some("opaque") => vmm::vmm::resources::SymlinkPolicy::Opaque,
                    Some("follow") => vmm::vmm::resources::SymlinkPolicy::Follow,
                    Some(other) => {
                        eprintln!(
                            "--mount policy must be one of: deny, opaque, follow (got {other:?})"
                        );
                        std::process::exit(2);
                    }
                };
                mounts.push(
                    vmm::vmm::resources::MountSpec::new(host, tag, guest_path)
                        .with_symlinks(policy),
                );
                i += 2;
            }
            "--vcpus" => {
                n_vcpus = parse_arg(&args, i, "--vcpus");
                vcpus_explicit = true;
                i += 2;
            }
            "--profile" => {
                let value = arg_value(&args, i, "--profile");
                profile = VmProfile::parse(&value).or_else(|| {
                    eprintln!("--profile: expected latency or throughput, got {value:?}");
                    std::process::exit(2);
                });
                i += 2;
            }
            "--snapshot-after-ms" => {
                snapshot_after_ms = Some(parse_arg(&args, i, "--snapshot-after-ms"));
                i += 2;
            }
            "--snapshot-at" => {
                snapshot_at = Some(parse_arg(&args, i, "--snapshot-at"));
                i += 2;
            }
            "--snapshot-on-listener" => {
                snapshot_on_listener = true;
                i += 1;
            }
            "--snapshot-on-pre-exec" => {
                snapshot_on_pre_exec = true;
                i += 1;
            }
            "--snapshot-out" => {
                snapshot_out = Some(arg_value(&args, i, "--snapshot-out"));
                i += 2;
            }
            "--balloon-target-pages" => {
                balloon_target_pages =
                    Some(parse_arg::<u64>(&args, i, "--balloon-target-pages") as u32);
                i += 2;
            }
            "--restore-from" => {
                restore_from = Some(arg_value(&args, i, "--restore-from"));
                i += 2;
            }
            "--cow-restore" => {
                cow_restore = true;
                i += 1;
            }
            "--quiesce-ms" => {
                quiesce_ms = parse_arg(&args, i, "--quiesce-ms");
                i += 2;
            }
            "--log-sink" => {
                log_sink = Some(arg_value(&args, i, "--log-sink"));
                i += 2;
            }
            "--tls-listen" => {
                tls_listen = Some(arg_value(&args, i, "--tls-listen"));
                i += 2;
            }
            "--tls-vm-port" => {
                tls_vm_port = Some(parse_arg(&args, i, "--tls-vm-port"));
                i += 2;
            }
            "--tls-cert" => {
                tls_cert = Some(arg_value(&args, i, "--tls-cert"));
                i += 2;
            }
            "--tls-key" => {
                tls_key = Some(arg_value(&args, i, "--tls-key"));
                i += 2;
            }
            "--env" => {
                let value = arg_value(&args, i, "--env");
                if let Some((k, v)) = value.split_once('=') {
                    env_pairs.push((k.to_string(), v.to_string()));
                } else {
                    eprintln!("--env: expected K=V, got {value:?}");
                    std::process::exit(2);
                }
                i += 2;
            }
            "--env-file" => {
                env_file = Some(arg_value(&args, i, "--env-file"));
                i += 2;
            }
            "--egress-policy" => {
                egress_policy = Some(arg_value(&args, i, "--egress-policy"));
                i += 2;
            }
            "--vsock-mux" => {
                vsock_mux = Some(arg_value(&args, i, "--vsock-mux"));
                i += 2;
            }
            "--vsock-mux-handoff" => {
                vsock_mux_handoff = Some(arg_value(&args, i, "--vsock-mux-handoff"));
                i += 2;
            }
            "--vsock-exec" => {
                vsock_exec = Some(arg_value(&args, i, "--vsock-exec"));
                i += 2;
            }
            "--vsock-exec-guest-port" => {
                let v = arg_value(&args, i, "--vsock-exec-guest-port");
                vsock_exec_guest_port = Some(v.parse().unwrap_or_else(|e| {
                    eprintln!("--vsock-exec-guest-port: {e}");
                    std::process::exit(2);
                }));
                i += 2;
            }
            "--http-port" => {
                http_port = Some(arg_value(&args, i, "--http-port"));
                i += 2;
            }
            "--pool-worker" => {
                pool_worker = Some(arg_value(&args, i, "--pool-worker"));
                i += 2;
            }
            "--tsi-token" => {
                let v = arg_value(&args, i, "--tsi-token");
                if v.len() != 64 || !v.bytes().all(|b| b.is_ascii_hexdigit()) {
                    eprintln!(
                        "--tsi-token: expected 64 lowercase hex chars (32 bytes), got {}",
                        v.len()
                    );
                    std::process::exit(2);
                }
                tsi_token_hex = Some(v.to_ascii_lowercase());
                i += 2;
            }
            _ => {
                eprintln!("unknown arg: {}", args[i]);
                std::process::exit(2);
            }
        }
    }

    let tls_cfg = match (tls_listen, tls_cert, tls_key) {
        (Some(l), Some(c), Some(k)) => Some(vmm::vmm::tls::TlsConfig {
            listen_addr: l,
            vm_port: tls_vm_port,
            cert_path: c,
            key_path: k,
        }),
        (None, None, None) => None,
        _ => {
            eprintln!("--tls-listen / --tls-cert / --tls-key must all be set together (--tls-vm-port optional)");
            std::process::exit(2);
        }
    };

    if let Some(p) = log_sink.as_deref() {
        vmm::devices::serial::set_log_sink(p).unwrap_or_else(|e| {
            eprintln!("--log-sink: {e}");
            std::process::exit(2);
        });
    }
    // Enable line-marker detection (heartbeat counter + "parking
    // PID 1") for any snapshot mode. Listener-only bakes use the
    // parked-marker as the early non-service fallback so they
    // don't sit through the full --snapshot-after-ms timeout.
    vmm::devices::serial::set_heartbeat_detection(
        snapshot_at.is_some()
            || snapshot_on_listener
            || snapshot_on_pre_exec
            || snapshot_after_ms.is_some(),
    );

    if let Some(json) = build_env_payload(&env_pairs, env_file.as_deref()) {
        eprintln!(
            "  env JSON: {} bytes (served on AF_VSOCK port 1026)",
            json.len()
        );
        vmm::devices::virtio::vsock::muxer::set_env_json(json);
    }

    if let Some(p) = egress_policy.as_deref() {
        eprintln!("  egress policy: {p}");
        vmm::vmm::egress_policy::set(p);
    }

    let mut pool_restore_path: Option<String> = None;
    let mut pool_sock: Option<std::os::unix::net::UnixStream> = None;
    if let Some(sock_path) = pool_worker.as_deref() {
        use std::io::{BufRead, Write};
        let mut sock = std::os::unix::net::UnixStream::connect(sock_path).unwrap_or_else(|e| {
            eprintln!("--pool-worker connect {sock_path}: {e}");
            std::process::exit(1);
        });
        sock.write_all(b"READY\n").ok();
        eprintln!("  pool-worker connected to {sock_path}");
        // Bake-then-pool: spawned without --restore-from AND without
        // --snapshot-out. The lib doesn't send an initial RESTORE in
        // this mode — it waits for BAKE_READY (emitted by the runner
        // after init reaches its readiness trigger). Skip the
        // RESTORE handshake; runner.rs detects bake-then-pool and
        // signals BAKE_READY itself.
        let bake_then_pool = restore_from.is_none() && snapshot_out.is_none();
        if bake_then_pool {
            pool_sock = Some(sock);
        } else {
            let mut reader = std::io::BufReader::new(sock.try_clone().unwrap_or_else(|e| {
                eprintln!("--pool-worker clone {sock_path}: {e}");
                std::process::exit(1);
            }));
            let mut line = String::new();
            if reader.read_line(&mut line).is_err() {
                eprintln!("  pool-worker: supervisor closed before RESTORE");
                std::process::exit(0);
            }
            let cmd = line.trim();
            let Some(rest) = cmd.strip_prefix("RESTORE ") else {
                eprintln!("  pool-worker: expected RESTORE, got {cmd:?}");
                std::process::exit(1);
            };
            let mut parts = rest.split_ascii_whitespace();
            let base = parts.next().unwrap_or("").to_string();
            for kv in parts {
                if let Some(v) = kv.strip_prefix("egress_policy=") {
                    vmm::vmm::egress_policy::set(v);
                }
            }
            pool_restore_path = Some(base);
            pool_sock = Some(sock);
            cow_restore = true;
        }
    }
    let restore_from = pool_restore_path.or(restore_from);

    // TSI auth token: decode the hex once, append the kernel
    // cmdline arg if the worker boots a fresh kernel. On restore
    // we still set `tsi_token` on VmResources (the muxer enforces
    // on every dispatch) but we don't touch the cmdline — the
    // kernel's `static u8 tsi_auth_token[32]` is already captured
    // in the snapshot's memory image. The bake-time worker
    // appended the cmdline; restoring re-uses that same token,
    // which is why the library passes the SAME hex string from
    // metadata.json on every restore.
    let tsi_token_bytes: Option<[u8; 32]> = tsi_token_hex.as_deref().map(|hex| {
        let mut out = [0u8; 32];
        for i in 0..32 {
            out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap_or_else(|e| {
                eprintln!("--tsi-token: malformed hex at byte {i}: {e}");
                std::process::exit(2);
            });
        }
        out
    });
    if let Some(hex) = tsi_token_hex.as_deref() {
        if !cmdline.is_empty() && !cmdline.ends_with(' ') {
            cmdline.push(' ');
        }
        cmdline.push_str("supermachine.tsi_token=");
        cmdline.push_str(hex);
    }

    // Guest IPv6 policy: **disabled by default**, regardless of
    // whether the host has IPv6 routing.
    //
    // Why disabled. libkrun's TSI hijack — the in-kernel hook that
    // redirects guest AF_INET binds/connects to the host's vsock
    // transport — does NOT cleanly handle AF_INET6. The most
    // visible symptom is `bind(AF_INET6, "::", port)` blocking
    // INDEFINITELY: the syscall enters TSI, never gets a host-side
    // response wired back, and Node/Python/etc see neither a
    // success callback nor an 'error' event. The workload looks
    // hung. Field-reported 2026-05-20 (essential-upsell integrator);
    // workaround was switching to `'0.0.0.0'` which we want to
    // happen automatically.
    //
    // Setting `ipv6.disable=1` on the kernel cmdline makes the
    // guest's AF_INET6 socket syscall fail with EAFNOSUPPORT.
    // Node, libuv, glibc, musl all treat that as "v6 not
    // available" and either emit an 'error' event (servers) or
    // fall back to v4 (clients). No silent hangs.
    //
    // What we lose. Outbound IPv6 from the guest. In practice this
    // matters less than it sounds:
    //   * getaddrinfo only returns A records → host TSI proxy
    //     still connects via whatever the host's stack chose
    //     (which may be v6 if the host's resolver returned AAAA).
    //   * v6-only destinations are unreachable from the guest.
    //     Rare on modern infrastructure (everything dual-stacks).
    //
    // Opt-in re-enable. Set `SUPERMACHINE_GUEST_IPV6=1` (env on the
    // host process) when you have a workload that genuinely needs
    // v6 in the guest AND you've audited that none of its code
    // calls `bind(AF_INET6, ...)`. The probe below still runs to
    // inform the host TSI proxy of v6 routing availability.
    let host_ipv6 = probe_host_ipv6_route();
    let guest_ipv6_opt_in =
        std::env::var("SUPERMACHINE_GUEST_IPV6").as_deref() == Ok("1");
    if !cmdline.is_empty() && !cmdline.ends_with(' ') {
        cmdline.push(' ');
    }
    if host_ipv6 && guest_ipv6_opt_in {
        cmdline.push_str("supermachine.host_ipv6=1");
    } else {
        // `ipv6.disable=1` makes AF_INET6 socket() return
        // EAFNOSUPPORT — the v6 leg of getaddrinfo short-circuits
        // and v6 binds fail fast. `supermachine.host_ipv6=N` is
        // kept as an informational hint for the host TSI proxy
        // (whether it should attempt v6-aware fallback on
        // outbound connects).
        cmdline.push_str(if host_ipv6 {
            "supermachine.host_ipv6=1 ipv6.disable=1"
        } else {
            "supermachine.host_ipv6=0 ipv6.disable=1"
        });
    }
    eprintln!(
        "  host IPv6 routing: {} (guest IPv6: {})",
        if host_ipv6 { "available" } else { "unavailable" },
        if host_ipv6 && guest_ipv6_opt_in {
            "enabled via SUPERMACHINE_GUEST_IPV6=1"
        } else {
            "disabled (libkrun TSI bind-hang workaround; \
             set SUPERMACHINE_GUEST_IPV6=1 to override)"
        }
    );

    if kernel_path.is_none() && restore_from.is_none() {
        runner::run_proof_of_life().unwrap_or_else(|e| {
            eprintln!("HVF proof-of-life failed: {e}");
            std::process::exit(1);
        });
        return;
    }

    let mut resources = match (kernel_path, initrd_path) {
        (Some(kernel), Some(initramfs)) => VmResources::for_kernel(kernel, initramfs),
        (Some(kernel), None) => VmResources::new().with_kernel_path(kernel),
        (None, Some(initramfs)) => VmResources::new().with_initramfs(initramfs),
        (None, None) => VmResources::new(),
    }
    .with_cmdline(cmdline)
    .with_memory_mib(memory_mib)
    .with_vcpus(n_vcpus)
    .with_cow_restore(cow_restore)
    .with_quiesce_ms(quiesce_ms)
    .with_tsi_token(tsi_token_bytes);
    if let Some(path) = restore_from {
        resources = resources.with_restore(path);
    }
    for path in blk_paths {
        resources = resources.with_block_device(path);
    }
    for volume in volumes {
        resources = resources.with_volume(volume);
    }
    for mount in mounts {
        resources = resources.with_mount(mount);
    }
    if let Some(after_ms) = snapshot_after_ms {
        if let Some(out_path) = snapshot_out.as_deref() {
            resources = resources.with_snapshot_after_ms(after_ms, out_path);
        } else {
            resources.snapshot.after_ms = Some(after_ms);
        }
    }
    if let Some(at_heartbeat) = snapshot_at {
        if let Some(out_path) = snapshot_out.as_deref() {
            resources = resources.with_snapshot_at_heartbeat(at_heartbeat, out_path);
        } else {
            resources.snapshot.at_heartbeat = Some(at_heartbeat);
        }
    }
    if snapshot_on_listener {
        if let Some(out_path) = snapshot_out.as_deref() {
            resources = resources.with_snapshot_on_listener(out_path);
        } else {
            resources.snapshot.on_listener = true;
        }
    }
    if snapshot_on_pre_exec {
        if let Some(out_path) = snapshot_out.as_deref() {
            resources = resources.with_snapshot_on_pre_exec(out_path);
        } else {
            resources.snapshot.on_pre_exec = true;
        }
    }
    if resources.snapshot.out_path.is_none() {
        resources.snapshot.out_path = snapshot_out;
    }
    resources.balloon_target_pages = balloon_target_pages;
    if let Some(path) = vsock_mux {
        resources = resources.with_vsock_mux(path);
    }
    if let Some(path) = vsock_mux_handoff {
        resources = resources.with_vsock_mux_handoff(path);
    }
    if let Some(path) = vsock_exec {
        resources = resources.with_vsock_exec(path);
    }
    if let Some(port) = vsock_exec_guest_port {
        resources = resources.with_vsock_exec_guest_port(port);
    }
    if let Some(port) = http_port {
        resources = resources.with_http_port(port);
    }
    if let Some(profile) = profile {
        if !vcpus_explicit {
            resources.apply_profile_defaults(profile);
        }
    }

    runner::run(
        &resources,
        RunOptions {
            tls: tls_cfg,
            pool_sock,
            pool_worker: None,
            experimental_skip_warm_gic_restore: std::env::var_os("SUPERMACHINE_SKIP_WARM_GIC_RESTORE")
                .is_some(),
        },
    )
    .unwrap_or_else(|e| {
        eprintln!("VM run failed: {e}");
        std::process::exit(2);
    });
}

/// Quick host-side probe: can we route IPv6 to the public internet?
///
/// We open a UDP6 socket and `connect()` it to a known-globally-
/// reachable Cloudflare v6 anycast (`2606:4700:4700::1111:53`).
/// UDP `connect()` doesn't send any packet — it just records the
/// peer and forces the kernel to pick a source address. If the host
/// has no IPv6 default route, the kernel returns ENETUNREACH (51 on
/// macOS, 101 on Linux) immediately; if it has, `connect` succeeds
/// and `getsockname` would return a global v6 source.
///
/// Why this matters: when the guest sees AAAA records and tries v6
/// first, a v6 destination unreachable from the host wedges legacy
/// clients (alpine `apk`, busybox `wget` — both lack v4-fallback
/// after a v6 connect failure). We signal the guest via cmdline
/// so init-oci can disable guest IPv6 and force getaddrinfo to
/// return v4 only.
fn probe_host_ipv6_route() -> bool {
    use std::net::{IpAddr, Ipv6Addr, SocketAddr, UdpSocket};
    // bind ephemeral v6 socket
    let Ok(sock) = UdpSocket::bind(SocketAddr::new(
        IpAddr::V6(Ipv6Addr::UNSPECIFIED),
        0,
    )) else {
        return false;
    };
    // `connect` is non-blocking on UDP — just sets up routing.
    // Cloudflare 2606:4700:4700::1111 is anycast, present on every
    // IPv6 transit.
    let target = SocketAddr::new(
        IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111)),
        53,
    );
    sock.connect(target).is_ok()
        && sock
            .local_addr()
            .map(|a| match a.ip() {
                IpAddr::V6(v6) => {
                    // Reject unspecified `::` or loopback `::1` —
                    // both mean "no real v6 source", e.g. the
                    // kernel picked no v6 interface.
                    !v6.is_unspecified() && !v6.is_loopback()
                }
                _ => false,
            })
            .unwrap_or(false)
}

fn build_env_payload(pairs: &[(String, String)], file: Option<&str>) -> Option<String> {
    if pairs.is_empty() && file.is_none() {
        return None;
    }
    if let Some(path) = file {
        return std::fs::read_to_string(path).ok();
    }
    let mut out = String::from(r#"{"env":{"#);
    for (i, (k, v)) in pairs.iter().enumerate() {
        if i > 0 {
            out.push(',');
        }
        out.push('"');
        json_escape_into(&mut out, k);
        out.push_str(r#"":"#);
        out.push('"');
        json_escape_into(&mut out, v);
        out.push('"');
    }
    out.push_str(r#"},"secrets":{}}"#);
    Some(out)
}

fn json_escape_into(out: &mut String, s: &str) {
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 0x20 => {
                use std::fmt::Write;
                let _ = write!(out, "\\u{:04x}", c as u32);
            }
            c => out.push(c),
        }
    }
}

#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
fn main() {
    eprintln!("supermachine only runs on macOS aarch64");
}
