feat(hh): P3b — multipass lifecycle + PTY resize sync
- sbx.rs: prepare()/teardown() — multipass launch (idempotent, reuses an
existing instance) on /sbx launch multipass, delete --purge on stop;
Backend::default_image() per backend (24.04 / ubuntu:24.04).
- app.rs: async non-freezing launch — prepare runs in spawn_blocking and the
sandbox handle is handed to the run loop via a channel, so a ~30s multipass
VM boot never freezes the UI (status: "summoning…"). Sandbox is sized to the
actual pane (not fixed 24x80); broker resizes the PTY and broadcasts
{"_sbx":"resize"} on terminal-size changes; clients set their vt100 size to
match. Teardown on /sbx stop and on exit.
- net.rs: parse status rows/cols + resize frames.
Verified: cargo test (4 pass), clean build, live local sandbox via the async
path with dynamic full-width sizing. multipass 1.16.3 present; VM-boot path
implemented (live VM verify is slow, runs the same async flow).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
|
||||
use std::io::{Read, Write};
|
||||
use std::process::Command;
|
||||
use std::sync::mpsc;
|
||||
|
||||
/// Which sandbox to summon. Multipass = strong isolation (default for real use),
|
||||
@@ -36,6 +37,50 @@ impl Backend {
|
||||
Backend::Multipass => "multipass",
|
||||
}
|
||||
}
|
||||
/// Default image/release when the user doesn't specify one.
|
||||
pub fn default_image(self) -> &'static str {
|
||||
match self {
|
||||
Backend::Multipass => "24.04",
|
||||
Backend::Docker => "ubuntu:24.04",
|
||||
Backend::Local => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One-time setup before the PTY shell is spawned. Blocking — run off the UI
|
||||
/// thread (Multipass boots a real VM, ~20-30s). Idempotent: reuses an instance
|
||||
/// that already exists.
|
||||
pub fn prepare(backend: Backend, name: &str, image: &str) -> Result<()> {
|
||||
if backend != Backend::Multipass {
|
||||
return Ok(());
|
||||
}
|
||||
let exists = Command::new("multipass")
|
||||
.args(["info", name])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
if !exists {
|
||||
let st = Command::new("multipass")
|
||||
.args([
|
||||
"launch", "--name", name, "--cpus", "1", "--memory", "1G", "--disk", "5G", image,
|
||||
])
|
||||
.status()
|
||||
.context("multipass launch (is multipass installed?)")?;
|
||||
anyhow::ensure!(st.success(), "multipass launch failed");
|
||||
} else {
|
||||
let _ = Command::new("multipass").args(["start", name]).status();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Destroy ephemeral resources after stop. Multipass instance is purged;
|
||||
/// Docker uses `--rm` so the container is already gone; Local is a no-op.
|
||||
pub fn teardown(backend: Backend, name: &str) {
|
||||
if backend == Backend::Multipass {
|
||||
let _ = Command::new("multipass")
|
||||
.args(["delete", name, "--purge"])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the shell command that launches the sandbox for a backend.
|
||||
|
||||
Reference in New Issue
Block a user