Skip to content

fix(macos): couple forge3 lifecycle to the app via a death-pipe guardian - #46

Merged
ssddOnTop merged 2 commits into
mainfrom
feat/forge3-guardian
Aug 5, 2026
Merged

ssddOnTop merged 2 commits into
mainfrom
feat/forge3-guardian

Conversation

@ssddOnTop

Copy link
Copy Markdown
Collaborator

Stacked on #45 (feat/thin-installer-v2). Merge that first; this diff shows only the guardian commits.

Problem

forge3 is spawned into its own process group (needed for clean tree kills), which detaches it from the app's lifetime. The graceful-quit path (applicationShouldTerminate → SIGTERM/SIGKILL) only runs on a polite quit — Force Quit, kill -9, a crash, Ctrl-C on swift run, logout, or a Sparkle relaunch skip it entirely and leak an orphaned forge3 still listening on its loopback port. macOS has no PR_SET_PDEATHSIG, so the parent's death is otherwise unobservable by the child.

Fix: guardian process with a death pipe

Asymmetric lifecycle coupling:

  • Any app death → forge3 dies. The app spawns a guardian; the kernel closes the app's pipe end unconditionally on process death — including SIGKILL — so the guardian always observes EOF and sweeps the forge3 process group.
  • forge3 death ≠ app death. The guardian exits carrying forge3's status; the app's supervisor restarts as before.

Mechanics:

  • The guardian is the same ForgeMenuBar executable re-exec'ed with an internal --forge3-guardian argv marker, dispatched in AppDelegate before any AppKit initialization. No new target, nothing extra to ship or sign.
  • One guardian per forge3 run. ForgeProcessHost now spawns the guardian (same stdio wiring: stdin /dev/null, stdout/stderr onto the rotating-log pipe), creates a death pipe, holds the write end for the app's lifetime, and passes the read end to the guardian as fd 3.
  • The guardian spawns forge3 into its own process group with the already-sanitized environment passed through unchanged (FORGE_/FORGE3_/RUST_LOG allowlist — all debug vars still reach forge3), then waits on two events:
    • forge3 exit → SIGKILL the group (sweeps grandchildren while the unreaped leader pins the pgid, no PID-reuse race), reap, and exit with forge3's mapped status — normal exit propagates verbatim (so the supervisor's status-75 immediate-restart path is preserved), signal death maps to 128+sig.
    • death-pipe EOF (app died, or app closed it for an intentional stop) → SIGTERM then immediate SIGKILL of the forge3 group, exit. Escalation is immediate by design: production logs show forge3 never honors SIGTERM (652/652 stops required SIGKILL), and delaying inside the guardian could race the host's own fallback escalation.
  • App-side model unchanged: ForgeProcessHost watches the guardian's pid with the same DispatchSourceProcess/reap logic (the guardian's exit status is forge3's), so ServiceSupervisor restart/backoff needed zero changes. The prior SIGTERM→SIGKILL escalation is retained as a belt-and-braces fallback against a hung guardian, which ignores SIGTERM while forge3's SIGTERM disposition is restored to default at spawn.

Testing

  • swift build clean; integration tests compile (suite run pending a toolchain repair on the dev machine — a half-applied macOS update currently breaks the xctest runner; CI should run them).
  • New integration coverage: guardian launch + stdout/env forwarding to the log, exit-75 propagation, signal→128+sig mapping, abrupt death-pipe EOF orphan cleanup, prompt intentional stop.
  • Live out-of-process smoke tests against the real built binary: exit-status propagation 0/75/137 verified; kill -9 of the pipe-holder process kills the fake forge3 and its grandchild within seconds; no stray processes after.

Unchanged

UI files, Sparkle integration, forgecode-sdk — untouched. No visible behavior changes.

Base automatically changed from feat/thin-installer-v2 to main August 5, 2026 07:12
@ssddOnTop
ssddOnTop force-pushed the feat/forge3-guardian branch from 9f704ee to 66cf25c Compare August 5, 2026 07:22
@ssddOnTop
ssddOnTop merged commit 90e12e0 into main Aug 5, 2026
7 checks passed
@ssddOnTop
ssddOnTop deleted the feat/forge3-guardian branch August 5, 2026 07:34
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