Split out of #112 (secondary finding 3), which was closed by #113 — that PR only addressed the restart storm. Reported by @laulpogan.
Problem
internal/app/app.go:198 uses signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) and the process then returns from Main normally. A SIGTERM kill is therefore indistinguishable from a voluntary shutdown in every signal available to an operator:
- launchd reports
last exit code = 0 — identical to "user stopped it".
- A
wait-based supervisor sees status 0 and logs a voluntary exit.
Why it matters
This is what made #112 take hours to diagnose. The restart storm was hundreds of SIGTERM kills from launchctl kickstart -k, and every external signal said "clean exit", sending the reporter through launchd environment, stdin EOF, ThrottleInterval, plist churn, and bind conflicts before the actual cause. The storm itself is fixed, but the diagnostic gap will cost the next person the same time for any other kill source.
Options
- Log the shutdown cause:
shutting down: received SIGTERM.
- And/or exit
143 (128+15) so last exit code carries the information. Note SIGTERM from systemctl stop / launchctl stop then becomes a documented, expected non-zero — worth a docs note if that tradeoff is unwanted, in which case the supervisor-visible cause string alone is the alternative.
For contrast, the state-file lock path already gets this right: internal/app/state_file.go plus state_file_unix.go's non-blocking flock exit 1 with an accurate, actionable message. The shutdown path is missing that same distinguishability.
Verified against current main (1040251).
Split out of #112 (secondary finding 3), which was closed by #113 — that PR only addressed the restart storm. Reported by @laulpogan.
Problem
internal/app/app.go:198usessignal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)and the process then returns fromMainnormally. ASIGTERMkill is therefore indistinguishable from a voluntary shutdown in every signal available to an operator:last exit code = 0— identical to "user stopped it".wait-based supervisor sees status0and logs a voluntary exit.Why it matters
This is what made #112 take hours to diagnose. The restart storm was hundreds of
SIGTERMkills fromlaunchctl kickstart -k, and every external signal said "clean exit", sending the reporter through launchd environment,stdinEOF,ThrottleInterval, plist churn, and bind conflicts before the actual cause. The storm itself is fixed, but the diagnostic gap will cost the next person the same time for any other kill source.Options
shutting down: received SIGTERM.143(128+15) solast exit codecarries the information. NoteSIGTERMfromsystemctl stop/launchctl stopthen becomes a documented, expected non-zero — worth a docs note if that tradeoff is unwanted, in which case the supervisor-visible cause string alone is the alternative.For contrast, the state-file lock path already gets this right:
internal/app/state_file.goplusstate_file_unix.go's non-blockingflockexit1with an accurate, actionable message. The shutdown path is missing that same distinguishability.Verified against current
main(1040251).