fix(sbx): make /sbx save work on a live multipass sandbox
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
CI / rust client (hh) (macos-latest) (push) Waiting to run
CI / rust client (hh) (ubuntu-latest) (push) Waiting to run
CI / rust coverage (push) Waiting to run
CI / python server (3.10) (push) Waiting to run
CI / python server (3.11) (push) Waiting to run
CI / python server (3.12) (push) Waiting to run
CI / headless e2e smoke (push) Waiting to run
CI / dependency audit (push) Waiting to run
CI / secret scanning (push) Waiting to run
Multipass can only snapshot a powered-off instance, so `/sbx save` on a running multipass sandbox previously just surfaced multipass's "instance must be stopped" error — making the snapshot that `/sbx load` needs impossible to create through the UI. Now multipass save powers the instance down before snapshotting, and the handler tears the shared session down first (same as `/sbx stop`, but WITHOUT purging so the instance — and thus the snapshot — survives for a later `/sbx load`). Docker is unchanged: `docker commit` captures a live container, so its save stays non-disruptive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1673,6 +1673,20 @@ fn handle_command(
|
||||
"saving sandbox state as '{label}'{}…",
|
||||
if local { " (+ local copy)" } else { "" }
|
||||
));
|
||||
// Docker commits a *live* container, so its save is non-
|
||||
// disruptive. Multipass can only snapshot a powered-off
|
||||
// instance, so saving it necessarily stops the shared shell —
|
||||
// tear the live session down (same as `/sbx stop`, but WITHOUT
|
||||
// purging: the instance must survive so the snapshot does too).
|
||||
if be == sbx::Backend::Multipass {
|
||||
if let Some(mut sb) = broker.take() {
|
||||
sb.stop();
|
||||
}
|
||||
broker_meta.take();
|
||||
*announced_dims = None;
|
||||
send_frame(out_tx, room, json!({"_sbx":"status","state":"stopped"}));
|
||||
app.sys("multipass must power off to snapshot — stopping the shared shell, then saving (reload it with `/sbx load`)");
|
||||
}
|
||||
let (tx, lbl) = (app_tx.clone(), label.clone());
|
||||
tokio::spawn(async move {
|
||||
let res = tokio::task::spawn_blocking(move || sbx::save_state(be, &name, &label, local)).await;
|
||||
|
||||
@@ -589,6 +589,16 @@ pub fn save_state(backend: Backend, name: &str, label: &str, local: bool) -> Res
|
||||
Ok(format!("image {tag}"))
|
||||
}
|
||||
Backend::Multipass => {
|
||||
// Multipass only snapshots a *stopped* instance (unlike docker, which
|
||||
// commits a live container). Power it off first — the caller has
|
||||
// already torn down the shared shell, so this is the save *and* stop.
|
||||
// The instance stays registered (we don't purge), so `/sbx load` can
|
||||
// restore the snapshot later.
|
||||
let _ = Command::new("multipass")
|
||||
.args(["stop", name])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
let out = Command::new("multipass")
|
||||
.args(["snapshot", name, "--name", label])
|
||||
.output()
|
||||
|
||||
Reference in New Issue
Block a user