github.com/puellanivis/breton@v0.2.16/lib/os/process/context.go (about) 1 package process 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "os/signal" 9 "runtime/debug" 10 "sync" 11 "syscall" 12 13 "github.com/puellanivis/breton/lib/sync/edge" 14 ) 15 16 // signaler provides a basic api for channels to support optional signal handling, like for SIGHUP. 17 type signaler struct { 18 sync.Once 19 ch chan struct{} 20 } 21 22 // get returns a receive-only copy of the underlying channel for the signaler. 23 // If the channel does not exist, it will be allocated. 24 func (h *signaler) get() <-chan struct{} { 25 h.Do(func() { 26 h.ch = make(chan struct{}) 27 }) 28 29 return h.ch 30 } 31 32 // trySend does the job of trying to send the optional notification to the underlying channel. 33 // It will return true if it successfully sends a notification. 34 func (h *signaler) trySend() bool { 35 select { 36 case h.ch <- struct{}{}: // A send on a `nil` channel always blocks. 37 return true 38 default: 39 } 40 41 return false 42 } 43 44 var sigHandler struct { 45 sync.Once 46 ctx context.Context 47 ch chan os.Signal 48 e edge.Edge 49 50 hup signaler 51 } 52 53 // HangupChannel provides a receiver channel that will notify the caller of when a SIGHUP signal has been received. 54 // This signal is often used by daemons to be notified of a request to reload configuration data. 55 // 56 // If the signal handler would block trzing to send this notification, 57 // then it will treat the signal the same as any other terminating signal. 58 func HangupChannel() <-chan struct{} { 59 return sigHandler.hup.get() 60 } 61 62 // signalHandler starts a long-lived goroutine, which will run in the background until the process ends. 63 // It returns a `context.Context` that will be canceled in the event of a signal being received. 64 // It will then continue to listen for more signals, 65 // If it receives three signals, then we presume that we are not shutting down properly, and panic with all stacktraces. 66 func signalHandler(parent context.Context) context.Context { 67 ctx, cancel := context.WithCancel(parent) 68 69 go func() { 70 killChan := make(chan struct{}, 3) 71 72 signal.Notify(sigHandler.ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) 73 for sig := range sigHandler.ch { 74 fmt.Fprintln(os.Stderr, "received signal:", sig) 75 76 switch sig { 77 case syscall.SIGQUIT: 78 debug.SetTraceback("all") 79 panic("SIGQUIT") 80 81 case syscall.SIGHUP: 82 if sigHandler.hup.trySend() { 83 continue 84 } 85 } 86 87 // Prospectively transition the edge, so we can maybe avoid injecting an unnecessary SIGTERM. 88 sigHandler.e.Up() 89 90 cancel() 91 92 select { 93 case killChan <- struct{}{}: // killChan is not full, keep handling signals. 94 default: 95 // We have gotten three signals and we are still kicking, 96 // panic and dump all stack traces. 97 debug.SetTraceback("all") 98 panic("not responding to signals") 99 } 100 } 101 102 debug.SetTraceback("all") 103 panic("signal handler channel unexpectedly closed") 104 }() 105 106 return ctx 107 } 108 109 // Context returns a process-level `context.Context` 110 // that is cancelled when the program receives a termination signal. 111 // 112 // A process should start a graceful shutdown process once this context is cancelled. 113 // 114 // This context should not be used as a parent to any requests, 115 // otherwise those requests will also be cancelled 116 // instead of being allowed to complete their work. 117 func Context() context.Context { 118 sigHandler.Do(func() { 119 sigHandler.ch = make(chan os.Signal, 1) 120 sigHandler.ctx = signalHandler(context.Background()) 121 }) 122 123 return sigHandler.ctx 124 } 125 126 // Shutdown starts any graceful shutdown processes waiting for `process.Context()` to be cancelled. 127 // 128 // Shutdown works by injecting a `syscall.SIGTERM` directly to the signal handler, 129 // which will cancel the `process.Context()` the same as a real SIGTERM. 130 // 131 // Shutdown returns an error only if it is unable to send the signal. 132 // Notably, it is not an error to call Shutdown if we have already triggered a graceful shutdown. 133 // 134 // Shutdown does not wait for anything to finish before returning. 135 func Shutdown() error { 136 if !sigHandler.e.Up() { 137 // We have already triggered a graceful shutdown, so nothing to do. 138 return nil 139 } 140 141 select { 142 case sigHandler.ch <- syscall.SIGTERM: 143 return nil 144 default: 145 } 146 147 return errors.New("could not send signal") 148 } 149 150 // Quit ends the program as soon as possible, dumping a stacktrace of all goroutines. 151 // 152 // Quit works by injecting a `syscall.SIGQUIT` directly to the signal handler, 153 // which will cause a panic, and stacktrace of all goroutines the same as a real SIGQUIT. 154 // 155 // If Quit cannot inject the signal, 156 // it will setup an unrecoverable panic to occur. 157 // 158 // In all cases, Quit will not return. 159 func Quit() { 160 select { 161 case sigHandler.ch <- syscall.SIGQUIT: 162 default: 163 // We start up a separate goroutine for this to ensure that no `recover()` can block this panic. 164 go func() { 165 debug.SetTraceback("all") 166 panic("process was force quit") 167 }() 168 } 169 170 select {} // Block forever so we never return. 171 }