diff --git a/crates/maestro-daemon/src/session.rs b/crates/maestro-daemon/src/session.rs index b6cef9e..ad27037 100644 --- a/crates/maestro-daemon/src/session.rs +++ b/crates/maestro-daemon/src/session.rs @@ -228,12 +228,14 @@ impl SessionManager { return Err(e.into()); } }; - if let Err(e) = self.inner.tmux.new_session_piped( - id, - &effective_cwd, - &req.command, - &pipe_pane_command(&fifo_path), - ) { + let spawned = { + let tmux = self.inner.tmux.clone(); + let cwd = effective_cwd.clone(); + let command = req.command.clone(); + let sink = pipe_pane_command(&fifo_path); + tmux_blocking(move || tmux.new_session_piped(id, &cwd, &command, &sink)).await + }; + if let Err(e) = spawned { // `bridge` drops at the end of this block, closing the FIFO reader. let _ = std::fs::remove_file(&fifo_path); self.rollback_worktree(&worktree_base_repo, &worktree_path); @@ -254,7 +256,8 @@ impl SessionManager { if let Err(e) = self.inner.sidecar.write(&session) { // tmux already spawned the session; kill it so no ghost survives a // failed sidecar write, then unwind the FIFO + worktree. - let _ = self.inner.tmux.kill_session(id); + let tmux = self.inner.tmux.clone(); + let _ = tmux_blocking(move || tmux.kill_session(id)).await; let _ = std::fs::remove_file(&fifo_path); self.rollback_worktree(&worktree_base_repo, &worktree_path); return Err(SessionError::Sidecar(e)); @@ -297,7 +300,8 @@ impl SessionManager { }; let record = removed.ok_or(SessionError::NotFound(id))?; - let _ = self.inner.tmux.kill_session(id); + let tmux = self.inner.tmux.clone(); + let _ = tmux_blocking(move || tmux.kill_session(id)).await; let _ = std::fs::remove_file(&record.fifo_path); self.inner .sidecar @@ -346,7 +350,10 @@ impl SessionManager { /// us to repair them. pub async fn recover(&self) -> Result { tokio::fs::create_dir_all(&self.inner.fifo_dir).await?; - let live = self.inner.tmux.list_sessions()?; + let live = { + let tmux = self.inner.tmux.clone(); + tmux_blocking(move || tmux.list_sessions()).await? + }; let mut adopted = 0usize; for id in live { let mut session = match self.inner.sidecar.read(id) { @@ -381,10 +388,11 @@ impl SessionManager { // Re-pipe so output starts flowing again. `Tmux::pipe_pane` runs // without tmux's `-o` flag, so any stale pipe the previous daemon // left running to a now-unread FIFO is replaced (NAP-228). - let _ = self - .inner - .tmux - .pipe_pane(id, &pipe_pane_command(&fifo_path)); + let _ = { + let tmux = self.inner.tmux.clone(); + let sink = pipe_pane_command(&fifo_path); + tmux_blocking(move || tmux.pipe_pane(id, &sink)).await + }; let mut sessions = self.inner.sessions.lock().await; sessions.insert( id, @@ -407,9 +415,10 @@ impl SessionManager { if !self.has(id).await { return Err(SessionError::NotFound(id)); } - self.inner - .tmux - .send_keys(id, bytes) + let tmux = self.inner.tmux.clone(); + let bytes = bytes.to_vec(); + tmux_blocking(move || tmux.send_keys(id, &bytes)) + .await .map_err(SessionError::Tmux) } @@ -417,13 +426,28 @@ impl SessionManager { if !self.has(id).await { return Err(SessionError::NotFound(id)); } - self.inner - .tmux - .resize_window(id, cols, rows) + let tmux = self.inner.tmux.clone(); + tmux_blocking(move || tmux.resize_window(id, cols, rows)) + .await .map_err(SessionError::Tmux) } } +/// Run a blocking `tmux` subprocess call on the blocking thread pool so it +/// never stalls a tokio worker. `tmux` is a fork+exec that can take tens of +/// milliseconds on a loaded host, and `send_keys` rides the per-keystroke hot +/// path — running it inline would starve HTTP handlers, the registry WS, the +/// liveness watcher, and PTY readers (NAP-256). +async fn tmux_blocking(f: F) -> T +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + tokio::task::spawn_blocking(f) + .await + .expect("tmux blocking task panicked") +} + /// One pass of the liveness watcher: diff the in-memory session set against /// the tmux-reported one, and reap anything we still track but tmux has lost. async fn run_liveness_pass(inner: &Arc) { @@ -439,11 +463,14 @@ async fn run_liveness_pass(inner: &Arc) { sessions.keys().copied().collect() }; - let live_ids = match inner.tmux.list_sessions() { - Ok(ids) => ids, - Err(e) => { - tracing::warn!(error = ?e, "liveness watcher: tmux list-sessions failed"); - return; + let live_ids = { + let tmux = inner.tmux.clone(); + match tmux_blocking(move || tmux.list_sessions()).await { + Ok(ids) => ids, + Err(e) => { + tracing::warn!(error = ?e, "liveness watcher: tmux list-sessions failed"); + return; + } } }; let live: HashSet = live_ids.into_iter().collect();