The kit handles no signals
(decision 21):
forbidigo refuses signal.Notify and its relatives in every package. The
program’s main owns them. Four programs carry the same twenty lines; this
is the shape.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
err := run(ctx)
stop() // a second Ctrl-C now ends the process the usual way
os.Exit(exitCode(err))
}
Rules#
- One context.
rungetsctxand hands it down. Every pool, every child process and every request is made under it, so one interrupt ends them all, andfanoutreports the items it did not start as skipped. - Interrupt once. The first signal cancels
ctx. A program that starts child commands sends themSIGINTonce, starts none during the shutdown, and usescmd.WaitDelayso that a child that ignores it is killed rather than waited for. - The second signal is the default.
stop()afterrunreturns, or a secondNotifyContextthat is released at the first signal, gives back the default action so that a person can end a stuck shutdown. - Exit code in the program.
exitCodemaps the error to the program’s numbers.cobratree.IsUsagemarks a usage error, and the program’sClassifyhook givesprogressthe skips and failures (decision 21, decision 24). An interrupt exits with the number the program documents,130by convention. - Deferred work runs.
os.Exitskips deferred calls, sorunreturns first and closes the display and the event log, ascliprogress.Sessiondoes, and only then doesmainexit. - A server shuts down gracefully. A program with a local web server
calls
Shutdownwith a short deadline whenctxis done, then closes the SSE streams, then removes its runtime files.