You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
3.7 KiB
153 lines
3.7 KiB
// 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() |
|
}
|
|
|