use supermachine::bake;
use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
struct Args {
image: Option<String>,
name: Option<String>,
connect: bool,
detach: bool,
stop: bool,
http_port: u16,
guest_port: u16,
memory_mib: u32,
vcpus: u32,
runtime: String,
snapshots_dir: PathBuf,
workers_per_snapshot: u32,
pull_policy: String,
do_push: bool,
rm_on_stop: bool,
stop_grace_ms: u64,
pid_file: PathBuf,
log_file: PathBuf,
ready_timeout_ms: u64,
probe_path: Option<String>,
cmd_override: Option<String>,
push_args: Vec<String>,
}
fn home_join(path: &str) -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
.join(path)
}
fn trace_enabled() -> bool {
std::env::var_os("SUPERMACHINE_RUN_TRACE").is_some()
}
fn elapsed_ms(t0: Instant) -> u128 {
t0.elapsed().as_millis()
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn print_version() {
println!("supermachine {VERSION}");
}
fn usage() {
eprintln!(
"supermachine {VERSION} — run any OCI image as a microVM on macOS HVF.\n\
\n\
USAGE:\n \
supermachine <COMMAND> [OPTIONS]\n\
\n\
COMMANDS:\n \
run IMAGE Pull from registry, bake on first use, then run.\n \
run oci-layout:PATH Bake from a local OCI image layout dir.\n \
run oci-archive:PATH Bake from a local OCI archive tar.\n \
run --connect Probe the running daemon, print its endpoint.\n \
run --stop Stop the background daemon for --http-port.\n \
pull IMAGE Bake IMAGE to a snapshot, then exit (no router).\n \
images List baked snapshots.\n \
rmi NAME Remove a baked snapshot by name (or image ref).\n \
ps List running supermachine daemons.\n \
logs [TARGET] [-f] Tail router log; TARGET = port, snapshot name, or image.\n \
exec [TARGET] [-t] -- cmd args...\n \
Run a process inside a running guest (docker exec).\n \
setup Re-sign the supermachine-worker binary with the HVF\n \
entitlement (re-run after every `cargo build`).\n \
codesign BINARY Sign BINARY with the bundled HVF entitlement so it\n \
can call hv_vm_create. Use this on your own Rust app\n \
that embeds the supermachine library.\n \
Pass --identity \"Developer ID Application: ...\" for\n \
distribution; default is ad-hoc signing for dev.\n \
bundle --into DIR Copy kernel + init shim into DIR (typically\n \
YourApp.app/Contents/Resources/) so the embedded\n \
supermachine library finds them at runtime.\n \
entitlements-path Print the path to a temp file containing the bundled\n \
entitlements plist (for use with `codesign`).\n \
assets-path Print the directory supermachine looked up for\n \
kernel + init assets (debug helper).\n \
--version, -V Print version and exit.\n \
--help, -h Show this help.\n\
\n\
RUN OPTIONS:\n \
(no flags) Foreground, like `docker run`. Ctrl-C stops it.\n \
--detach Background. Like `docker run -d`. CLI returns\n \
once the workload answers on / (max 8s).\n \
--http-port PORT Edge HTTP port to bind (default: 8080).\n \
--name NAME Snapshot name (default: derived from image).\n \
--port PORT Guest service port to bridge to (default: 80).\n \
--memory MIB Guest RAM for the snapshot (default: 256 MiB).\n \
--vcpus N Number of vCPUs (default: 1). N=2 unlocks\n \
~1.5x sustained concurrent HTTP throughput.\n \
Higher N (up to 16) further scales CPU-bound\n \
workloads; pause-and-capture rendezvous makes\n \
snapshot/restore reliable across all counts.\n \
--workers-per-snapshot N Pool size per snapshot.\n \
--snapshots-dir DIR Where to store baked snapshots.\n \
--pull POLICY missing | always | never (default: missing).\n \
--no-push Skip bake, reuse existing snapshot.\n \
--volume HOST:GUEST Persistent volume. HOST is a name (resolves to\n \
~/.local/supermachine/volumes/NAME.img) or an\n \
absolute path. GUEST is the mount point inside\n \
the guest. ext4, 1 GiB by default. Repeatable.\n \
--entrypoint EP Replace the image's ENTRYPOINT.\n \
--workdir DIR Set the workload's working directory.\n \
--user USER Run the workload as USER (uid:gid or name).\n \
--hostname NAME Set the guest hostname.\n \
--restart POLICY no | on-failure | always | unless-stopped.\n \
Default: no. Watchdog uses this on worker death.\n \
--health-cmd CMD Probe inside the guest. Empty = no check.\n \
--health-interval N Run health-cmd every N seconds (default 30).\n \
Note: TSI doesn't loop back inside the guest,\n \
so use process checks (`pgrep ...`) or filesystem\n \
checks rather than `curl http://localhost:...`.\n \
--rm docker-style: wipe the snapshot dir on stop.\n \
--stop-grace, -t SEC docker stop -t analog: SIGTERM grace window\n \
before SIGKILL. Default 10s. Forwarded to the\n \
workload via the in-guest agent.\n \
--env KEY=VAL Pass env var to the guest workload (repeatable).\n \
--cmd CMD Override the image's CMD (JSON array string).\n \
--enable-egress | --no-egress\n \
--egress-policy POLICY deny_private | allowlist:CIDRS | denylist:...\n \
--probe-path PATH Strict-mode readiness check (require 200 on PATH).\n \
Default is any HTTP response on /; only set this\n \
if you specifically want to gate startup on 200.\n\
\n\
EXAMPLES:\n \
supermachine setup # one-time after cargo build\n \
supermachine run nginx:1.27-alpine # foreground; Ctrl-C to stop\n \
supermachine run nginx:1.27-alpine --detach # background, then:\n \
curl http://127.0.0.1:8080/\n \
supermachine run --stop # tear down --detach daemon"
);
}
fn parse_args() -> Result<Args, String> {
let mut it = std::env::args().skip(1);
let Some(cmd) = it.next() else {
usage();
std::process::exit(0);
};
if cmd == "--help" || cmd == "-h" || cmd == "help" {
usage();
std::process::exit(0);
}
if cmd == "--version" || cmd == "-V" || cmd == "version" {
print_version();
std::process::exit(0);
}
if cmd == "setup" {
match run_setup() {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine setup: {e}");
std::process::exit(1);
}
}
}
if cmd == "codesign" {
let argv: Vec<String> = it.collect();
match run_codesign(&argv) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine codesign: {e}");
std::process::exit(1);
}
}
}
if cmd == "bundle" {
let argv: Vec<String> = it.collect();
match run_bundle(&argv) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine bundle: {e}");
std::process::exit(1);
}
}
}
if cmd == "entitlements-path" {
match run_entitlements_path() {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine entitlements-path: {e}");
std::process::exit(1);
}
}
}
if cmd == "assets-path" {
run_assets_path();
std::process::exit(0);
}
if cmd == "pull" {
let argv: Vec<String> = it.collect();
match run_pull(&argv) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine pull: {e}");
std::process::exit(1);
}
}
}
if cmd == "images" {
let argv: Vec<String> = it.collect();
match run_images(&argv) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine images: {e}");
std::process::exit(1);
}
}
}
if cmd == "rmi" {
let argv: Vec<String> = it.collect();
match run_rmi(&argv) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine rmi: {e}");
std::process::exit(1);
}
}
}
if cmd == "ps" {
let argv: Vec<String> = it.collect();
match run_ps(&argv) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine ps: {e}");
std::process::exit(1);
}
}
}
if cmd == "logs" {
let argv: Vec<String> = it.collect();
match run_logs(&argv) {
Ok(()) => std::process::exit(0),
Err(e) => {
eprintln!("supermachine logs: {e}");
std::process::exit(1);
}
}
}
if cmd == "exec" {
let argv: Vec<String> = it.collect();
match run_exec(&argv) {
Ok(code) => std::process::exit(code),
Err(e) => {
eprintln!("supermachine exec: {e}");
std::process::exit(1);
}
}
}
if cmd != "run" {
return Err(format!("unknown command: {cmd}"));
}
let mut image = None;
let mut name = None;
let mut connect = false;
let mut detach = false;
let mut stop = false;
let mut http_port = std::env::var("SUPERMACHINE_HTTP_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8080);
let mut guest_port = 80;
let mut memory_mib = 256;
let mut vcpus: u32 = 1;
let mut runtime = "supermachine".to_owned();
let mut snapshots_dir = std::env::var_os("SUPERMACHINE_SNAPSHOTS")
.map(PathBuf::from)
.unwrap_or_else(|| home_join(".local/supermachine-snapshots"));
let mut workers_per_snapshot = std::env::var("SUPERMACHINE_WORKERS_PER_SNAPSHOT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1);
let mut pull_policy =
std::env::var("SUPERMACHINE_PULL_POLICY").unwrap_or_else(|_| "missing".to_owned());
let mut do_push = true;
let mut rm_on_stop = false;
let mut stop_grace_ms: u64 = std::env::var("SUPERMACHINE_STOP_GRACE_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10_000);
let state_dir = std::env::var_os("SUPERMACHINE_STATE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| home_join(".local/supermachine/run"));
let mut pid_file = None;
let mut log_file = None;
let mut ready_timeout_ms = std::env::var("SUPERMACHINE_READY_TIMEOUT_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(30000);
let mut probe_path = None;
let mut cmd_override = None;
let mut push_args = Vec::new();
while let Some(arg) = it.next() {
match arg.as_str() {
"--connect" => {
connect = true;
do_push = false;
}
"--detach" => {
detach = true;
}
"--stop" => {
stop = true;
do_push = false;
}
"--http-port" => {
let value = it
.next()
.ok_or_else(|| "missing --http-port value".to_owned())?;
http_port = value.parse().map_err(|e| format!("--http-port: {e}"))?;
}
"--probe-path" => {
let value = it
.next()
.ok_or_else(|| "missing --probe-path value".to_owned())?;
if !value.starts_with('/') || value.contains('\r') || value.contains('\n') {
return Err("--probe-path must start with '/' and not contain CR/LF".to_owned());
}
probe_path = Some(value);
}
"--name" => {
name = Some(it.next().ok_or_else(|| "missing --name value".to_owned())?);
}
"--port" => {
let value = it.next().ok_or_else(|| "missing --port value".to_owned())?;
guest_port = value.parse().map_err(|e| format!("--port: {e}"))?;
}
"--memory" => {
let value = it
.next()
.ok_or_else(|| "missing --memory value".to_owned())?;
memory_mib = value.parse().map_err(|e| format!("--memory: {e}"))?;
}
"--vcpus" | "--cpus" => {
let value = it
.next()
.ok_or_else(|| "missing --vcpus value".to_owned())?;
let n: u32 = value.parse().map_err(|e| format!("--vcpus: {e}"))?;
if n == 0 {
return Err("--vcpus must be >= 1".to_owned());
}
if n > 16 {
return Err(format!("--vcpus must be <= 16, got {n}"));
}
vcpus = n;
}
"--runtime" | "--backend" => {
let value = it.next().ok_or_else(|| format!("missing {arg} value"))?;
if value == "spike14" || value == "spike22" { return Err(
"that --runtime value is from a previous backend split that has since collapsed; supermachine is the only VMM now"
.to_owned(),
);
}
runtime = value;
}
"--workers-per-snapshot" => {
let value = it
.next()
.ok_or_else(|| "missing --workers-per-snapshot value".to_owned())?;
workers_per_snapshot = value
.parse()
.map_err(|e| format!("--workers-per-snapshot: {e}"))?;
}
"--snapshots-dir" => {
snapshots_dir = PathBuf::from(
it.next()
.ok_or_else(|| "missing --snapshots-dir value".to_owned())?,
);
}
"--pull" => {
pull_policy = it.next().ok_or_else(|| "missing --pull value".to_owned())?;
}
"--pid-file" => {
pid_file = Some(PathBuf::from(
it.next()
.ok_or_else(|| "missing --pid-file value".to_owned())?,
));
}
"--log-file" => {
log_file = Some(PathBuf::from(
it.next()
.ok_or_else(|| "missing --log-file value".to_owned())?,
));
}
"--ready-timeout-ms" => {
let value = it
.next()
.ok_or_else(|| "missing --ready-timeout-ms value".to_owned())?;
ready_timeout_ms = value
.parse()
.map_err(|e| format!("--ready-timeout-ms: {e}"))?;
}
"--cmd" => {
let value = it.next().ok_or_else(|| "missing --cmd value".to_owned())?;
cmd_override = Some(value.clone());
push_args.push(arg);
push_args.push(value);
}
"--volume" | "-v" => {
let value = it.next().ok_or_else(|| "missing --volume value".to_owned())?;
push_args.push("--volume".to_owned());
push_args.push(value);
}
"--health-cmd" => {
let value = it.next().ok_or_else(|| "missing --health-cmd value".to_owned())?;
push_args.push("--health-cmd".to_owned());
push_args.push(value);
}
"--health-interval" => {
let value = it.next().ok_or_else(|| "missing --health-interval value".to_owned())?;
let n: u64 = value
.parse()
.map_err(|e| format!("--health-interval (seconds): {e}"))?;
if n == 0 || n > 3600 {
return Err(format!("--health-interval must be 1..=3600s, got {n}"));
}
push_args.push("--health-interval".to_owned());
push_args.push(n.to_string());
}
"--restart" => {
let value = it.next().ok_or_else(|| "missing --restart value".to_owned())?;
let normalized = match value.as_str() {
"no" | "always" | "on-failure" => value.clone(),
"unless-stopped" => "always".to_owned(),
s => return Err(format!("--restart must be no|on-failure|always|unless-stopped, got {s:?}")),
};
push_args.push("--restart".to_owned());
push_args.push(normalized);
}
"--entrypoint" | "--workdir" | "--user" | "--hostname" => {
let value = it.next().ok_or_else(|| format!("missing {arg} value"))?;
push_args.push(arg);
push_args.push(value);
}
"--supermachine-snapshot-after-ms"
| "--supermachine-listener-settle-ms"
| "--extra-file"
| "--env"
| "--egress-policy"
| "--inbound-tls-cert"
| "--inbound-tls-key" => {
let value = it.next().ok_or_else(|| format!("missing {arg} value"))?;
push_args.push(arg);
push_args.push(value);
}
"--rm" => {
rm_on_stop = true;
}
"--stop-grace" | "-t" => {
let value = it
.next()
.ok_or_else(|| "missing --stop-grace value".to_owned())?;
let secs: f64 = value
.parse()
.map_err(|e| format!("--stop-grace: {e}"))?;
if secs < 0.0 || secs > 600.0 {
return Err(format!("--stop-grace must be 0..=600s, got {secs}"));
}
stop_grace_ms = (secs * 1000.0) as u64;
}
"--no-push" | "--enable-egress" | "--no-egress" | "--inbound-tls-autogen" => {
if arg == "--no-push" {
do_push = false;
} else {
push_args.push(arg);
}
}
"--help" | "-h" => {
usage();
std::process::exit(0);
}
s if s.starts_with('-') => return Err(format!("unknown flag: {s}")),
s => {
if image.is_some() {
return Err(format!("extra arg: {s}"));
}
image = Some(s.to_owned());
}
}
}
if !connect && !stop && image.is_none() {
return Err("missing image".to_owned());
}
if runtime != "supermachine" {
return Err(format!(
"unknown runtime: {runtime} (the only supported runtime is supermachine)"
));
}
if !matches!(pull_policy.as_str(), "missing" | "always" | "never") {
return Err(format!("unknown --pull policy: {pull_policy}"));
}
let pid_file = pid_file.unwrap_or_else(|| state_dir.join(format!("router-{http_port}.pid")));
let log_file = log_file.unwrap_or_else(|| state_dir.join(format!("router-{http_port}.log")));
Ok(Args {
image,
name,
connect,
detach,
stop,
http_port,
guest_port,
memory_mib,
vcpus,
runtime,
snapshots_dir,
workers_per_snapshot,
pull_policy,
do_push,
rm_on_stop,
stop_grace_ms,
pid_file,
log_file,
ready_timeout_ms,
probe_path,
cmd_override,
push_args,
})
}
struct HttpResponse {
status: u16,
bytes: Vec<u8>,
}
fn http_get(port: u16, path: &str) -> std::io::Result<HttpResponse> {
let mut stream = TcpStream::connect(("127.0.0.1", port))?;
stream.set_read_timeout(Some(Duration::from_millis(200)))?;
stream.set_write_timeout(Some(Duration::from_millis(200)))?;
write!(
stream,
"GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
)?;
let mut buf = Vec::with_capacity(1024);
let mut tmp = [0u8; 1024];
loop {
match stream.read(&mut tmp) {
Ok(0) => break,
Ok(n) => {
buf.extend_from_slice(&tmp[..n]);
}
Err(e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut =>
{
break
}
Err(e) => return Err(e),
}
}
let status = parse_status(&buf).unwrap_or(0);
Ok(HttpResponse { status, bytes: buf })
}
fn parse_status(buf: &[u8]) -> Option<u16> {
let first_line_end = buf.windows(2).position(|w| w == b"\r\n")?;
let first_line = std::str::from_utf8(&buf[..first_line_end]).ok()?;
let mut parts = first_line.split_ascii_whitespace();
let version = parts.next()?;
if !version.starts_with("HTTP/") {
return None;
}
parts.next()?.parse().ok()
}
fn health_ok(port: u16) -> std::io::Result<bool> {
let response = http_get(port, "/_health")?;
Ok(response.status == 200
&& response
.bytes
.windows(br#""status":"ok""#.len())
.any(|w| w == br#""status":"ok""#))
}
fn probe_200(port: u16, path: &str) -> std::io::Result<bool> {
Ok(http_get(port, path)?.status == 200)
}
fn workload_responding(port: u16, total_timeout_ms: u64) -> std::io::Result<bool> {
let deadline =
std::time::Instant::now() + std::time::Duration::from_millis(total_timeout_ms);
let mut last_err: Option<std::io::Error> = None;
while std::time::Instant::now() < deadline {
match http_get(port, "/") {
Ok(resp) => {
if matches!(resp.status, 502 | 503 | 504) {
last_err = Some(std::io::Error::new(
std::io::ErrorKind::Other,
format!("upstream {}: {}", resp.status, String::from_utf8_lossy(&resp.bytes).chars().take(120).collect::<String>()),
));
} else {
return Ok(true);
}
}
Err(e) => last_err = Some(e),
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
if let Some(e) = last_err {
Err(e)
} else {
Ok(false)
}
}
fn repo_root() -> Result<PathBuf, String> {
if let Some(root) = std::env::var_os("SUPERMACHINE_ROOT") {
return Ok(PathBuf::from(root));
}
let exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?;
for ancestor in exe.ancestors() {
if ancestor.join("tools/supermachine-push").is_file() {
return Ok(ancestor.to_path_buf());
}
}
for ancestor in exe.ancestors() {
if ancestor.join("share/supermachine/kernel").is_file() {
return Ok(ancestor.to_path_buf());
}
}
std::env::current_dir().map_err(|e| format!("current_dir: {e}"))
}
fn run_setup() -> Result<(), String> {
let mut errors = Vec::new();
let mut ok_steps = Vec::new();
#[cfg(target_os = "macos")]
{
match supermachine::codesign::locate_worker_bin() {
Some(bin) => match supermachine::codesign::ensure_worker_signed(&bin) {
Ok(()) => ok_steps.push(format!("signed {}", bin.display())),
Err(e) => errors.push(format!("codesign: {e}")),
},
None => errors.push(
"supermachine-worker not found. \
If you `cargo install`d, the worker should be a sibling \
of supermachine in ~/.cargo/bin/. If you `cargo build`d, \
it lives at target/release/supermachine-worker."
.to_owned(),
),
}
}
let assets = supermachine::AssetPaths::discover();
match assets.kernel.as_deref() {
Some(k) => ok_steps.push(format!("kernel ready at {}", k.display())),
None => errors.push(
"kernel asset not found. The supermachine-kernel crate ships \
the kernel bytes inside this binary; first AssetPaths::discover() \
call extracts them to $XDG_DATA_HOME/supermachine/v{VERSION}/. \
If extraction failed, check that ~/.local/share/supermachine/ \
is writable."
.to_owned(),
),
}
match assets.init_oci_bin.as_deref() {
Some(p) => ok_steps.push(format!("init-oci ready at {}", p.display())),
None => errors.push(
"init-oci asset not found (same root cause as kernel; see above).".to_owned(),
),
}
println!("supermachine setup:");
for s in &ok_steps {
println!(" ok: {s}");
}
for e in &errors {
println!(" err: {e}");
}
if errors.is_empty() {
println!(" (ready — try `supermachine run python:3.12-alpine ...`)");
Ok(())
} else {
Err(format!("{} step(s) failed", errors.len()))
}
}
fn run_codesign(argv: &[String]) -> Result<(), String> {
let mut binary: Option<&str> = None;
let mut identity: String = "-".to_owned(); let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"--identity" | "-s" => {
i += 1;
identity = argv
.get(i)
.ok_or_else(|| "missing --identity value".to_owned())?
.clone();
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine codesign BINARY [--identity IDENTITY]\n\
\n\
Default IDENTITY is `-` (ad-hoc). For a distributable .app,\n\
pass `--identity \"Developer ID Application: Name (TEAMID)\"`."
);
std::process::exit(0);
}
s if s.starts_with('-') => {
return Err(format!("unknown flag: {s}"));
}
s => {
if binary.is_some() {
return Err(format!("extra arg: {s}"));
}
binary = Some(s);
}
}
i += 1;
}
let binary = binary.ok_or_else(|| "missing BINARY argument".to_owned())?;
let plist_dir = std::env::temp_dir();
let plist_path = plist_dir.join(format!("supermachine-entitlements-{}.plist", std::process::id()));
std::fs::write(&plist_path, supermachine::assets::ENTITLEMENTS_PLIST)
.map_err(|e| format!("write temp plist {}: {e}", plist_path.display()))?;
let result = (|| {
let mut cmd = Command::new("codesign");
cmd.args(["-s", &identity, "--entitlements"])
.arg(&plist_path)
.arg("--force");
if identity != "-" {
cmd.args(["--options", "runtime"]);
}
cmd.arg(binary);
let status = cmd.status().map_err(|e| format!("spawn codesign: {e}"))?;
if status.success() {
Ok(())
} else {
Err(format!("codesign exit {:?}", status.code()))
}
})();
let _ = std::fs::remove_file(&plist_path);
result?;
println!("signed {binary} (identity: {identity})");
Ok(())
}
fn run_bundle(argv: &[String]) -> Result<(), String> {
let mut into: Option<PathBuf> = None;
let mut image: Option<String> = None;
let mut snapshots_dir = default_snapshots_dir();
let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"--into" => {
i += 1;
into = Some(PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --into value".to_owned())?,
));
}
"--image" => {
i += 1;
image = Some(
argv.get(i)
.ok_or_else(|| "missing --image value".to_owned())?
.clone(),
);
}
"--snapshots-dir" => {
i += 1;
snapshots_dir = PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --snapshots-dir value".to_owned())?,
);
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine bundle --into DIR [--image NAME]\n\
\x20 [--snapshots-dir DIR]\n\
\n\
Without --image: copies the kernel image and init shim into\n\
DIR (typically `YourApp.app/Contents/Resources/`).\n\
\n\
With --image NAME: produces a self-contained snapshot bundle\n\
at DIR/NAME/ containing the kernel, restore.snap, metadata.json\n\
(with paths relativized), delta.squashfs, and every layer\n\
squashfs referenced by the snapshot. The embedder loads it via\n\
`Image::from_snapshot(\"...Resources/NAME/\")` — no supermachine\n\
install needed on the end user's machine.\n\
\n\
NAME accepts either a snapshot dir name (as shown by\n\
`supermachine images`) or an image ref (`nginx:1.27-alpine`,\n\
auto-sanitized)."
);
std::process::exit(0);
}
s => return Err(format!("unknown arg: {s}")),
}
i += 1;
}
let into = into.ok_or_else(|| "missing --into DIR".to_owned())?;
std::fs::create_dir_all(&into)
.map_err(|e| format!("mkdir {}: {e}", into.display()))?;
if let Some(image) = image {
return bundle_snapshot(&into, &image, &snapshots_dir);
}
let assets = supermachine::assets::AssetPaths::discover();
let mut copied = 0;
if let Some(kernel) = &assets.kernel {
let dst = into.join("kernel");
std::fs::copy(kernel, &dst)
.map_err(|e| format!("copy kernel {} → {}: {e}", kernel.display(), dst.display()))?;
println!(" kernel: {} → {}", kernel.display(), dst.display());
copied += 1;
} else {
return Err(
"no kernel found in any standard location — set $SUPERMACHINE_ASSETS_DIR or rebuild from source".to_owned(),
);
}
if let Some(init_bin) = &assets.init_oci_bin {
let dst = into.join("init-oci");
std::fs::copy(init_bin, &dst)
.map_err(|e| format!("copy init-oci {} → {}: {e}", init_bin.display(), dst.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&dst)
.map_err(|e| format!("stat {}: {e}", dst.display()))?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&dst, perms)
.map_err(|e| format!("chmod {}: {e}", dst.display()))?;
}
println!(" init-oci: {} → {}", init_bin.display(), dst.display());
copied += 1;
}
if let Some(init_src) = &assets.init_oci_src {
let dst = into.join("init-oci.c");
std::fs::copy(init_src, &dst)
.map_err(|e| format!("copy init-oci.c {} → {}: {e}", init_src.display(), dst.display()))?;
println!(" init-oci.c:{} → {}", init_src.display(), dst.display());
copied += 1;
}
println!("bundled {copied} asset(s) into {}", into.display());
Ok(())
}
fn bundle_snapshot(into: &Path, image: &str, snapshots_dir: &Path) -> Result<(), String> {
let candidate = snapshots_dir.join(image);
let snap_dir = if candidate.is_dir() {
candidate
} else {
let derived = snapshots_dir.join(supermachine::bake::snapshot_name_for_image(image));
if !derived.is_dir() {
return Err(format!(
"no snapshot for {image} (looked in {} and {})",
candidate.display(),
derived.display(),
));
}
derived
};
let name = snap_dir
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("snapshot dir has no name: {}", snap_dir.display()))?
.to_owned();
let metadata_src = snap_dir.join("metadata.json");
if !metadata_src.is_file() {
return Err(format!("missing {}", metadata_src.display()));
}
let restore_src = snap_dir.join("restore.snap");
if !restore_src.is_file() {
return Err(format!("missing {}", restore_src.display()));
}
let bundle_dir = into.join(&name);
std::fs::create_dir_all(&bundle_dir)
.map_err(|e| format!("mkdir {}: {e}", bundle_dir.display()))?;
let layers_dir = bundle_dir.join("layers");
std::fs::create_dir_all(&layers_dir)
.map_err(|e| format!("mkdir {}: {e}", layers_dir.display()))?;
let mut total_bytes: u64 = 0;
let assets = supermachine::assets::AssetPaths::discover();
let kernel = assets.kernel.as_ref().ok_or_else(|| {
"no kernel found in any standard location — set $SUPERMACHINE_ASSETS_DIR or rebuild from source"
.to_owned()
})?;
let kernel_dst = bundle_dir.join("kernel");
let kernel_bytes = std::fs::copy(kernel, &kernel_dst)
.map_err(|e| format!("copy kernel {} → {}: {e}", kernel.display(), kernel_dst.display()))?;
total_bytes += kernel_bytes;
println!(" kernel: {} ({})", kernel_dst.display(), human_bytes(kernel_bytes));
let restore_dst = bundle_dir.join("restore.snap");
let restore_bytes = std::fs::copy(&restore_src, &restore_dst)
.map_err(|e| format!("copy restore.snap: {e}"))?;
total_bytes += restore_bytes;
println!(" restore.snap: {} ({})", restore_dst.display(), human_bytes(restore_bytes));
let meta_text = std::fs::read_to_string(&metadata_src)
.map_err(|e| format!("read {}: {e}", metadata_src.display()))?;
let mut meta: serde_json::Value = serde_json::from_str(&meta_text)
.map_err(|e| format!("parse {}: {e}", metadata_src.display()))?;
if let Some(obj) = meta.as_object_mut() {
obj.insert(
"kernel".to_owned(),
serde_json::Value::String("kernel".to_owned()),
);
}
if let Some(layers) = meta.get("layers").cloned().and_then(|v| match v {
serde_json::Value::Array(a) => Some(a),
_ => None,
}) {
let mut new_layers: Vec<serde_json::Value> = Vec::with_capacity(layers.len());
for layer in &layers {
let src = layer
.as_str()
.ok_or_else(|| "layer path is not a string".to_owned())?;
let src_path = PathBuf::from(src);
if !src_path.is_file() {
return Err(format!(
"layer file missing: {} (referenced by {})",
src_path.display(),
metadata_src.display()
));
}
let basename = src_path
.file_name()
.ok_or_else(|| format!("layer path has no filename: {}", src_path.display()))?;
let dst = layers_dir.join(basename);
let bytes = std::fs::copy(&src_path, &dst).map_err(|e| {
format!("copy layer {} → {}: {e}", src_path.display(), dst.display())
})?;
total_bytes += bytes;
new_layers.push(serde_json::Value::String(format!(
"layers/{}",
basename.to_string_lossy()
)));
}
if let Some(obj) = meta.as_object_mut() {
obj.insert("layers".to_owned(), serde_json::Value::Array(new_layers));
}
println!(" layers: {} files ({})", layers.len(), human_bytes(total_bytes - kernel_bytes - restore_bytes));
}
if let Some(delta) = meta.get("delta_squashfs").and_then(|v| v.as_str()).map(PathBuf::from) {
if delta.is_file() {
let dst = bundle_dir.join("delta.squashfs");
let bytes = std::fs::copy(&delta, &dst)
.map_err(|e| format!("copy delta.squashfs: {e}"))?;
total_bytes += bytes;
if let Some(obj) = meta.as_object_mut() {
obj.insert(
"delta_squashfs".to_owned(),
serde_json::Value::String("delta.squashfs".to_owned()),
);
}
println!(" delta.squashfs:{} ({})", dst.display(), human_bytes(bytes));
}
}
let init_cpio = snap_dir.join("init.cpio.gz");
if init_cpio.is_file() {
let dst = bundle_dir.join("init.cpio.gz");
let bytes = std::fs::copy(&init_cpio, &dst)
.map_err(|e| format!("copy init.cpio.gz: {e}"))?;
total_bytes += bytes;
if let Some(obj) = meta.as_object_mut() {
obj.insert(
"init_cpio".to_owned(),
serde_json::Value::String("init.cpio.gz".to_owned()),
);
}
}
let metadata_dst = bundle_dir.join("metadata.json");
let pretty = serde_json::to_string_pretty(&meta)
.map_err(|e| format!("serialize metadata: {e}"))?;
std::fs::write(&metadata_dst, pretty)
.map_err(|e| format!("write {}: {e}", metadata_dst.display()))?;
println!(" metadata.json: {}", metadata_dst.display());
println!(
"bundled {} ({}) into {}",
name,
human_bytes(total_bytes),
bundle_dir.display()
);
println!(
" load it from Rust: Image::from_snapshot(\"{}\")",
bundle_dir.display()
);
Ok(())
}
fn run_entitlements_path() -> Result<(), String> {
let path = std::env::temp_dir()
.join(format!("supermachine-entitlements-{}.plist", std::process::id()));
std::fs::write(&path, supermachine::assets::ENTITLEMENTS_PLIST)
.map_err(|e| format!("write temp plist {}: {e}", path.display()))?;
println!("{}", path.display());
Ok(())
}
fn run_assets_path() {
let assets = supermachine::assets::AssetPaths::discover();
println!("kernel: {}",
assets.kernel.as_ref().map(|p| p.display().to_string()).unwrap_or_else(|| "(not found)".to_owned())
);
println!("init-oci bin: {}",
assets.init_oci_bin.as_ref().map(|p| p.display().to_string()).unwrap_or_else(|| "(not found)".to_owned())
);
println!("init-oci src: {}",
assets.init_oci_src.as_ref().map(|p| p.display().to_string()).unwrap_or_else(|| "(not found)".to_owned())
);
if let Ok(d) = std::env::var("SUPERMACHINE_ASSETS_DIR") {
println!("(SUPERMACHINE_ASSETS_DIR override: {d})");
}
}
fn router_bin(root: &Path) -> Result<PathBuf, String> {
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
let sibling = dir.join("supermachine-router");
if sibling.is_file() {
return Ok(sibling);
}
}
}
for candidate in [
"target/release/supermachine-router",
"bin/supermachine-router",
] {
let p = root.join(candidate);
if p.is_file() {
return Ok(p);
}
}
Err(format!(
"missing router binary at {} (run `cargo build --release`)",
root.join("target/release/supermachine-router").display()
))
}
fn run_push(args: &Args, run_t0: Instant) -> Result<(), String> {
if !args.do_push {
if trace_enabled() {
eprintln!(
"supermachine: push skipped after {}ms",
elapsed_ms(run_t0)
);
}
return Ok(());
}
let root = repo_root()?;
let image = args
.image
.clone()
.ok_or_else(|| "missing image".to_owned())?;
let request = bake::BakeRequest {
image,
name: args.name.clone(),
runtime: args.runtime.clone(),
guest_port: args.guest_port,
memory_mib: args.memory_mib,
vcpus: args.vcpus,
pull_policy: args.pull_policy.clone(),
snapshots_dir: args.snapshots_dir.clone(),
cmd_override: args.cmd_override.clone(),
extra_args: args.push_args.clone(),
};
bake::run_push(&request, run_t0, &root)
}
fn process_alive(pid: u32) -> bool {
Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
fn signal_process(pid: u32, signal: &str) {
let _ = Command::new("kill")
.arg(format!("-{signal}"))
.arg(pid.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
fn router_sidecar_path(pid_file: &Path, suffix: &str) -> PathBuf {
let mut p = pid_file.to_path_buf();
p.set_extension(suffix);
p
}
fn stop_router(args: &Args) -> Result<(), String> {
let pid_text = match std::fs::read_to_string(&args.pid_file) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if args.rm_on_stop {
rm_snapshot_for_args(args)?;
}
return Ok(());
}
Err(e) => return Err(format!("read pid file {}: {e}", args.pid_file.display())),
};
let pid: u32 = match pid_text.trim().parse() {
Ok(pid) => pid,
Err(e) => {
let _ = std::fs::remove_file(&args.pid_file);
return Err(format!("invalid pid file {}: {e}", args.pid_file.display()));
}
};
if process_alive(pid) {
let snapshot_sidecar_early = router_sidecar_path(&args.pid_file, "snapshot");
if let Ok(snap_name) = std::fs::read_to_string(&snapshot_sidecar_early) {
let snap_name = snap_name.trim();
if !snap_name.is_empty() {
let exec_sock = PathBuf::from(format!(
"/tmp/supermachine-router-socks/{snap_name}-w0.sock-exec"
));
if exec_sock.exists() {
let body = serde_json::json!({
"action": "signal",
"signum": 15, });
if let Err(e) = supermachine::exec::send_control(&exec_sock, &body) {
if std::env::var_os("SUPERMACHINE_RUN_TRACE").is_some() {
eprintln!("supermachine: workload SIGTERM via agent: {e}");
}
}
}
}
}
signal_process(pid, "TERM");
let grace_iters = (args.stop_grace_ms / 50).max(1);
for _ in 0..grace_iters {
if !process_alive(pid) {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
if process_alive(pid) {
signal_process(pid, "KILL");
}
}
let snapshot_sidecar = router_sidecar_path(&args.pid_file, "snapshot");
let rm_sidecar = router_sidecar_path(&args.pid_file, "rm");
let sticky_rm = rm_sidecar.exists();
let snapshot_name = std::fs::read_to_string(&snapshot_sidecar)
.ok()
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty());
let _ = std::fs::remove_file(&args.pid_file);
let _ = std::fs::remove_file(&snapshot_sidecar);
let _ = std::fs::remove_file(&rm_sidecar);
if args.rm_on_stop || sticky_rm {
let name = snapshot_name
.or_else(|| derive_default_snapshot_name(args))
.ok_or_else(|| {
"--rm on stop: cannot determine which snapshot to remove (no sidecar, no image, no --name)"
.to_owned()
})?;
rm_snapshot(&args.snapshots_dir, &name)?;
}
Ok(())
}
fn rm_snapshot(snapshots_dir: &Path, name: &str) -> Result<(), String> {
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(format!("invalid snapshot name: {name:?}"));
}
let dir = snapshots_dir.join(name);
match std::fs::remove_dir_all(&dir) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Err(format!("no such snapshot: {name}"))
}
Err(e) => Err(format!("remove {}: {e}", dir.display())),
}
}
fn rm_snapshot_for_args(args: &Args) -> Result<(), String> {
let name = derive_default_snapshot_name(args).ok_or_else(|| {
"--rm: cannot determine which snapshot to remove (need image or --name)".to_owned()
})?;
match rm_snapshot(&args.snapshots_dir, &name) {
Ok(()) | Err(_) => Ok(()),
}
}
fn wait_ready(port: u16, timeout_ms: u64) -> bool {
let deadline = Instant::now() + Duration::from_millis(timeout_ms);
loop {
if health_ok(port).unwrap_or(false) {
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(5));
}
}
fn router_command(args: &Args) -> Result<Command, String> {
let root = repo_root()?;
let router = router_bin(&root)?;
let mut cmd = Command::new(router);
cmd.arg("--snapshots-dir")
.arg(&args.snapshots_dir)
.arg("--http-port")
.arg(args.http_port.to_string())
.arg("--workers-per-snapshot")
.arg(args.workers_per_snapshot.to_string())
.env("SUPERMACHINE_SNAPSHOTS", &args.snapshots_dir)
.env(
"SUPERMACHINE_WORKERS_PER_SNAPSHOT",
args.workers_per_snapshot.to_string(),
);
if let Some(name) = derive_default_snapshot_name(args) {
cmd.arg("--default-snapshot").arg(&name);
cmd.env("SUPERMACHINE_DEFAULT_SNAPSHOT", &name);
}
Ok(cmd)
}
fn derive_default_snapshot_name(args: &Args) -> Option<String> {
if let Some(n) = args.name.as_ref() {
return Some(n.clone());
}
args.image
.as_ref()
.map(|img| bake::snapshot_name_for_image(img))
}
fn start_router_detached(args: &Args) -> Result<(), String> {
if health_ok(args.http_port).unwrap_or(false) {
return Ok(());
}
if let Some(parent) = args.pid_file.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create pid dir {}: {e}", parent.display()))?;
}
if let Some(parent) = args.log_file.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create log dir {}: {e}", parent.display()))?;
}
std::fs::create_dir_all(&args.snapshots_dir)
.map_err(|e| format!("create snapshots dir {}: {e}", args.snapshots_dir.display()))?;
let _ = std::fs::remove_file(&args.pid_file);
let log = OpenOptions::new()
.create(true)
.append(true)
.open(&args.log_file)
.map_err(|e| format!("open log file {}: {e}", args.log_file.display()))?;
let log_err = log
.try_clone()
.map_err(|e| format!("clone log file {}: {e}", args.log_file.display()))?;
let mut child = router_command(args)?
.stdin(Stdio::null())
.stdout(Stdio::from(log))
.stderr(Stdio::from(log_err))
.spawn()
.map_err(|e| format!("spawn router: {e}"))?;
let pid = child.id();
std::fs::write(&args.pid_file, format!("{pid}\n"))
.map_err(|e| format!("write pid file {}: {e}", args.pid_file.display()))?;
if let Some(name) = derive_default_snapshot_name(args) {
let snapshot_sidecar = router_sidecar_path(&args.pid_file, "snapshot");
let _ = std::fs::write(&snapshot_sidecar, format!("{name}\n"));
}
if args.rm_on_stop {
let rm_sidecar = router_sidecar_path(&args.pid_file, "rm");
let _ = std::fs::write(&rm_sidecar, "");
}
if wait_ready(args.http_port, args.ready_timeout_ms) {
return Ok(());
}
let _ = child.kill();
let _ = child.wait();
let _ = std::fs::remove_file(&args.pid_file);
let _ = std::fs::remove_file(&router_sidecar_path(&args.pid_file, "snapshot"));
let _ = std::fs::remove_file(&router_sidecar_path(&args.pid_file, "rm"));
Err(format!(
"router did not become ready; see {}",
args.log_file.display()
))
}
fn run_router_foreground(args: &Args) -> Result<i32, String> {
std::fs::create_dir_all(&args.snapshots_dir)
.map_err(|e| format!("create snapshots dir {}: {e}", args.snapshots_dir.display()))?;
let status = router_command(args)?
.status()
.map_err(|e| format!("run router: {e}"))?;
Ok(status.code().unwrap_or(1))
}
fn report_probe_result(
image: Option<&str>,
http_port: u16,
probe_path: Option<&str>,
result: std::io::Result<bool>,
) -> std::process::ExitCode {
match result {
Ok(true) => {
println!("endpoint: http://127.0.0.1:{http_port}/");
std::process::ExitCode::SUCCESS
}
Ok(false) => {
let image_hint = image.map(|s| format!(" for {s}")).unwrap_or_default();
if let Some(path) = probe_path {
eprintln!(
"probe {path}{image_hint} did not return HTTP 200 at http://127.0.0.1:{http_port}"
);
} else {
eprintln!(
"no healthy supermachine daemon{image_hint} at http://127.0.0.1:{http_port}/"
);
}
std::process::ExitCode::from(1)
}
Err(e) => {
if let Some(path) = probe_path {
eprintln!("probe {path} failed at http://127.0.0.1:{http_port}: {e}");
} else {
eprintln!("no healthy supermachine daemon at http://127.0.0.1:{http_port}/: {e}");
}
std::process::ExitCode::from(1)
}
}
}
fn default_snapshots_dir() -> PathBuf {
std::env::var_os("SUPERMACHINE_SNAPSHOTS")
.map(PathBuf::from)
.unwrap_or_else(|| home_join(".local/supermachine-snapshots"))
}
fn default_state_dir() -> PathBuf {
std::env::var_os("SUPERMACHINE_STATE_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| home_join(".local/supermachine/run"))
}
fn run_pull(argv: &[String]) -> Result<(), String> {
let mut image: Option<String> = None;
let mut name: Option<String> = None;
let mut memory_mib: u32 = 256;
let mut vcpus: u32 = 1;
let mut guest_port: u16 = 80;
let mut pull_policy = std::env::var("SUPERMACHINE_PULL_POLICY")
.unwrap_or_else(|_| "missing".to_owned());
let mut snapshots_dir = default_snapshots_dir();
let mut cmd_override: Option<String> = None;
let mut extra_args: Vec<String> = Vec::new();
let mut i = 0;
while i < argv.len() {
let arg = argv[i].as_str();
match arg {
"--name" => {
i += 1;
name = Some(
argv.get(i)
.ok_or_else(|| "missing --name value".to_owned())?
.clone(),
);
}
"--memory" => {
i += 1;
memory_mib = argv
.get(i)
.ok_or_else(|| "missing --memory value".to_owned())?
.parse()
.map_err(|e| format!("--memory: {e}"))?;
}
"--vcpus" | "--cpus" => {
i += 1;
let n: u32 = argv
.get(i)
.ok_or_else(|| "missing --vcpus value".to_owned())?
.parse()
.map_err(|e| format!("--vcpus: {e}"))?;
if n == 0 {
return Err("--vcpus must be >= 1".to_owned());
}
if n > 16 {
return Err(format!("--vcpus must be <= 16, got {n}"));
}
vcpus = n;
}
"--port" => {
i += 1;
guest_port = argv
.get(i)
.ok_or_else(|| "missing --port value".to_owned())?
.parse()
.map_err(|e| format!("--port: {e}"))?;
}
"--pull" => {
i += 1;
pull_policy = argv
.get(i)
.ok_or_else(|| "missing --pull value".to_owned())?
.clone();
}
"--snapshots-dir" => {
i += 1;
snapshots_dir = PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --snapshots-dir value".to_owned())?,
);
}
"--cmd" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing --cmd value".to_owned())?
.clone();
cmd_override = Some(v.clone());
extra_args.push("--cmd".to_owned());
extra_args.push(v);
}
"--env"
| "--egress-policy"
| "--volume"
| "-v"
| "--entrypoint"
| "--workdir"
| "--user"
| "--hostname"
| "--restart"
| "--health-cmd"
| "--health-interval"
| "--supermachine-snapshot-after-ms"
| "--supermachine-listener-settle-ms" => {
let flag = if arg == "-v" { "--volume".to_owned() } else { arg.to_owned() };
i += 1;
let v = argv
.get(i)
.ok_or_else(|| format!("missing {flag} value"))?
.clone();
extra_args.push(flag);
extra_args.push(v);
}
"--enable-egress" | "--no-egress" => {
extra_args.push(arg.to_owned());
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine pull IMAGE [--name NAME] [--memory MIB]\n\
\x20 [--port PORT] [--pull POLICY]\n\
\x20 [--snapshots-dir DIR]\n\
\n\
Bake IMAGE to a snapshot under --snapshots-dir, then exit.\n\
No router is started. Equivalent to `docker pull` for the\n\
supermachine cache.\n\
\n\
Non-service images (rust:1-slim, python:slim, node:slim,\n\
anything that doesn't bind a port) just work — the snapshot\n\
auto-falls-back to init-state when no listener appears."
);
std::process::exit(0);
}
s if s.starts_with('-') => return Err(format!("unknown flag: {s}")),
s => {
if image.is_some() {
return Err(format!("extra arg: {s}"));
}
image = Some(s.to_owned());
}
}
i += 1;
}
let image = image.ok_or_else(|| "missing IMAGE".to_owned())?;
if !matches!(pull_policy.as_str(), "missing" | "always" | "never") {
return Err(format!("unknown --pull policy: {pull_policy}"));
}
let root = repo_root()?;
let request = bake::BakeRequest {
image: image.clone(),
name,
runtime: "supermachine".to_owned(),
guest_port,
memory_mib,
vcpus,
pull_policy,
snapshots_dir,
cmd_override,
extra_args,
};
let t0 = Instant::now();
bake::run_push(&request, t0, &root)?;
println!("pulled {image} in {}ms", elapsed_ms(t0));
Ok(())
}
#[derive(Debug)]
struct SnapshotEntry {
name: String,
image: String,
memory_mib: u64,
physical_bytes: u64,
baked_at: String,
runtime_sha16: String,
}
fn read_snapshot_entry(dir: &Path) -> Option<SnapshotEntry> {
let metadata_path = dir.join("metadata.json");
let text = std::fs::read_to_string(&metadata_path).ok()?;
let json: serde_json::Value = serde_json::from_str(&text).ok()?;
let name = json
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_owned())
.unwrap_or_else(|| {
dir.file_name()
.and_then(|s| s.to_str())
.unwrap_or("?")
.to_owned()
});
Some(SnapshotEntry {
name,
image: json
.get("image")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_owned(),
memory_mib: json.get("memory_mib").and_then(|v| v.as_u64()).unwrap_or(0),
physical_bytes: json
.get("snapshot_physical_bytes")
.and_then(|v| v.as_u64())
.unwrap_or(0),
baked_at: json
.get("baked_at")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_owned(),
runtime_sha16: json
.get("runtime_sha16")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_owned(),
})
}
fn human_bytes(n: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
if n >= GB {
format!("{:.1} GB", n as f64 / GB as f64)
} else if n >= MB {
format!("{:.1} MB", n as f64 / MB as f64)
} else if n >= KB {
format!("{:.1} KB", n as f64 / KB as f64)
} else {
format!("{n} B")
}
}
fn run_images(argv: &[String]) -> Result<(), String> {
let mut snapshots_dir = default_snapshots_dir();
let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"--snapshots-dir" => {
i += 1;
snapshots_dir = PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --snapshots-dir value".to_owned())?,
);
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine images [--snapshots-dir DIR]\n\
\n\
List baked snapshots in --snapshots-dir (default:\n\
~/.local/supermachine-snapshots/). Each row is one\n\
snapshot ready for `supermachine run --no-push --name NAME`."
);
std::process::exit(0);
}
s => return Err(format!("unknown arg: {s}")),
}
i += 1;
}
let entries = match std::fs::read_dir(&snapshots_dir) {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
println!("no snapshots in {} (yet)", snapshots_dir.display());
return Ok(());
}
Err(e) => return Err(format!("read {}: {e}", snapshots_dir.display())),
};
let mut rows: Vec<SnapshotEntry> = entries
.filter_map(|r| r.ok())
.filter(|e| e.path().is_dir())
.filter_map(|e| read_snapshot_entry(&e.path()))
.collect();
if rows.is_empty() {
println!("no snapshots in {} (yet)", snapshots_dir.display());
return Ok(());
}
rows.sort_by(|a, b| b.baked_at.cmp(&a.baked_at));
let name_w = rows.iter().map(|r| r.name.len()).max().unwrap_or(4).max(4);
let image_w = rows
.iter()
.map(|r| r.image.len())
.max()
.unwrap_or(5)
.max(5);
println!(
"{:<name_w$} {:<image_w$} {:>8} {:>9} {:<20} {}",
"NAME",
"IMAGE",
"MEM",
"SIZE",
"BAKED",
"RUNTIME",
name_w = name_w,
image_w = image_w,
);
for r in &rows {
println!(
"{:<name_w$} {:<image_w$} {:>6} MB {:>9} {:<20} {}",
r.name,
r.image,
r.memory_mib,
human_bytes(r.physical_bytes),
r.baked_at,
r.runtime_sha16,
name_w = name_w,
image_w = image_w,
);
}
Ok(())
}
fn run_rmi(argv: &[String]) -> Result<(), String> {
let mut snapshots_dir = default_snapshots_dir();
let mut names: Vec<String> = Vec::new();
let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"--snapshots-dir" => {
i += 1;
snapshots_dir = PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --snapshots-dir value".to_owned())?,
);
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine rmi NAME [NAME ...] [--snapshots-dir DIR]\n\
\n\
Remove baked snapshots. NAME is the snapshot name as shown\n\
by `supermachine images` (typically the sanitized image ref,\n\
e.g. nginx_1_27-alpine).\n\
\n\
Cached layer blobs in ~/.local/supermachine-layer-cache/ are\n\
intentionally left in place — they're shared across snapshots\n\
and re-downloading is the slow part."
);
std::process::exit(0);
}
s if s.starts_with('-') => return Err(format!("unknown flag: {s}")),
s => names.push(s.to_owned()),
}
i += 1;
}
if names.is_empty() {
return Err("missing NAME (try `supermachine images` to list)".to_owned());
}
let mut errors = 0;
for name in &names {
let resolved = if snapshots_dir.join(name).is_dir() {
name.clone()
} else {
bake::snapshot_name_for_image(name)
};
match rm_snapshot(&snapshots_dir, &resolved) {
Ok(()) => println!("removed {resolved}"),
Err(e) => {
eprintln!("rmi {name}: {e}");
errors += 1;
}
}
}
if errors > 0 {
return Err(format!("{errors} snapshot(s) failed to remove"));
}
Ok(())
}
#[derive(Debug)]
struct RouterRow {
port: u16,
pid: u32,
snapshot: String,
rm: bool,
log_path: PathBuf,
}
fn list_routers(state_dir: &Path) -> Result<Vec<RouterRow>, String> {
let entries = match std::fs::read_dir(state_dir) {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(format!("read {}: {e}", state_dir.display())),
};
let mut rows = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let stem = match path.file_stem().and_then(|s| s.to_str()) {
Some(s) => s.to_owned(),
None => continue,
};
if path.extension().and_then(|s| s.to_str()) != Some("pid") {
continue;
}
let port: u16 = match stem.strip_prefix("router-").and_then(|s| s.parse().ok()) {
Some(p) => p,
None => continue,
};
let pid: u32 = match std::fs::read_to_string(&path)
.ok()
.and_then(|s| s.trim().parse().ok())
{
Some(p) => p,
None => continue,
};
if !process_alive(pid) {
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(router_sidecar_path(&path, "snapshot"));
let _ = std::fs::remove_file(router_sidecar_path(&path, "rm"));
continue;
}
let snapshot = std::fs::read_to_string(router_sidecar_path(&path, "snapshot"))
.ok()
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "(unknown)".to_owned());
let rm = router_sidecar_path(&path, "rm").exists();
let log_path = state_dir.join(format!("router-{port}.log"));
rows.push(RouterRow {
port,
pid,
snapshot,
rm,
log_path,
});
}
rows.sort_by_key(|r| r.port);
Ok(rows)
}
fn run_ps(argv: &[String]) -> Result<(), String> {
let mut state_dir = default_state_dir();
let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"--state-dir" => {
i += 1;
state_dir = PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --state-dir value".to_owned())?,
);
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine ps [--state-dir DIR]\n\
\n\
List running supermachine daemons (background routers\n\
started via `supermachine run --detach`). Stale pidfiles\n\
from crashed daemons are swept as a side effect."
);
std::process::exit(0);
}
s => return Err(format!("unknown arg: {s}")),
}
i += 1;
}
let rows = list_routers(&state_dir)?;
if rows.is_empty() {
println!("no running supermachine daemons");
return Ok(());
}
let snap_w = rows
.iter()
.map(|r| r.snapshot.len())
.max()
.unwrap_or(8)
.max(8);
println!(
"{:>5} {:>7} {:<snap_w$} {:<3} ENDPOINT",
"PORT",
"PID",
"SNAPSHOT",
"RM",
snap_w = snap_w,
);
for r in &rows {
println!(
"{:>5} {:>7} {:<snap_w$} {:<3} http://127.0.0.1:{}/",
r.port,
r.pid,
r.snapshot,
if r.rm { "yes" } else { "no" },
r.port,
snap_w = snap_w,
);
}
Ok(())
}
fn run_logs(argv: &[String]) -> Result<(), String> {
let mut state_dir = default_state_dir();
let mut follow = false;
let mut workload_log = true;
let mut target: Option<String> = None;
let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"--follow" | "-f" => follow = true,
"--workload" => workload_log = true,
"--router" => workload_log = false,
"--state-dir" => {
i += 1;
state_dir = PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --state-dir value".to_owned())?,
);
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine logs [TARGET] [--workload|--router]\n\
\x20 [--follow] [--state-dir DIR]\n\
\n\
Tail logs for a running daemon. By default shows the\n\
workload's stdout/stderr (mixed with the guest kernel\n\
console — `docker logs`-equivalent for app debugging).\n\
Pass --router for the supermachine-router daemon's own\n\
startup log.\n\
\n\
TARGET = port number, snapshot name, or image ref. If\n\
omitted, picks the only running daemon (error if there\n\
are zero or more than one).\n\
\n\
Use --follow / -f to stream new lines as they arrive."
);
std::process::exit(0);
}
s if s.starts_with('-') => return Err(format!("unknown flag: {s}")),
s => {
if target.is_some() {
return Err(format!("extra arg: {s}"));
}
target = Some(s.to_owned());
}
}
i += 1;
}
let rows = list_routers(&state_dir)?;
let row = match (target.as_deref(), rows.len()) {
(None, 0) => return Err("no running supermachine daemons".to_owned()),
(None, 1) => rows.into_iter().next().unwrap(),
(None, _) => {
return Err(
"multiple daemons running — pass a port number or snapshot name (see `supermachine ps`)"
.to_owned(),
)
}
(Some(t), _) => {
if let Ok(port) = t.parse::<u16>() {
rows.into_iter()
.find(|r| r.port == port)
.ok_or_else(|| format!("no running daemon on port {port}"))?
} else {
let candidates = [t.to_owned(), bake::snapshot_name_for_image(t)];
rows.into_iter()
.find(|r| candidates.iter().any(|c| c == &r.snapshot))
.ok_or_else(|| format!("no running daemon for {t}"))?
}
}
};
let path = if workload_log {
PathBuf::from(format!(
"/tmp/supermachine-router-{}-w0.log",
row.snapshot
))
} else {
row.log_path.clone()
};
tail_log(&path, follow)
}
fn tail_log(path: &Path, follow: bool) -> Result<(), String> {
let mut file = std::fs::File::open(path)
.map_err(|e| format!("open {}: {e}", path.display()))?;
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let mut buf = [0u8; 8192];
loop {
let n = file
.read(&mut buf)
.map_err(|e| format!("read {}: {e}", path.display()))?;
if n == 0 {
break;
}
stdout
.write_all(&buf[..n])
.map_err(|e| format!("write stdout: {e}"))?;
}
if !follow {
return Ok(());
}
let pos = file
.seek(SeekFrom::Current(0))
.map_err(|e| format!("seek {}: {e}", path.display()))?;
let mut cursor = pos;
loop {
std::thread::sleep(Duration::from_millis(100));
let len = std::fs::metadata(path)
.map_err(|e| format!("stat {}: {e}", path.display()))?
.len();
if len < cursor {
cursor = 0;
file = std::fs::File::open(path)
.map_err(|e| format!("re-open {}: {e}", path.display()))?;
}
if len == cursor {
continue;
}
file.seek(SeekFrom::Start(cursor))
.map_err(|e| format!("seek {}: {e}", path.display()))?;
loop {
let n = file
.read(&mut buf)
.map_err(|e| format!("read {}: {e}", path.display()))?;
if n == 0 {
break;
}
stdout
.write_all(&buf[..n])
.map_err(|e| format!("write stdout: {e}"))?;
cursor += n as u64;
}
}
}
fn run_exec(argv: &[String]) -> Result<i32, String> {
let mut tty = false;
let mut env_pairs: Vec<(String, String)> = Vec::new();
let mut cwd: Option<String> = None;
let mut target: Option<String> = None;
let mut state_dir = default_state_dir();
let mut cmd: Option<Vec<String>> = None;
let mut i = 0;
while i < argv.len() {
let arg = argv[i].as_str();
match arg {
"--" => {
cmd = Some(argv[i + 1..].to_vec());
break;
}
"--tty" | "-t" => tty = true,
"--env" | "-e" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing --env value".to_owned())?
.clone();
let (k, val) = v
.split_once('=')
.ok_or_else(|| format!("--env expects K=V, got {v:?}"))?;
env_pairs.push((k.to_owned(), val.to_owned()));
}
"--cwd" | "-w" => {
i += 1;
cwd = Some(
argv.get(i)
.ok_or_else(|| "missing --cwd value".to_owned())?
.clone(),
);
}
"--state-dir" => {
i += 1;
state_dir = PathBuf::from(
argv.get(i)
.ok_or_else(|| "missing --state-dir value".to_owned())?,
);
}
"--help" | "-h" => {
eprintln!(
"usage: supermachine exec [TARGET] [--tty|-t] [--env K=V]\n\
\x20 [--cwd PATH] [--state-dir DIR] -- cmd args...\n\
\n\
Run a process inside a running guest. TARGET = port, snapshot\n\
name, or image ref (same as `logs`); omitted picks the only\n\
running daemon.\n\
\n\
Examples:\n\
\x20 supermachine exec -- /bin/sh -c 'ls /etc'\n\
\x20 supermachine exec --tty -- /bin/sh\n\
\x20 supermachine exec my-snap --env FOO=bar -- printenv FOO"
);
std::process::exit(0);
}
s if s.starts_with('-') => return Err(format!("unknown flag: {s}")),
s => {
if target.is_some() {
return Err(format!("extra arg: {s} (use `--` before the command)"));
}
target = Some(s.to_owned());
}
}
i += 1;
}
let cmd = cmd.ok_or_else(|| "missing command (use `--` to separate it from flags)".to_owned())?;
if cmd.is_empty() {
return Err("missing command after `--`".to_owned());
}
let rows = list_routers(&state_dir)?;
let row = match (target.as_deref(), rows.len()) {
(None, 0) => return Err("no running supermachine daemons".to_owned()),
(None, 1) => rows.into_iter().next().unwrap(),
(None, _) => {
return Err(
"multiple daemons running — pass a port number, snapshot name, or image ref"
.to_owned(),
)
}
(Some(t), _) => {
if let Ok(port) = t.parse::<u16>() {
rows.into_iter()
.find(|r| r.port == port)
.ok_or_else(|| format!("no running daemon on port {port}"))?
} else {
let candidates = [t.to_owned(), bake::snapshot_name_for_image(t)];
rows.into_iter()
.find(|r| candidates.iter().any(|c| c == &r.snapshot))
.ok_or_else(|| format!("no running daemon for {t}"))?
}
}
};
let exec_sock = PathBuf::from(format!(
"/tmp/supermachine-router-socks/{}-w0.sock-exec",
row.snapshot
));
if !exec_sock.exists() {
return Err(format!(
"exec socket missing at {} — does the daemon's snapshot have an in-guest agent? (snapshots baked before exec landed don't.)",
exec_sock.display()
));
}
let mut builder = supermachine::exec::ExecBuilder::new(exec_sock).argv(cmd);
for (k, v) in env_pairs {
builder = builder.env(k, v);
}
if let Some(c) = cwd {
builder = builder.cwd(c);
}
builder = builder.tty(tty);
if tty {
if let Some((cols, rows)) = current_winsize() {
builder = builder.winsize(cols, rows);
}
}
let mut child = builder
.spawn()
.map_err(|e| format!("dial agent: {e}"))?;
let _raw = if tty { Some(RawGuard::install()?) } else { None };
if tty {
spawn_winsize_forwarder(&child);
}
pump_stdio(&mut child, tty)?;
let status = child.wait().map_err(|e| format!("wait: {e}"))?;
Ok(status.code().unwrap_or(0))
}
fn current_winsize() -> Option<(u16, u16)> {
let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
let r = unsafe { libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) };
if r == 0 && ws.ws_col > 0 && ws.ws_row > 0 {
Some((ws.ws_col, ws.ws_row))
} else {
None
}
}
struct RawGuard {
fd: libc::c_int,
saved: libc::termios,
}
impl RawGuard {
fn install() -> Result<Self, String> {
let fd = libc::STDIN_FILENO;
let isatty = unsafe { libc::isatty(fd) };
if isatty == 0 {
return Ok(Self {
fd,
saved: unsafe { std::mem::zeroed() },
});
}
let mut saved: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(fd, &mut saved) } != 0 {
return Err(format!("tcgetattr: {}", std::io::Error::last_os_error()));
}
let mut raw = saved;
unsafe { libc::cfmakeraw(&mut raw) };
if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 {
return Err(format!("tcsetattr: {}", std::io::Error::last_os_error()));
}
Ok(Self { fd, saved })
}
}
impl Drop for RawGuard {
fn drop(&mut self) {
unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &self.saved) };
}
}
fn spawn_winsize_forwarder(_child: &supermachine::exec::ExecChild) {
use std::os::fd::{FromRawFd, OwnedFd};
use std::sync::atomic::{AtomicI32, Ordering};
static SIGWINCH_PIPE_W: AtomicI32 = AtomicI32::new(-1);
extern "C" fn handler(_: libc::c_int) {
let fd = SIGWINCH_PIPE_W.load(Ordering::SeqCst);
if fd >= 0 {
let b: u8 = 0;
unsafe {
libc::write(fd, &b as *const _ as *const libc::c_void, 1);
}
}
}
let mut fds = [0i32; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return;
}
let read_end = unsafe { OwnedFd::from_raw_fd(fds[0]) };
SIGWINCH_PIPE_W.store(fds[1], Ordering::SeqCst);
unsafe {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = handler as usize;
sa.sa_flags = libc::SA_RESTART;
libc::sigemptyset(&mut sa.sa_mask);
libc::sigaction(libc::SIGWINCH, &sa, std::ptr::null_mut());
}
drop(read_end);
}
fn pump_stdio(
child: &mut supermachine::exec::ExecChild,
tty: bool,
) -> Result<(), String> {
use std::io::{Read as _, Write as _};
let mut stdin = child.stdin().expect("stdin not yet taken");
let mut stdout = child.stdout().expect("stdout not yet taken");
let stderr = if tty {
child.stderr()
} else {
child.stderr()
};
let stdin_thread = std::thread::Builder::new()
.name("supermachine-exec-stdin".into())
.spawn(move || {
let mut local = std::io::stdin();
let mut buf = [0u8; 8192];
loop {
match local.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if stdin.write_all(&buf[..n]).is_err() {
break;
}
}
Err(_) => break,
}
}
let _ = stdin.close();
})
.map_err(|e| format!("spawn stdin pump: {e}"))?;
let stdout_thread = std::thread::Builder::new()
.name("supermachine-exec-stdout".into())
.spawn(move || {
let mut local = std::io::stdout();
let mut buf = [0u8; 8192];
loop {
match stdout.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if local.write_all(&buf[..n]).is_err() {
break;
}
let _ = local.flush();
}
Err(_) => break,
}
}
})
.map_err(|e| format!("spawn stdout pump: {e}"))?;
let stderr_thread = if let Some(mut stderr) = stderr {
Some(
std::thread::Builder::new()
.name("supermachine-exec-stderr".into())
.spawn(move || {
let mut local = std::io::stderr();
let mut buf = [0u8; 8192];
loop {
match stderr.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if local.write_all(&buf[..n]).is_err() {
break;
}
let _ = local.flush();
}
Err(_) => break,
}
}
})
.map_err(|e| format!("spawn stderr pump: {e}"))?,
)
} else {
None
};
let _ = stdin_thread;
let _ = stdout_thread.join();
if let Some(h) = stderr_thread {
let _ = h.join();
}
Ok(())
}
fn main() -> std::process::ExitCode {
let run_t0 = Instant::now();
let args = match parse_args() {
Ok(a) => a,
Err(e) => {
eprintln!("error: {e}");
usage();
return std::process::ExitCode::from(2);
}
};
#[cfg(target_os = "macos")]
if let Some(worker) = supermachine::codesign::locate_worker_bin() {
if let Err(e) = supermachine::codesign::ensure_worker_signed(&worker) {
eprintln!(
"warning: auto-codesign failed for {}: {e}\n \
If `hv_vm_create` errors with HV_DENIED (0xfae94007), \
retry `supermachine setup` manually.",
worker.display()
);
}
}
if args.connect {
let ok = if let Some(path) = args.probe_path.as_deref() {
probe_200(args.http_port, path)
} else {
health_ok(args.http_port)
};
return report_probe_result(
args.image.as_deref(),
args.http_port,
args.probe_path.as_deref(),
ok,
);
}
if args.stop {
return match stop_router(&args) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
std::process::ExitCode::from(1)
}
};
}
if let Err(e) = run_push(&args, run_t0) {
eprintln!("error: {e}");
return std::process::ExitCode::from(1);
}
if trace_enabled() {
eprintln!(
"supermachine: exec-router after {}ms",
elapsed_ms(run_t0)
);
}
if args.detach {
if let Err(e) = start_router_detached(&args) {
eprintln!("error: {e}");
return std::process::ExitCode::from(1);
}
if let Some(path) = args.probe_path.as_deref() {
return report_probe_result(
args.image.as_deref(),
args.http_port,
Some(path),
probe_200(args.http_port, path),
);
}
match workload_responding(args.http_port, 8000) {
Ok(true) => {
println!("endpoint: http://127.0.0.1:{}/", args.http_port);
std::process::ExitCode::SUCCESS
}
Ok(false) => {
eprintln!(
"workload at http://127.0.0.1:{}/ did not respond within 8s",
args.http_port
);
std::process::ExitCode::from(1)
}
Err(e) => {
eprintln!(
"workload at http://127.0.0.1:{}/ not responding: {e}",
args.http_port
);
std::process::ExitCode::from(1)
}
}
} else {
let result = run_router_foreground(&args);
if args.rm_on_stop {
if let Err(e) = rm_snapshot_for_args(&args) {
eprintln!("warning: --rm cleanup: {e}");
}
}
match result {
Ok(code) => std::process::ExitCode::from(code as u8),
Err(e) => {
eprintln!("error: {e}");
std::process::ExitCode::from(1)
}
}
}
}