Conversation
…every IPC connection
Orphan leak: the app-store supervisor starts the wallet in its own process
group. macOS has no parent-death signal, so whenever the daemon exits
without running its shutdown path (watchdog os.Exit, SIGKILL, crash,
launchd restart) the wallet is reparented to launchd and keeps serving its
socket with the sqlite ledger and cap-state log open. Measured with the
apps-sweep harness (mini-supervisor SIGKILLed, app watched 6 s):
before: ORPHAN, still answering IPC, on darwin/arm64, darwin/amd64
(Rosetta), and linux/amd64 + linux/arm64 without Pdeathsig
after: SELF-EXIT in 915-1025 ms on all four, socket unlinked,
"graceful shutdown complete" logged
watchParent polls getppid once a second and cancels the run context when
the parent changes, so shutdown goes through the normal SIGTERM path.
A wallet started by init/launchd directly (ppid 1) is not watched;
--exit-with-parent=false opts out for standalone use.
Log growth: every IPC call wrote "conn open" + "conn closed" (96.5 bytes
per call, 289,509 bytes over 3,000 calls) to stderr, which the supervisor
wires straight into the daemon's log. Now behind --log-conns (default
off); serve errors are still logged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Go's UnixListener.Close unlinks the socket path by name. When a wallet
shuts down after a replacement has already taken over the same path, that
Close deletes the replacement's socket and leaves the live wallet running
but unreachable. This happens when:
- the wallet exits because its parent died (previous commit) and the
respawned daemon has already started a new wallet, which removed the
stale socket file and bound a fresh one. Daemons before app-store
v1.0.3 have no reapStale to stop the old copy first;
- a SIGTERM reaches an old instance after its successor is up.
The listener now records the socket file it bound, and Close keeps the
path when it no longer refers to that socket (SetUnlinkOnClose(false)).
Normal shutdown still removes the wallet's own socket.
TestOwnedListenerCloseKeepsReplacementSocket fails without the check
("replacement's socket was removed by the old instance's Close") and
passes with it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TestWalletExitsWhenParentDies runs the real main() under a stand-in daemon (own process group, no Pdeathsig, as the supervisor sets it up on macOS), SIGKILLs the stand-in, and requires the wallet to exit on its own, log "graceful shutdown complete" and remove its socket. With --exit-with-parent defaulting to false it fails: "wallet pid N still running 10.028s after its parent was killed (orphaned)". With the fix the wallet exits within about 2 s (macOS, linux/arm64, linux/amd64). TestServeLogsConnectionsOnlyWhenAsked checks that serve writes no per-connection lines by default and one open/close pair per call with --log-conns. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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
An app-lifecycle sweep of
io.pilot.wallet(the catalogue's 0.3.3 bundle, plus source builds of 0.3.3 andmain, on darwin/arm64, darwin/amd64 under Rosetta, linux/amd64 and linux/arm64) found three leaks. In the sweep, the app's parent was SIGKILLed and the app was watched for 6 s.cap-state.jsonlopen. The next daemon then starts a second wallet next to it.ORPHAN: app still alive 6s after parent SIGKILL … serving_after_parent_death=trueon all four platforms when Pdeathsig is off.psonly, not touched), an orphaned wallet with ppid 1 was running beside the daemon's current wallet. Both had byte-identical argv: the same--db,--socketand--cap-state.conn open from=+conn closedto stderr, which the supervisor wires into the daemon log: 96.5 bytes per call, 289,417 bytes over 3,000 calls.UnixListener.Closeunlinks the socket path by name. If an old wallet shuts down after a replacement has already bound the same path, the old one deletes the new one's socket, and the live wallet keeps running but can't be reached. This can happen when the respawned daemon starts a new wallet before the orphan notices its parent died: daemons before app-store v1.0.3 have noreapStale. It can also happen when a SIGTERM reaches an old instance late.Fix
watchParent(cmd/wallet/parentwatch.go):getppidonce a second.--exit-with-parent=falseturns it off.--log-conns, off by default. Serve errors are still logged.ownSocket(cmd/wallet/socketguard.go): the listener remembers the socket file it bound. OnCloseit leaves the path alone (SetUnlinkOnClose(false)) once the path refers to a different socket.Tests
TestWalletExitsWhenParentDiesruns the realmain()under a stand-in daemon that uses its own process group and no Pdeathsig, as the supervisor does on macOS. The test SIGKILLs the stand-in, then requires the wallet to exit on its own, loggraceful shutdown complete, and remove its socket.--exit-with-parentdefaulting to false, it fails:wallet pid 8981 still running 10.028s after its parent was killed (orphaned).--init.TestOwnedListenerCloseKeepsReplacementSocketfails without the ownership check (replacement's socket was removed by the old instance's Close) and passes with it. Two more tests cover normal unlink and an externally removed path.TestServeLogsConnectionsOnlyWhenAsked: with the default, 25 calls give 0 per-connection lines. With--log-conns, they give 50.go vet ./...andgo test -race ./...pass on macOS arm64 and linux/arm64. On linux/amd64 the new tests pass.TestRunSmokeon linux/amd64 emulated through Rosetta, under full-suite load. Its 3 s socket deadline is shorter than the observed 4.3 s startup.origin/mainfails the same way (run 2 of 2:--- FAIL: TestRunSmoke (5.70s) … did not become reachable). This PR doesn't touch that path.Harness results (built from this branch)
Signed with a throwaway key; N=200
wallet.evm.chainscalls plus the SIGTERM, SIGKILL-stop and orphan phases.Not in this PR
wallet.hookPreSendMessage/wallet.hookPostRecvMessageare declared inmanifest.json(extends+exposes) but never registered, so IPC returnsmethod not found. Nothing calls them today: neither web4 nor the supervisor wirespkg/extend.affiliatesentry still has the placeholder keyed25519:BBBB….walletbinary.🤖 Generated with Claude Code