diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a2cf7d6..88523634 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,9 @@ jobs: - name: Start plano natively env: OPENAI_API_KEY: test-key-not-used - run: crates/target/release/planoai up tests/e2e/config_native_smoke.yaml + run: | + crates/target/release/planoai up tests/e2e/config_native_smoke.yaml + sleep 2 - name: Health check run: | diff --git a/crates/plano-cli/src/native/runner.rs b/crates/plano-cli/src/native/runner.rs index 61d44a3d..0275b101 100644 --- a/crates/plano-cli/src/native/runner.rs +++ b/crates/plano-cli/src/native/runner.rs @@ -313,21 +313,29 @@ pub async fn start_native( Ok(()) } -/// Spawn a detached daemon process. Returns the child PID. +/// Spawn a fully detached daemon process. Returns the child PID. fn daemon_exec(args: &[String], env: &HashMap, log_path: &Path) -> Result { use std::os::unix::process::CommandExt; use std::process::{Command, Stdio}; let log_file = fs::File::create(log_path)?; - let child = Command::new(&args[0]) - .args(&args[1..]) - .envs(env) - .stdin(Stdio::null()) - .stdout(log_file.try_clone()?) - .stderr(log_file) - .process_group(0) // detach from parent's process group - .spawn()?; + // SAFETY: setsid() is async-signal-safe and called before exec + let child = unsafe { + Command::new(&args[0]) + .args(&args[1..]) + .envs(env) + .stdin(Stdio::null()) + .stdout(log_file.try_clone()?) + .stderr(log_file) + .pre_exec(|| { + // Create a new session so the child doesn't get SIGHUP/SIGTERM + // when the parent shell exits + nix::unistd::setsid().map_err(std::io::Error::other)?; + Ok(()) + }) + .spawn()? + }; Ok(child.id() as i32) }