fix(server): terminal subprocess polling no longer floods the PID space - #6377
fix(server): terminal subprocess polling no longer floods the PID space#6377SunkenInTime wants to merge 3 commits into
Conversation
The terminal subprocess poller ran pgrep and a full ps table dump per terminal every second, each spawned by bare name so every call walked the full PATH with one failed posix_spawn per directory. With many terminals this sustains hundreds of process creations per second and can wrap the macOS PID space (pingdotgg#6332). Now one ps -eo pid=,ppid=,comm= snapshot (resolved to an absolute path at startup) is taken per poll tick and every terminal derives its child state from that table, matching the shape the Windows path already used.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2861360. Configure here.
ApprovabilityVerdict: Approved f10cf45 This PR fixes PID space exhaustion by refactoring terminal subprocess polling to use one shared process table snapshot per tick instead of per-terminal spawns. The change is well-scoped to internal polling logic, includes test coverage for the new behavior, and properly handles failures by preserving prior state. You can customize Macroscope's approvability policy. Learn more. |
A failed, timed-out, or truncated ps/PowerShell snapshot was parsed into an empty table, marking every terminal idle and clearing its registered process ids. Fail the snapshot instead so the tick logs a warning and keeps prior state. Flagged by Macroscope and Cursor Bugbot on the PR.
There was a problem hiding this comment.
One convention finding on the new snapshot failure path in apps/server/src/terminal/Manager.ts: the snapshot failure is modeled by fabricating an Error whose message carries the structural context, instead of putting that context on TerminalSubprocessCheckError as attributes.
Posted via Macroscope — Effect Service Conventions
Replace the manufactured Error cause with structural exitCode/timedOut/ stdoutTruncated fields on TerminalSubprocessCheckError and derive the message from them, keeping cause for real spawn failures.

What Changed
The terminal subprocess poller now takes one process-table snapshot per poll tick and derives every terminal's child state from it, instead of spawning
pgrep -P <pid>plus up to two fullps -eo pid=,ppid=dumps (and aps -p <child> -o comm=) per terminal every second.ps -eo pid=,ppid=,comm=per tick, shared across all terminals.psis resolved to an absolute path (/bin/ps, then/usr/bin/ps) once at startup so the spawn no longer walksPATH.Get-CimInstancePowerShell call is likewise taken once per tick instead of once per terminal.pgrep/ps -pspawns.Why
Fixes #6332. With N terminals the poller spawned up to 3 processes per terminal per second, each by bare name — so every spawn attempted one
posix_spawnperPATHentry until the hit. On the reporter's machine (14 terminals, 46-entryPATH) that measured ~455 process creations/sec, 94% failingENOENT, wrapping the macOS PID space every ~4 minutes and eventually destabilizinglaunchservicesd.Cost is now O(1) spawns per tick regardless of terminal count, and the one remaining spawn is a single successful
posix_spawn. This also fixes a latent double-execution of the fullpstable in the pgrep-fallback path, and gives Windows the same per-terminal → per-tick reduction (one PowerShell process per second instead of one per terminal).Behavior notes: child detection, labels (
commnormalization), and the process-tree ids fed to port discovery are unchanged.pgrepis no longer used at all. ThesubprocessInspectortest seam is preserved.Windows benchmark
Measured on Windows 11 with 16 logical processors and about 378 visible processes, using the exact
Get-CimInstance Win32_Processcommand and 1.5-second timeout used by the poller. These are scan-phase measurements from this host, not universal latency guarantees.Without enforcing the timeout, 14 simultaneous scans consumed 37.6 CPU-seconds, peaked at 1.17 GB aggregate working set, and took 3.43 seconds of wall time.
The structural launch reduction is
1 - (1 / terminal count): 75% at 4 terminals, 87.5% at 8, 92.9% at 14, and 96.7% at 30.Checklist
Work done by Claude (Fable 5) in Claude Code, directed by @SunkenInTime.
Note
Medium Risk
Touches terminal activity labels and process IDs used for port discovery on a 1s poll loop; snapshot-skip semantics are safer than falsely marking terminals idle but change failure handling versus the old per-terminal path.
Overview
Fixes PID-space flooding (#6332) by replacing per-terminal subprocess inspection with one process-table snapshot per poll tick shared across all running sessions.
On POSIX, each tick runs a single
ps -eo pid=,ppid=,comm=(nopgrepor per-childps -p);psis resolved once at startup to/bin/psor/usr/bin/psto avoid PATH-walking on every spawn. On Windows, the existingGet-CimInstancePowerShell query runs once per tick instead of once per terminal. Per-terminal state is derived in-process viaderiveSubprocessInspectResultfrom the parsed parent/child map and command names.Behavior change: if the snapshot fails (non-zero exit, timeout, or truncated stdout), the whole tick is skipped with a warning—terminals are not cleared to idle and last-known subprocess labels/activity are retained.
TerminalSubprocessCheckErrorno longer includesterminalPidand reports snapshot-level failure details instead.Regression tests cover multi-terminal shared snapshot usage and snapshot-failure retention.
Reviewed by Cursor Bugbot for commit f10cf45. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix terminal subprocess polling to use one shared process snapshot per tick instead of per-terminal commands
pgrep/psor PowerShell calls each poll tick, which could exhaust the PID space under load.TerminalManagertakes a singleps/PowerShell snapshot per tick and derives per-terminal activity from the in-memory result viaderiveSubprocessInspectResult.psbinary path is resolved once at startup (checking/bin/psand/usr/bin/ps) to avoid repeated PATH lookups.Macroscope summarized f10cf45.