From 2d5762fea685e563141e745177c26850604e6ffe Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 12 Apr 2026 01:14:13 -0700 Subject: [PATCH] fix: detach ecc2 background session runners --- ecc2/src/session/manager.rs | 94 ++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index d8fd1e55..fff81a36 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -2983,7 +2983,8 @@ async fn spawn_session_runner_for_program( working_dir: &Path, current_exe: &Path, ) -> Result<()> { - let child = Command::new(current_exe) + let mut command = Command::new(current_exe); + command .arg("run-session") .arg("--session-id") .arg(session_id) @@ -2995,7 +2996,10 @@ async fn spawn_session_runner_for_program( .arg(working_dir) .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stderr(Stdio::null()); + configure_background_runner_command(&mut command); + + let child = command .spawn() .with_context(|| format!("Failed to spawn ECC runner from {}", current_exe.display()))?; @@ -3005,6 +3009,35 @@ async fn spawn_session_runner_for_program( Ok(()) } +fn configure_background_runner_command(command: &mut Command) { + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + + // Detach the runner from the caller's shell/session so it keeps + // processing a live harness session after `ecc-tui start` returns. + unsafe { + command.as_std_mut().pre_exec(|| { + if libc::setsid() == -1 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + + const DETACHED_PROCESS: u32 = 0x0000_0008; + const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200; + command + .as_std_mut() + .creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP); + } +} + fn build_agent_command( cfg: &Config, agent_type: &str, @@ -5032,6 +5065,22 @@ mod tests { anyhow::bail!("timed out waiting for {}", path.display()); } + fn wait_for_text(path: &Path, needle: &str) -> Result { + for _ in 0..200 { + if path.exists() { + let content = fs::read_to_string(path) + .with_context(|| format!("failed to read {}", path.display()))?; + if content.contains(needle) { + return Ok(content); + } + } + + thread::sleep(StdDuration::from_millis(20)); + } + + anyhow::bail!("timed out waiting for {}", path.display()); + } + fn command_env_map(command: &Command) -> BTreeMap { command .as_std() @@ -5047,6 +5096,47 @@ mod tests { .collect() } + #[cfg(unix)] + #[tokio::test(flavor = "current_thread")] + async fn background_runner_command_starts_new_session() -> Result<()> { + let tempdir = TestDir::new("manager-detached-runner")?; + let script_path = tempdir.path().join("detached-runner.py"); + let log_path = tempdir.path().join("detached-runner.log"); + let script = format!( + "#!/usr/bin/env python3\nimport os\nimport pathlib\nimport time\n\npath = pathlib.Path(r\"{}\")\npath.write_text(f\"pid={{os.getpid()}} sid={{os.getsid(0)}}\", encoding=\"utf-8\")\ntime.sleep(30)\n", + log_path.display() + ); + fs::write(&script_path, script)?; + let mut permissions = fs::metadata(&script_path)?.permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&script_path, permissions)?; + + let mut command = Command::new(&script_path); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + configure_background_runner_command(&mut command); + + let mut child = command.spawn()?; + let child_pid = child.id().context("detached child pid")? as i32; + let content = wait_for_text(&log_path, "sid=")?; + let sid = content + .split_whitespace() + .find_map(|part| part.strip_prefix("sid=")) + .context("session id should be logged")? + .parse::() + .context("session id should parse")?; + let parent_sid = unsafe { libc::getsid(0) }; + + assert_eq!(sid, child_pid); + assert_ne!(sid, parent_sid); + + let _ = child.kill().await; + let _ = child.wait().await; + Ok(()) + } + fn write_package_manager_project_files( repo_root: &Path, package_manager_field: Option<&str>,