From 4c51a61907026e18a6d48ff116021b8095d43652 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 28 May 2026 13:20:50 +1000 Subject: [PATCH 1/2] fix(staged): detach interactive env-capture shell from the user's TTY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `capture_shell_env`/`capture_shell_env_blocking` spawned `$SHELL -ils` inheriting the parent's controlling terminal. The interactive shell then `tcsetpgrp`'d the user's real /dev/tty and, on exit, left the foreground process group pointing at a dead PGID — causing the outer zsh to die with EIO on TTY read after `just dev` had been running. Add `setsid()` via `pre_exec` to both spawn paths so the child runs in a new session with no controlling terminal, matching the canonical pattern in `crates/builderbot-actions/src/executor.rs`. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Matt Toohey --- apps/staged/src-tauri/src/shell_env.rs | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/staged/src-tauri/src/shell_env.rs b/apps/staged/src-tauri/src/shell_env.rs index 327af076..803b76c0 100644 --- a/apps/staged/src-tauri/src/shell_env.rs +++ b/apps/staged/src-tauri/src/shell_env.rs @@ -476,6 +476,22 @@ async fn capture_shell_env( .stderr(Stdio::null()) .kill_on_drop(true); + // Detach from the parent's controlling TTY so an interactive `$SHELL -ils` + // can't `tcsetpgrp` onto the user's terminal or mutate its modes via + // rcfiles. Without this, the spawned shell inherits the staged binary's + // ctty (the user's real /dev/tty up the chain), and on exit leaves the + // user's TTY foreground-PG pointing at a dead PGID — the outer zsh later + // dies with EIO on read. + #[cfg(unix)] + unsafe { + use std::os::unix::process::CommandExt; + // SAFETY: `setsid()` is async-signal-safe. + cmd.pre_exec(|| { + libc::setsid(); + Ok(()) + }); + } + let mut child = cmd.spawn()?; { let mut stdin = child @@ -534,6 +550,17 @@ fn capture_shell_env_blocking( .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()); + // See `capture_shell_env` for why this is required. + #[cfg(unix)] + unsafe { + use std::os::unix::process::CommandExt; + // SAFETY: `setsid()` is async-signal-safe. + cmd.pre_exec(|| { + libc::setsid(); + Ok(()) + }); + } + let mut child = cmd.spawn()?; { let mut stdin = child From da6e3553b2e0409489ec7fdfc08dada5d7a63968 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 28 May 2026 16:31:58 +1000 Subject: [PATCH 2/2] chore(staged): drop redundant CommandExt import in async env-capture `tokio::process::Command` exposes `pre_exec` as an inherent unsafe method on Unix, so the `use std::os::unix::process::CommandExt;` line added in 4c51a61 was dead. The sibling sync path still needs the import because `std::process::Command::pre_exec` only exists via the trait. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Matt Toohey --- apps/staged/src-tauri/src/shell_env.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/staged/src-tauri/src/shell_env.rs b/apps/staged/src-tauri/src/shell_env.rs index 803b76c0..06a6fa10 100644 --- a/apps/staged/src-tauri/src/shell_env.rs +++ b/apps/staged/src-tauri/src/shell_env.rs @@ -484,7 +484,6 @@ async fn capture_shell_env( // dies with EIO on read. #[cfg(unix)] unsafe { - use std::os::unix::process::CommandExt; // SAFETY: `setsid()` is async-signal-safe. cmd.pre_exec(|| { libc::setsid();