diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/interrupt.iml b/.idea/interrupt.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/interrupt.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..cc2f1ed --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cmd/ctrlctest.go b/cmd/ctrlctest.go new file mode 100644 index 0000000..32fc33b --- /dev/null +++ b/cmd/ctrlctest.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + + "github.com/p9c/pod/pkg/util/interrupt" +) + +func main() { + interrupt.AddHandler(func() { + fmt.Println("IT'S THE END OF THE WORLD!") + }) + <-interrupt.HandlersDone +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..713b190 --- /dev/null +++ b/log.go @@ -0,0 +1,42 @@ +package interrupt + +import ( + "github.com/p9c/pod/pkg/logg" +) + +var subsystem = logg.AddLoggerSubsystem() +var F, E, W, I, D, T logg.LevelPrinter = logg.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7f01ad0 --- /dev/null +++ b/main.go @@ -0,0 +1,187 @@ +package interrupt + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "syscall" + + uberatomic "go.uber.org/atomic" + + "github.com/p9c/pod/pkg/util/qu" + + "github.com/kardianos/osext" +) + +type HandlerWithSource struct { + Source string + Fn func() +} + +var ( + Restart bool // = true + requested uberatomic.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) +) + +var interruptCallbacks []func() +var interruptCallbackSources []string + +// Listener listens for interrupt signals, registers interrupt callbacks, +// and responds to custom shutdown signals as required +func Listener() { + invokeCallbacks := func() { + D.Ln( + "running interrupt callbacks", + len(interruptCallbacks), + strings.Repeat(" ", 48), + interruptCallbackSources, + ) + // run handlers in LIFO order. + for i := range interruptCallbacks { + idx := len(interruptCallbacks) - 1 - i + D.Ln("running callback", idx, interruptCallbackSources[idx]) + interruptCallbacks[idx]() + } + D.Ln("interrupt handlers finished") + HandlersDone.Q() + if Restart { + var file string + var e error + file, e = osext.Executable() + if e != nil { + E.Ln(e) + return + } + D.Ln("restarting") + if runtime.GOOS != "windows" { + e = syscall.Exec(file, os.Args, os.Environ()) + if e != nil { + F.Ln(e) + } + } else { + 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:]...) + D.Ln("windows restart done") + if e = cmd.Start(); E.Chk(e) { + } + // // select{} + // os.Exit(0) + } + } + // time.Sleep(time.Second * 3) + // os.Exit(1) + // close(HandlersDone) + } +out: + for { + select { + case sig := <-ch: + // if !requested { + // L.Printf("\r>>> received signal (%s)\n", sig) + D.Ln("received interrupt signal", sig) + requested.Store(true) + invokeCallbacks() + // pprof.Lookup("goroutine").WriteTo(os.Stderr, 2) + // } + break out + case <-ShutdownRequestChan.Wait(): + // if !requested { + W.Ln("received shutdown request - shutting down...") + requested.Store(true) + invokeCallbacks() + break out + // } + case handler := <-addHandlerChan: + // if !requested { + // D.Ln("adding handler") + 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) + D.Ln("handler added by:", msg) + 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) + D.Ln("interrupt requested", f, l, requested.Load()) + if requested.Load() { + D.Ln("requested again") + return + } + requested.Store(true) + ShutdownRequestChan.Q() + // qu.PrintChanState() + var ok bool + select { + case _, ok = <-ShutdownRequestChan: + default: + } + D.Ln("shutdownrequestchan", ok) + 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() { + Restart = true + D.Ln("requesting restart") + Request() +} + +// Requested returns true if an interrupt has been requested +func Requested() bool { + return requested.Load() +} diff --git a/sigterm.go b/sigterm.go new file mode 100644 index 0000000..4e547cc --- /dev/null +++ b/sigterm.go @@ -0,0 +1,13 @@ +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package interrupt + +import ( + "os" + "syscall" +) + +func init() { + + signals = []os.Signal{os.Interrupt, syscall.SIGTERM} +}