diff --git a/crates/spaceshd/src/surface.rs b/crates/spaceshd/src/surface.rs index bc68b14..0e739f4 100644 --- a/crates/spaceshd/src/surface.rs +++ b/crates/spaceshd/src/surface.rs @@ -1,10 +1,31 @@ use spacesh_core::{snapshot::snapshot_ansi, GridSurface}; use spacesh_core::snapshot::Snapshot; 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::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 { + 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 FLUSH_INTERVAL: Duration = Duration::from_millis(6); const FLUSH_BYTES: usize = 16 * 1024; @@ -181,4 +202,29 @@ mod tests { let (snap, _sub) = reply_rx.await.unwrap(); 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:?}"); + } }