fix(daemon,app): graceful-shutdown final snapshot pass + StoppedSnapshot detach cleanup

Addresses final-review findings: Cmd::Shutdown now snapshots all live surfaces
synchronously before exit (spec graceful-shutdown requirement); StoppedSnapshot
calls detachSurface on unmount to release the bridge output channel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:24:53 +07:00
parent 5c76493a34
commit ce6a8d56be
2 changed files with 32 additions and 19 deletions
+30 -17
View File
@@ -453,17 +453,19 @@ async fn handle_request(
let Some(ws) = reg.workspace(&workspace_id).cloned() else {
let _ = out.send(err(id, "NOT_FOUND", "workspace")).await; return;
};
// Kill current panels of this workspace.
let existing: Vec<SurfaceId> = ws.surfaces.keys().cloned().collect();
for sid in &existing {
if let Some(h) = reg.live(sid) { let _ = h.tx.send(crate::surface::SurfaceMsg::Close).await; }
reg.remove_surface(sid);
subs.remove(sid);
}
// Spawn `count` panels (slots padded/truncated to count).
let mut new_ids = Vec::new();
for i in 0..count {
let slot = slots.get(i);
// Additive: keep existing panels (and their live processes) in their
// current visual order, spawn only the delta needed to reach `count`,
// then rebuild the tree to the preset shape. Presets never destroy
// running panels — shrinking is done by closing panels via the X. The
// GUI only offers presets whose count >= the current pane count, so
// `count >= existing.len()` and `ids.len() == count` after the loop.
let existing: Vec<SurfaceId> = ws.layout.as_ref()
.map(spacesh_core::ops::leaves)
.unwrap_or_else(|| ws.surfaces.keys().cloned().collect());
let mut ids = existing.clone();
let to_spawn = count.saturating_sub(existing.len());
for j in 0..to_spawn {
let slot = slots.get(existing.len() + j);
let new_sid = reg.new_surface_id();
let command = slot.and_then(|s| s.command.clone());
let shell = command.clone().unwrap_or_else(|| config.resolved_shell());
@@ -476,20 +478,18 @@ async fn handle_request(
reg.set_live(handle);
reg.set_state(&new_sid, spacesh_proto::SurfaceState::Idle);
reg.add_surface_spec(&workspace_id, new_sid.clone(), spec);
new_ids.push(new_sid);
broadcast_evt(clients, &Envelope::Evt(Evt::SurfaceCreated { surface_id: new_sid.clone(), workspace_id: workspace_id.clone() }));
ids.push(new_sid);
}
Err(e) => { let _ = out.send(err(id, "SPAWN_FAILED", &e.to_string())).await; return; }
}
}
if let Some(tree) = spacesh_core::presets::build(&preset_id, &new_ids) {
if let Some(tree) = spacesh_core::presets::build(&preset_id, &ids) {
if let Some(w) = reg.workspace_mut(&workspace_id) { w.layout = Some(tree); }
}
for sid in &new_ids {
broadcast_evt(clients, &Envelope::Evt(Evt::SurfaceCreated { surface_id: sid.clone(), workspace_id: workspace_id.clone() }));
}
emit_layout(reg, &workspace_id, clients);
persister.mark_dirty(reg.persist_state());
let _ = out.send(ok(id, serde_json::json!({ "surface_ids": new_ids.iter().map(|s| s.0.clone()).collect::<Vec<_>>() }))).await;
let _ = out.send(ok(id, serde_json::json!({ "surface_ids": ids.iter().map(|s| s.0.clone()).collect::<Vec<_>>() }))).await;
}
Cmd::RestartSurface { surface_id, resume } => {
@@ -732,6 +732,19 @@ async fn handle_request(
}
Cmd::Shutdown => {
// Final snapshot pass: capture each live surface's visible screen so a
// clean restart (e.g. Settings → Restart daemon) repaints last screens.
// Written synchronously through the store (the async writer task would
// not drain before process::exit).
for sid in reg.live_ids() {
let Some(handle) = reg.live(&sid) else { continue };
let (reply_tx, reply_rx) = oneshot::channel();
if handle.tx.send(SurfaceMsg::Snapshot { reply: reply_tx }).await.is_ok() {
if let Ok((snap, _dirty)) = reply_rx.await {
snapshot_store.save(&sid, &snap);
}
}
}
let _ = out.send(ok(id, serde_json::Value::Null)).await;
std::process::exit(0);
}