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:
@@ -6,7 +6,7 @@ import { SearchBar } from "./SearchBar";
|
|||||||
import { StatusRing } from "./StatusRing";
|
import { StatusRing } from "./StatusRing";
|
||||||
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
import { COLORS, FONT, STATE_COLOR } from "./theme";
|
||||||
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
|
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
|
||||||
import { setRatios, restartSurface, setZoom, moveSurface, attachSurface } from "./socketBridge";
|
import { setRatios, restartSurface, setZoom, moveSurface, attachSurface, detachSurface } from "./socketBridge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -149,7 +149,7 @@ function StoppedSnapshot({ surfaceId, font, palette }: { surfaceId: string; font
|
|||||||
void attachSurface(surfaceId, () => {}).then((res) => {
|
void attachSurface(surfaceId, () => {}).then((res) => {
|
||||||
if (!disposed && res.snapshot) term.write(res.snapshot);
|
if (!disposed && res.snapshot) term.write(res.snapshot);
|
||||||
});
|
});
|
||||||
return () => { disposed = true; term.dispose(); };
|
return () => { disposed = true; term.dispose(); void detachSurface(surfaceId); };
|
||||||
}, [surfaceId, font, palette]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [surfaceId, font, palette]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
return <div ref={hostRef} style={{ position: "absolute", inset: 0, opacity: 0.45, pointerEvents: "none" }} />;
|
return <div ref={hostRef} style={{ position: "absolute", inset: 0, opacity: 0.45, pointerEvents: "none" }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,17 +453,19 @@ async fn handle_request(
|
|||||||
let Some(ws) = reg.workspace(&workspace_id).cloned() else {
|
let Some(ws) = reg.workspace(&workspace_id).cloned() else {
|
||||||
let _ = out.send(err(id, "NOT_FOUND", "workspace")).await; return;
|
let _ = out.send(err(id, "NOT_FOUND", "workspace")).await; return;
|
||||||
};
|
};
|
||||||
// Kill current panels of this workspace.
|
// Additive: keep existing panels (and their live processes) in their
|
||||||
let existing: Vec<SurfaceId> = ws.surfaces.keys().cloned().collect();
|
// current visual order, spawn only the delta needed to reach `count`,
|
||||||
for sid in &existing {
|
// then rebuild the tree to the preset shape. Presets never destroy
|
||||||
if let Some(h) = reg.live(sid) { let _ = h.tx.send(crate::surface::SurfaceMsg::Close).await; }
|
// running panels — shrinking is done by closing panels via the X. The
|
||||||
reg.remove_surface(sid);
|
// GUI only offers presets whose count >= the current pane count, so
|
||||||
subs.remove(sid);
|
// `count >= existing.len()` and `ids.len() == count` after the loop.
|
||||||
}
|
let existing: Vec<SurfaceId> = ws.layout.as_ref()
|
||||||
// Spawn `count` panels (slots padded/truncated to count).
|
.map(spacesh_core::ops::leaves)
|
||||||
let mut new_ids = Vec::new();
|
.unwrap_or_else(|| ws.surfaces.keys().cloned().collect());
|
||||||
for i in 0..count {
|
let mut ids = existing.clone();
|
||||||
let slot = slots.get(i);
|
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 new_sid = reg.new_surface_id();
|
||||||
let command = slot.and_then(|s| s.command.clone());
|
let command = slot.and_then(|s| s.command.clone());
|
||||||
let shell = command.clone().unwrap_or_else(|| config.resolved_shell());
|
let shell = command.clone().unwrap_or_else(|| config.resolved_shell());
|
||||||
@@ -476,20 +478,18 @@ async fn handle_request(
|
|||||||
reg.set_live(handle);
|
reg.set_live(handle);
|
||||||
reg.set_state(&new_sid, spacesh_proto::SurfaceState::Idle);
|
reg.set_state(&new_sid, spacesh_proto::SurfaceState::Idle);
|
||||||
reg.add_surface_spec(&workspace_id, new_sid.clone(), spec);
|
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; }
|
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); }
|
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);
|
emit_layout(reg, &workspace_id, clients);
|
||||||
persister.mark_dirty(reg.persist_state());
|
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 } => {
|
Cmd::RestartSurface { surface_id, resume } => {
|
||||||
@@ -732,6 +732,19 @@ async fn handle_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Cmd::Shutdown => {
|
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;
|
let _ = out.send(ok(id, serde_json::Value::Null)).await;
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user