// Package interrupt is a library for providing handling for Ctrl-C/Interrupt // handling and triggering callbacks for such things as closing files, flushing // buffers, and other elements of graceful shutdowns. package interrupt import ( "fmt" "os" "os/signal" "runtime" "lol.mleku.dev/log" "next.orly.dev/pkg/utils/atomic" "next.orly.dev/pkg/utils/qu" ) // HandlerWithSource is an interrupt handling closure and the source location // that it was sent from. type HandlerWithSource struct { Source string Fn func() } var ( // RestartRequested is set true after restart is requested. RestartRequested bool // = true requested atomic.Bool // ch is used to receive SIGINT (Ctrl+C) signals. ch chan os.Signal // signals is the list of signals that cause the interrupt signals = []os.Signal{os.Interrupt} // ShutdownRequestChan is a channel that can receive shutdown requests ShutdownRequestChan = qu.T() // addHandlerChan is used to add an interrupt handler to the list of // handlers to be invoked on SIGINT (Ctrl+C) signals. addHandlerChan = make(chan HandlerWithSource) // HandlersDone is closed after all interrupt handlers run the first time an // interrupt is signaled. HandlersDone = make(qu.C) interruptCallbacks []func() interruptCallbackSources []string ) // Listener listens for interrupt signals, registers interrupt callbacks, and // responds to custom shutdown signals as required func Listener() { invokeCallbacks := func() { // run handlers in LIFO order. for i := range interruptCallbacks { idx := len(interruptCallbacks) - 1 - i log.T.F( "running callback %d from %s", idx, interruptCallbackSources[idx], ) interruptCallbacks[idx]() } log.D.Ln("interrupt handlers finished") HandlersDone.Q() if RestartRequested { Restart() } else { os.Exit(0) } } out: for { select { case _ = <-ch: fmt.Fprintf(os.Stderr, "\r") requested.Store(true) invokeCallbacks() break out case <-ShutdownRequestChan.Wait(): log.W.Ln("received shutdown request - shutting down...") requested.Store(true) invokeCallbacks() break out case handler := <-addHandlerChan: interruptCallbacks = append(interruptCallbacks, handler.Fn) interruptCallbackSources = append( interruptCallbackSources, handler.Source, ) case <-HandlersDone.Wait(): break out } } } // AddHandler adds a handler to call when a SIGINT (Ctrl+C) is received. func AddHandler(handler func()) { // Create the channel and start the main interrupt handler which invokes all // other callbacks and exits if not already done. _, loc, line, _ := runtime.Caller(1) msg := fmt.Sprintf("%s:%d", loc, line) if ch == nil { ch = make(chan os.Signal) signal.Notify(ch, signals...) go Listener() } addHandlerChan <- HandlerWithSource{ msg, handler, } } // Request programmatically requests a shutdown func Request() { _, f, l, _ := runtime.Caller(1) log.D.Ln("interrupt requested", f, l, requested.Load()) if requested.Load() { log.D.Ln("requested again") return } requested.Store(true) ShutdownRequestChan.Q() var ok bool select { case _, ok = <-ShutdownRequestChan: default: } if ok { close(ShutdownRequestChan) } } // GoroutineDump returns a string with the current goroutine dump in order to // show what's going on in case of timeout. func GoroutineDump() string { buf := make([]byte, 1<<18) n := runtime.Stack(buf, true) return string(buf[:n]) } // RequestRestart sets the reset flag and requests a restart func RequestRestart() { RestartRequested = true log.D.Ln("requesting restart") Request() } // Requested returns true if an interrupt has been requested func Requested() bool { return requested.Load() }