fix(engine): kill escaped child processes on worktree cleanup#157
Conversation
npm dev servers and other Node.js subprocesses commonly call setsid() or are spawned with detached:true, creating their own process groups. This caused them to survive killpg() on worktree deletion, leaving stale processes consuming resources. Fix: before signalling, snapshot the full descendant tree via `ps -ax -o pid,ppid` and walk PPIDs from the agent's root. These PIDs are killed directly alongside the existing killpg call, catching any process that escaped to a different process group. Covers both SIGTERM and SIGKILL phases. pnpm was unaffected because it typically execs into the script process rather than forking detached children. Also hooks kill_all_sessions() into the managed-mode parent-died path so a macOS app force-quit reaps agent subtrees before daemon exit, and updates the Xcode build script to copy pu-engine to ~/.pu/bin/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Warning Review limit reached
Your plan currently allows 1 review/hour. Refill in 27 minutes and 52 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 418bbaa614
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for handle in &handles { | ||
| if let Ok(pid) = i32::try_from(handle.pid) { | ||
| unsafe { | ||
| libc::killpg(pid, libc::SIGTERM); | ||
| } |
There was a problem hiding this comment.
Reap escaped descendants in managed shutdown path
main.rs now calls Engine::kill_all_sessions() when the app parent dies, but this helper only sends killpg to each session PID. Descendants that called setsid()/setpgid() (the exact escaped-child case handled in NativePtyHost::kill) are outside that process group, so they survive this shutdown path and remain orphaned after process::exit(0). This means force-quitting the macOS app can still leak detached node/dev-server processes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit b1762e0. kill_all_sessions now takes a shared ps snapshot via snapshot_process_tree(), collects descendants for all handles with descendants_from_tree(), and sends SIGTERM then SIGKILL to both process groups (killpg) and escaped descendants. The single snapshot is shared across all handles to avoid multiple ps invocations.
| for &desc in &descendants { | ||
| unsafe { | ||
| libc::kill(desc, libc::SIGKILL); | ||
| } |
There was a problem hiding this comment.
Recompute descendant PIDs before forced SIGKILL
The descendant PID list is snapshotted once (collect_process_descendants) and then reused after the grace wait for the forced-kill pass. If any descendant exits during that window, its PID can be reused by an unrelated process, and the later libc::kill(desc, SIGKILL) may terminate the wrong process. This risk appears only under PID reuse races, but when it happens it can kill unrelated user workloads.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit a875d30. Both the kill() method in pty_manager.rs and kill_all_sessions in engine/mod.rs now gate each descendant SIGKILL on kill(desc, 0) == 0. A non-zero result means the PID no longer refers to the original process (exited or recycled), so we skip the SIGKILL. The descendant snapshot is still taken before SIGTERM — that's intentional to capture the full tree while processes are alive — but the per-PID liveness check before each SIGKILL prevents hitting a recycled PID.
- Update rand 0.10.0 → 0.10.1 to resolve RUSTSEC-2026-0097 (unsound with custom logger using rand::rng(); only triggered under very specific conditions but flagged by cargo-deny) - Add deny.toml ignore for RUSTSEC-2025-0068 (serde_yml unsound/ unmaintained, no safe upgrade exists — tracked for replacement separately) Both advisories were already failing on main before this PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split collect_process_descendants into snapshot_process_tree (one ps call) + descendants_from_tree (pure tree walk). kill_all_sessions now takes a single snapshot and kills escaped descendants for all handles in one pass — consistent with the worktree-cleanup kill path and efficient regardless of how many agents are running. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…D reuse Before force-killing escaped descendants, verify the PID still refers to the original process via kill(pid, 0). If the PID was recycled during the grace period the liveness check fails and we skip the SIGKILL, preventing accidental kills of unrelated processes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bility Xcode 26 (runner image 20260520) uses a newer Swift compiler that enforces strict concurrency more aggressively, causing TreeSitterClient to error with "sending 'result' risks causing data races". The latest neon main commit (484d6fb) uses nonisolated(unsafe) to address these Swift 6.1 stricter checks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Test plan
🤖 Generated with Claude Code