Skip to content

fix(proxy): reap mitmproxy children on stop, quit, exit and startup - #777

Merged
Juliusolsson05 merged 4 commits into
mainfrom
fix/mitmproxy-reaping
Sep 5, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
fix/mitmproxy-reaping

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

Problem

Every Claude proxy-streaming session spawns its own mitmdump undetached (claudeSession.ts → createProxyServer in the claude-code-headless submodule). Any death of the Electron main that skips will-quit — Force Quit/SIGKILL, native crash, process.exit(1) from the crash hooks, app.exit(1) — reparents the child to launchd and it keeps running (mitmdump only writes on traffic; an orphan gets none). The 2026-09-03 audit of a two-day run found 20 orphaned mitmdumps (ppid 1, two days old) next to 12 live ones — each a Python process holding ~60–100 MB RSS and a bound port on a 16 GB laptop that already swaps (3.7 GB of proxy state on disk).

Second leak, same launcher: ProxyServer.stop() awaits child.once('exit') after SIGTERM/SIGKILL and never resolves if the child already exited on its own — one dead proxy hangs claudeSession.stop() → killAll() → the will-quit gate, and the user's only exit is Force Quit, which orphans every other session's proxy.

Refs #767 (expected item 5, process-hygiene half). Plan: docs/superpowers/plans/2026-09-03-mitmproxy-reaping.md.

Implemented behavior

  • Ownership marker: our mitmdumps are identified by the argv token we control (--set confdir=<STATE_DIR>/proxy/_shared-conf), now exported as MITMPROXY_SHARED_CONF_DIR and passed by claudeSession.ts, so spawn and search cannot drift. A user's own Homebrew mitmproxy never matches.
  • Ownership policy: kernel ppid — ppid === process.pid ours-alive; ppid <= 1 or dead owner → stale, kill; live foreign owner → keep and journal (conservative side of perf(diagnostics): default-off and off-thread the always-on diagnostic subsystems #767(d): only act when the owner is provably gone; sound because the state process lock guarantees one main per state dir).
  • Grace termination: SIGTERM → poll → SIGKILL → poll, injectable signals/clock, ESRCH = success.
  • Four layers: (1) per-session teardown raced against a 6 s deadline, escalating by marker + --listen-port + ppid-self so a wedged/dead child can never hang quit; (2) will-quit sweep after killAll() (composed in index.ts, gate stays proxy-agnostic); (3) synchronous SIGKILL on process.on('exit'); (4) startup reaper for the previous run's leftovers, journaled as proxy.mitmdump.stale_reaped with scanned/stale/reaped/survived/kept counts, fire-and-forget so boot is never delayed.
  • isPidRunning exported from processLock.ts so the reaper and the state lock share one liveness answer (same EPERM semantics).

Design decisions and tradeoffs

  • No packages/ changes in this PR: the parent-side fix (detached: true + kill(-pgid)) and an addon-side os.getppid() watchdog are submodule follow-ups; everything here is compatible with both landing later.
  • Windows: no bundled mitmdump, scanner returns nothing.
  • Killing is parallel (each terminate mostly sleeps in its grace window; twenty orphans serially would be 40 s at quit).

Links

Refs #767. Refs #495 (A9 rollback), #119.

Tests and verification

  • mitmproxyReaper.test.ts 23/23: stale-filter matrix (ppid 1 / dead owner kill; live-foreign-owner, ppid-self, unmarked, marker-prefix keep); ps row parsing with spaces; grace sequence under fake timers (TERM-only, TERM→KILL boundary, already-gone sends nothing); stopProxyServerWithDeadline (resolves clean; escalates by port; not-found when only a previous run's port match exists); startup reaper end-to-end with injected process list and journaled counts.
  • processLock + claude/runtime suites 44/44; npm run typecheck clean.
  • Manual Force Quit + relaunch check from the plan left to the owner (needs the real app + proxy sessions).

Known limitations / follow-ups

Signal-safety review repair (2026-09-04)

The marker/PPID scan now only discovers candidates. Immediately before TERM, delayed KILL and synchronous exit signalling, revalidate kernel command, start timestamp, parent and unchanged argv. Changed/unreadable/unknown identity is retained and is not counted as reaped. Bound process inspection and exit-loop work. Normal native mitmdump and Python launchers are supported; unknown interpreter layouts fail closed.

33 focused tests pass, including identity replacement during the grace period, unrelated executables and synchronous PID reuse; TypeScript and both full host CI gates pass on final head f62fd22 (run 33938037840). POSIX ps checks are not atomic pidfds: second-resolution timestamps and a small check-to-signal race remain. No real live process was signalled and force-quit qualification is still unperformed. No claim of currently finding the historical 20 orphans.

Juliusolsson05 and others added 4 commits September 3, 2026 17:03
Record why per-session mitmdump children outlive the app (undetached
spawn, no parent-death guard, a stop() that hangs when the child already
died, no startup sweep) and the src-side design: an argv ownership marker
we already control, ppid-based ownership, bounded teardown, a will-quit
sweep, and a startup reaper that journals its counts.

Refs #767

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
Every Claude proxy-streaming session spawns its own mitmdump undetached,
and any death of the Electron main that skips will-quit (Force Quit,
native crash, the crash hooks' process.exit(1)) reparents the child to
launchd: the 2026-09-03 audit found 20 orphaned mitmdumps (ppid 1, two
days old) next to 12 live ones, each holding ~60-100 MB RSS and a port
on a swapping 16 GB machine. ProxyServer.stop() also never resolves when
the child already died, hanging quit and forcing the Force Quit that
orphans the rest.

Host-side process hygiene, no submodule changes: identify our children
by the confdir argv token we control, decide ownership by kernel ppid
(conservative: only kill when the owner is provably dead), terminate
with a TERM/KILL grace sequence, bound each session teardown with a
stop deadline, sweep owned children after killAll, SIGKILL synchronously
on process exit, and reap stale processes from previous runs at startup
with journaled counts.

Refs #767 (item 5, process-hygiene half)
Integrate current main. Check kernel command, creation timestamp, parent and unchanged argv before TERM and delayed KILL; fail closed on changed or unreadable identity. Bound inspection and synchronous exit work. Retain the documented non-atomic POSIX check-to-signal limitation.

Refs #767
@Juliusolsson05
Juliusolsson05 merged commit 7fa4a7b into main Sep 5, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/mitmproxy-reaping branch September 5, 2026 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant