implemented event and req

This commit is contained in:
2025-09-02 20:32:53 +01:00
parent 76b251dea9
commit 51f04f5f60
104 changed files with 6368 additions and 125 deletions

View File

@@ -0,0 +1,2 @@
# interrupt
Handle shutdowns cleanly and enable hot reload

153
pkg/utils/interrupt/main.go Normal file
View File

@@ -0,0 +1,153 @@
// 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"
"utils.orly/atomic"
"utils.orly/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()
}

View File

@@ -0,0 +1,26 @@
//go:build linux
package interrupt
import (
"lol.mleku.dev/log"
"os"
"syscall"
"github.com/kardianos/osext"
)
// Restart uses syscall.Exec to restart the process. macOS and Windows are not
// implemented, currently.
func Restart() {
log.D.Ln("restarting")
file, e := osext.Executable()
if e != nil {
log.E.Ln(e)
return
}
e = syscall.Exec(file, os.Args, os.Environ())
if e != nil {
log.F.Ln(e)
}
}

View File

@@ -0,0 +1,20 @@
package interrupt
func Restart() {
// TODO: test this thing actually works!
// log.D.Ln("doing windows restart")
// // procAttr := new(os.ProcAttr)
// // procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr}
// // os.StartProcess(os.Args[0], os.Args[1:], procAttr)
// var s []string
// // s = []string{"cmd.exe", "/C", "start"}
// s = append(s, os.Args[0])
// // s = append(s, "--delaystart")
// s = append(s, os.Args[1:]...)
// cmd := exec.Command(s[0], s[1:]...)
// log.D.Ln("windows restart done")
// if err := cmd.Start(); log.Fail(err) {
// }
// // select{}
// os.Exit(0)
}

View File

@@ -0,0 +1,20 @@
package interrupt
func Restart() {
// TODO: test this thing actually works!
// log.D.Ln("doing windows restart")
// // procAttr := new(os.ProcAttr)
// // procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr}
// // os.StartProcess(os.Args[0], os.Args[1:], procAttr)
// var s []string
// // s = []string{"cmd.exe", "/C", "start"}
// s = append(s, os.Args[0])
// // s = append(s, "--delaystart")
// s = append(s, os.Args[1:]...)
// cmd := exec.Command(s[0], s[1:]...)
// log.D.Ln("windows restart done")
// if err := cmd.Start(); log.Fail(err) {
// }
// // select{}
// os.Exit(0)
}

View File

@@ -0,0 +1,12 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package interrupt
import (
"os"
"syscall"
)
func init() {
signals = []os.Signal{os.Interrupt, syscall.SIGTERM}
}