Repository navigation
fix(macos): couple forge3 lifecycle to the app via a death-pipe guardian - #46
Merged
Merged
Conversation
ssddOnTop
force-pushed
the
feat/forge3-guardian
branch
from
August 5, 2026 07:22
9f704ee to
66cf25c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 onswift run, logout, or a Sparkle relaunch skip it entirely and leak an orphaned forge3 still listening on its loopback port. macOS has noPR_SET_PDEATHSIG, so the parent's death is otherwise unobservable by the child.Fix: guardian process with a death pipe
Asymmetric lifecycle coupling:
Mechanics:
ForgeMenuBarexecutable re-exec'ed with an internal--forge3-guardianargv marker, dispatched inAppDelegatebefore any AppKit initialization. No new target, nothing extra to ship or sign.ForgeProcessHostnow 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.FORGE_/FORGE3_/RUST_LOGallowlist — all debug vars still reach forge3), then waits on two events:ForgeProcessHostwatches the guardian's pid with the sameDispatchSourceProcess/reap logic (the guardian's exit status is forge3's), soServiceSupervisorrestart/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 buildclean; 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).kill -9of 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.