feat(daemon): spawn_from_spec to (re)start surfaces from a persisted spec

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 21:22:29 +07:00
parent d516414ac9
commit b72f4cb3a5
+47 -1
View File
@@ -1,10 +1,31 @@
use spacesh_core::{snapshot::snapshot_ansi, GridSurface}; use spacesh_core::{snapshot::snapshot_ansi, GridSurface};
use spacesh_core::snapshot::Snapshot; use spacesh_core::snapshot::Snapshot;
use spacesh_proto::{SurfaceId, WorkspaceId}; use spacesh_proto::{SurfaceId, WorkspaceId};
use spacesh_pty::PtyHandle; use spacesh_proto::workspace::SurfaceSpec;
use spacesh_pty::{PtyHandle, SpawnSpec};
use tokio::sync::{broadcast, mpsc, oneshot}; use tokio::sync::{broadcast, mpsc, oneshot};
use tokio::time::{Duration, Instant}; use tokio::time::{Duration, Instant};
/// Spawn (or restart) a surface actor from a persisted spec. Injects
/// SPACESH_SURFACE_ID into the child env, mirroring `new_surface`.
pub fn spawn_from_spec(
id: SurfaceId,
workspace_id: WorkspaceId,
spec: &SurfaceSpec,
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
) -> std::io::Result<SurfaceHandle> {
let pty = PtyHandle::spawn(SpawnSpec {
command: spec.command.clone(),
args: spec.args.clone(),
cwd: std::path::PathBuf::from(&spec.cwd),
cols: spec.cols,
rows: spec.rows,
env: vec![("SPACESH_SURFACE_ID".into(), id.0.clone())],
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
Ok(spawn_surface(id, workspace_id, pty, spec.cols, spec.rows, exit_tx))
}
const BROADCAST_CAP: usize = 1024; const BROADCAST_CAP: usize = 1024;
const FLUSH_INTERVAL: Duration = Duration::from_millis(6); const FLUSH_INTERVAL: Duration = Duration::from_millis(6);
const FLUSH_BYTES: usize = 16 * 1024; const FLUSH_BYTES: usize = 16 * 1024;
@@ -181,4 +202,29 @@ mod tests {
let (snap, _sub) = reply_rx.await.unwrap(); let (snap, _sub) = reply_rx.await.unwrap();
assert!(snap.ansi.contains("SNAPME"), "snapshot: {:?}", snap.ansi); assert!(snap.ansi.contains("SNAPME"), "snapshot: {:?}", snap.ansi);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn spawn_from_spec_runs_the_command() {
let _serial = crate::test_support::serial();
let spec = SurfaceSpec {
command: "/bin/sh".into(),
args: vec!["-c".into(), "printf RESPAWN; sleep 0.3".into()],
cwd: std::env::temp_dir().to_string_lossy().into(),
agent_label: None, cols: 80, rows: 24, autostart: false,
};
let (exit_tx, _rx) = mpsc::unbounded_channel();
let handle = spawn_from_spec(SurfaceId("s_r".into()), WorkspaceId("w_1".into()), &spec, exit_tx).unwrap();
let (reply_tx, reply_rx) = oneshot::channel();
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
let mut sub = reply_rx.await.unwrap();
let mut got = String::new();
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(2);
while tokio::time::Instant::now() < deadline {
if let Ok(Ok(b)) = tokio::time::timeout(tokio::time::Duration::from_millis(100), sub.recv()).await {
got.push_str(&String::from_utf8_lossy(&b));
if got.contains("RESPAWN") { break; }
}
}
assert!(got.contains("RESPAWN"), "got: {got:?}");
}
} }