├── go.mod ├── cmd ├── example-panic │ └── main.go ├── example │ └── main.go ├── example-fatalln │ └── main.go └── example-error │ └── main.go ├── LICENSE ├── .circleci └── config.yml ├── README.md ├── stack.go └── closer.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xlab/closer 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /cmd/example-panic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/xlab/closer" 9 | ) 10 | 11 | func init() { 12 | log.SetFlags(log.Lshortfile | log.LstdFlags) 13 | } 14 | 15 | func main() { 16 | closer.Bind(cleanup) 17 | closer.Checked(run, true) 18 | } 19 | 20 | func run() error { 21 | fmt.Println("Will panic in 10 seconds...") 22 | time.Sleep(10 * time.Second) 23 | panic("KAWABANGA!") 24 | return nil 25 | } 26 | 27 | func cleanup() { 28 | fmt.Print("Hang on! I'm closing some DBs, wiping some trails...") 29 | time.Sleep(3 * time.Second) 30 | fmt.Println(" Done.") 31 | } 32 | -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/xlab/closer" 9 | ) 10 | 11 | func init() { 12 | log.SetFlags(log.Lshortfile | log.LstdFlags) 13 | } 14 | 15 | func main() { 16 | closer.Bind(cleanupFunc) 17 | 18 | go func() { 19 | // do some pseudo background work 20 | fmt.Println("10 seconds to go...") 21 | time.Sleep(10 * time.Second) 22 | closer.Close() 23 | }() 24 | 25 | closer.Hold() 26 | } 27 | 28 | func cleanupFunc() { 29 | fmt.Print("Hang on! I'm closing some DBs, wiping some trails..") 30 | time.Sleep(3 * time.Second) 31 | fmt.Println(" Done.") 32 | } 33 | -------------------------------------------------------------------------------- /cmd/example-fatalln/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/xlab/closer" 9 | ) 10 | 11 | func init() { 12 | log.SetFlags(log.Lshortfile | log.LstdFlags) 13 | } 14 | 15 | func main() { 16 | closer.Bind(cleanup) 17 | closer.Checked(run, true) 18 | } 19 | 20 | func run() error { 21 | fmt.Println("Will fatal in 10 seconds...") 22 | time.Sleep(10 * time.Second) 23 | closer.Fatalln("KAWABANGA!") 24 | return nil 25 | } 26 | 27 | func cleanup() { 28 | fmt.Print("Hang on! I'm closing some DBs, wiping some trails...") 29 | time.Sleep(3 * time.Second) 30 | fmt.Println(" Done.") 31 | } 32 | -------------------------------------------------------------------------------- /cmd/example-error/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/xlab/closer" 10 | ) 11 | 12 | func init() { 13 | log.SetFlags(log.Lshortfile | log.LstdFlags) 14 | } 15 | 16 | func main() { 17 | closer.Bind(cleanup) 18 | closer.Checked(run, true) 19 | } 20 | 21 | func run() error { 22 | fmt.Println("Will throw an error in 10 seconds...") 23 | time.Sleep(10 * time.Second) 24 | return errors.New("KAWABANGA!") 25 | } 26 | 27 | func cleanup() { 28 | fmt.Print("Hang on! I'm closing some DBs, wiping some trails...") 29 | time.Sleep(3 * time.Second) 30 | fmt.Println(" Done.") 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2015-2019 Maxim Kupriianov 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | # Define a job to be invoked later in a workflow. 6 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 7 | jobs: 8 | build: 9 | working_directory: ~/repo 10 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 11 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 12 | docker: 13 | - image: circleci/golang:1.15.8 14 | # Add steps to the job 15 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | keys: 20 | - go-mod-v4-{{ checksum "go.sum" }} 21 | - run: 22 | name: Install Dependencies 23 | command: go mod download 24 | - save_cache: 25 | key: go-mod-v4-{{ checksum "go.sum" }} 26 | paths: 27 | - "/go/pkg/mod" 28 | - run: 29 | name: Run tests 30 | command: | 31 | mkdir -p /tmp/test-reports 32 | gotestsum --junitfile /tmp/test-reports/unit-tests.xml 33 | - store_test_results: 34 | path: /tmp/test-reports 35 | 36 | # Invoke jobs via workflows 37 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 38 | workflows: 39 | sample: # This is the name of the workflow, feel free to change it to better match your workflow. 40 | # Inside the workflow, you define the jobs you want to run. 41 | jobs: 42 | - build 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Closer [![Circle CI](https://circleci.com/gh/xlab/closer/tree/master.svg?style=svg)](https://circleci.com/gh/xlab/closer/tree/master) [![GoDoc](https://godoc.org/github.com/xlab/closer?status.svg)](https://godoc.org/github.com/xlab/closer) 2 | 3 | The aim of this package is to provide an universal way to catch the event of application’s exit and perform some actions before it’s too late. `closer` doesn’t care about the way application tries to exit, i.e. was that a panic or just a signal from the OS, it calls the provided methods for cleanup and that’s the whole point. 4 | 5 | ![demo](https://habrastorage.org/getpro/habr/post_images/f2c/025/0cb/f2c0250cbc4e8519d706b5a35374d40d.png) 6 | 7 | ### Usage 8 | 9 | Be careful, this package is using the singleton pattern (like `net/http` does) and doesn't require any initialisation step. However, there’s an option to provide a custom configuration struct. 10 | 11 | ```go 12 | // Init allows user to override the defaults (a set of OS signals to watch for, for example). 13 | func Init(cfg Config) 14 | 15 | // Close sends a close request. 16 | // The app will be terminated by OS as soon as the first close request will be handled by closer, this 17 | // function will return no sooner. The exit code will always be 0 (success). 18 | func Close() 19 | 20 | // Bind will register the cleanup function that will be called when closer will get a close request. 21 | // All the callbacks will be called in the reverse order they were bound, that's similar to how `defer` works. 22 | func Bind(cleanup func()) 23 | 24 | // Checked runs the target function and checks for panics and errors it may yield. In case of panic or error, closer 25 | // will terminate the app with an error code, but either case it will call all the bound callbacks beforehand. 26 | // One can use this instead of `defer` if you need to care about errors and panics that always may happen. 27 | // This function optionally can emit log messages via standard `log` package. 28 | func Checked(target func() error, logging bool) 29 | 30 | // Hold is a helper that may be used to hold the main from returning, 31 | // until the closer will do a proper exit via `os.Exit`. 32 | func Hold() 33 | ``` 34 | 35 | The the usage examples: [example](/cmd/example/main.go), [example-error](/cmd/example-error/main.go) and [example-panic](/cmd/example-panic/main.go). 36 | 37 | ### Table of exit codes 38 | 39 | All errors and panics will be logged if the logging option of `closer.Checked` was set true, also the exit code (for `os.Exit`) will be determined accordingly: 40 | 41 | Event | Default exit code 42 | ------------- | ------------- 43 | error = nil | 0 (success) 44 | error != nil | 1 (failure) 45 | panic | 1 (failure) 46 | 47 | ### License 48 | 49 | [MIT](/LICENSE) 50 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | // SEE https://github.com/bugsnag/bugsnag-go/blob/master/errors/stackframe.go 2 | // for the origin of this code 3 | 4 | package closer 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io/ioutil" 10 | "runtime" 11 | "strings" 12 | ) 13 | 14 | // A StackFrame contains all necessary information about to generate a line 15 | // in a callstack. 16 | type StackFrame struct { 17 | File string 18 | LineNumber int 19 | Name string 20 | Package string 21 | ProgramCounter uintptr 22 | } 23 | 24 | // newStackFrame populates a stack frame object from the program counter. 25 | func newStackFrame(pc uintptr) (frame StackFrame) { 26 | 27 | frame = StackFrame{ProgramCounter: pc} 28 | if frame.Func() == nil { 29 | return 30 | } 31 | frame.Package, frame.Name = packageAndName(frame.Func()) 32 | 33 | // pc -1 because the program counters we use are usually return addresses, 34 | // and we want to show the line that corresponds to the function call 35 | frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1) 36 | return 37 | 38 | } 39 | 40 | // Func returns the function that this stackframe corresponds to 41 | func (frame *StackFrame) Func() *runtime.Func { 42 | if frame.ProgramCounter == 0 { 43 | return nil 44 | } 45 | return runtime.FuncForPC(frame.ProgramCounter) 46 | } 47 | 48 | // String returns the stackframe formatted in the same way as go does 49 | // in runtime/debug.Stack() 50 | func (frame *StackFrame) String() string { 51 | str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter) 52 | 53 | source, err := frame.SourceLine() 54 | if err != nil { 55 | return str 56 | } 57 | 58 | return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source) 59 | } 60 | 61 | // SourceLine gets the line of code (from File and Line) of the original source if possible 62 | func (frame *StackFrame) SourceLine() (string, error) { 63 | data, err := ioutil.ReadFile(frame.File) 64 | 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | lines := bytes.Split(data, []byte{'\n'}) 70 | if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) { 71 | return "???", nil 72 | } 73 | // -1 because line-numbers are 1 based, but our array is 0 based 74 | return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil 75 | } 76 | 77 | func packageAndName(fn *runtime.Func) (string, string) { 78 | name := fn.Name() 79 | pkg := "" 80 | 81 | // The name includes the path name to the package, which is unnecessary 82 | // since the file name is already included. Plus, it has center dots. 83 | // That is, we see 84 | // runtime/debug.*T·ptrmethod 85 | // and want 86 | // *T.ptrmethod 87 | // Since the package path might contains dots (e.g. code.google.com/...), 88 | // we first remove the path prefix if there is one. 89 | if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { 90 | pkg += name[:lastslash] + "/" 91 | name = name[lastslash+1:] 92 | } 93 | if period := strings.Index(name, "."); period >= 0 { 94 | pkg += name[:period] 95 | name = name[period+1:] 96 | } 97 | 98 | name = strings.Replace(name, "·", ".", -1) 99 | return pkg, name 100 | } 101 | -------------------------------------------------------------------------------- /closer.go: -------------------------------------------------------------------------------- 1 | // Package closer ensures a clean exit for your Go app. 2 | // 3 | // The aim of this package is to provide an universal way to catch the event of application’s exit 4 | // and perform some actions before it’s too late. Closer doesn’t care about the way application 5 | // tries to exit, i.e. was that a panic or just a signal from the OS, it calls the provided methods 6 | // for cleanup and that’s the whole point. 7 | // 8 | // Exit codes 9 | // 10 | // All errors and panics will be logged if the logging option of `closer.Checked` was set true, 11 | // also the exit code (for `os.Exit`) will be determined accordingly: 12 | // 13 | // Event | Default exit code 14 | // ------------- | ------------- 15 | // error = nil | 0 (success) 16 | // error != nil | 1 (failure) 17 | // panic | 1 (failure) 18 | // 19 | package closer 20 | 21 | import ( 22 | "fmt" 23 | "log" 24 | "os" 25 | "os/signal" 26 | "runtime" 27 | "sync" 28 | "syscall" 29 | ) 30 | 31 | var ( 32 | // DebugSignalSet is a predefined list of signals to watch for. Usually 33 | // these signals will terminate the app without executing the code in defer blocks. 34 | DebugSignalSet = []os.Signal{ 35 | syscall.SIGINT, 36 | syscall.SIGHUP, 37 | syscall.SIGTERM, 38 | } 39 | // DefaultSignalSet will have syscall.SIGABRT that should be 40 | // opted out if user wants to debug the stacktrace. 41 | DefaultSignalSet = append(DebugSignalSet, syscall.SIGABRT) 42 | ) 43 | 44 | var ( 45 | // ExitCodeOK is a successfull exit code. 46 | ExitCodeOK = 0 47 | // ExitCodeErr is a failure exit code. 48 | ExitCodeErr = 1 49 | // ExitSignals is the active list of signals to watch for. 50 | ExitSignals = DefaultSignalSet 51 | ) 52 | 53 | // Config should be used with Init function to override the defaults. 54 | type Config struct { 55 | ExitCodeOK int 56 | ExitCodeErr int 57 | ExitSignals []os.Signal 58 | } 59 | 60 | var c = newCloser() 61 | 62 | type closer struct { 63 | codeOK int 64 | codeErr int 65 | signals []os.Signal 66 | sem sync.Mutex 67 | closeOnce sync.Once 68 | cleanups []func() 69 | errChan chan struct{} 70 | doneChan chan struct{} 71 | signalChan chan os.Signal 72 | closeChan chan struct{} 73 | holdChan chan struct{} 74 | // 75 | cancelWaitChan chan struct{} 76 | } 77 | 78 | func newCloser() *closer { 79 | c := &closer{ 80 | codeOK: ExitCodeOK, 81 | codeErr: ExitCodeErr, 82 | signals: ExitSignals, 83 | // 84 | errChan: make(chan struct{}), 85 | doneChan: make(chan struct{}), 86 | signalChan: make(chan os.Signal, 1), 87 | closeChan: make(chan struct{}), 88 | holdChan: make(chan struct{}), 89 | // 90 | cancelWaitChan: make(chan struct{}), 91 | } 92 | 93 | signal.Notify(c.signalChan, c.signals...) 94 | 95 | // start waiting 96 | go c.wait() 97 | return c 98 | } 99 | 100 | func (c *closer) wait() { 101 | exitCode := c.codeOK 102 | 103 | // wait for a close request 104 | select { 105 | case <-c.cancelWaitChan: 106 | return 107 | case <-c.signalChan: 108 | case <-c.closeChan: 109 | break 110 | case <-c.errChan: 111 | exitCode = c.codeErr 112 | } 113 | 114 | // ensure we'll exit 115 | defer os.Exit(exitCode) 116 | 117 | c.sem.Lock() 118 | defer c.sem.Unlock() 119 | for _, fn := range c.cleanups { 120 | fn() 121 | } 122 | // done! 123 | close(c.doneChan) 124 | } 125 | 126 | // Close sends a close request. 127 | // The app will be terminated by OS as soon as the first close request will be handled by closer, this 128 | // function will return no sooner. The exit code will always be 0 (success). 129 | func Close() { 130 | // check if there was a panic 131 | if x := recover(); x != nil { 132 | var ( 133 | offset int = 3 134 | pc uintptr 135 | ok bool 136 | ) 137 | log.Printf("run time panic: %v", x) 138 | for offset < 32 { 139 | pc, _, _, ok = runtime.Caller(offset) 140 | if !ok { 141 | // close with an error 142 | c.closeErr() 143 | return 144 | } 145 | frame := newStackFrame(pc) 146 | fmt.Print(frame.String()) 147 | offset++ 148 | } 149 | // close with an error 150 | c.closeErr() 151 | return 152 | } 153 | // normal close 154 | c.closeOnce.Do(func() { 155 | close(c.closeChan) 156 | }) 157 | <-c.doneChan 158 | } 159 | 160 | // Fatalln works the same as log.Fatalln but respects the closer's logic. 161 | func Fatalln(v ...interface{}) { 162 | out := log.New(os.Stderr, "", log.Flags()) 163 | out.Output(2, fmt.Sprintln(v...)) 164 | c.closeErr() 165 | } 166 | 167 | // Fatalf works the same as log.Fatalf but respects the closer's logic. 168 | func Fatalf(format string, v ...interface{}) { 169 | out := log.New(os.Stderr, "", log.Flags()) 170 | out.Output(2, fmt.Sprintf(format, v...)) 171 | c.closeErr() 172 | } 173 | 174 | // Exit is the same as os.Exit but respects the closer's logic. It converts 175 | // any error code into ExitCodeErr (= 1, by default). 176 | func Exit(code int) { 177 | // check if there was a panic 178 | if x := recover(); x != nil { 179 | var ( 180 | offset int = 3 181 | pc uintptr 182 | ok bool 183 | ) 184 | log.Printf("run time panic: %v", x) 185 | for offset < 32 { 186 | pc, _, _, ok = runtime.Caller(offset) 187 | if !ok { 188 | // close with an error 189 | c.closeErr() 190 | return 191 | } 192 | frame := newStackFrame(pc) 193 | fmt.Print(frame.String()) 194 | offset++ 195 | } 196 | // close with an error 197 | c.closeErr() 198 | return 199 | } 200 | if code == ExitCodeOK { 201 | c.closeOnce.Do(func() { 202 | close(c.closeChan) 203 | }) 204 | <-c.doneChan 205 | return 206 | } 207 | c.closeErr() 208 | } 209 | 210 | func (c *closer) closeErr() { 211 | c.closeOnce.Do(func() { 212 | close(c.errChan) 213 | }) 214 | <-c.doneChan 215 | } 216 | 217 | // Init allows user to override the defaults (a set of OS signals to watch for, for example). 218 | func Init(cfg Config) { 219 | c.sem.Lock() 220 | signal.Stop(c.signalChan) 221 | close(c.cancelWaitChan) 222 | c.codeOK = cfg.ExitCodeOK 223 | c.codeErr = cfg.ExitCodeErr 224 | c.signals = cfg.ExitSignals 225 | signal.Notify(c.signalChan, c.signals...) 226 | c.cancelWaitChan = make(chan struct{}) 227 | go c.wait() 228 | c.sem.Unlock() 229 | } 230 | 231 | // Bind will register the cleanup function that will be called when closer will get a close request. 232 | // All the callbacks will be called in the reverse order they were bound, that's similar to how `defer` works. 233 | func Bind(cleanup func()) { 234 | c.sem.Lock() 235 | // store in the reverse order 236 | s := make([]func(), 0, 1+len(c.cleanups)) 237 | s = append(s, cleanup) 238 | c.cleanups = append(s, c.cleanups...) 239 | c.sem.Unlock() 240 | } 241 | 242 | // Checked runs the target function and checks for panics and errors it may yield. In case of panic or error, closer 243 | // will terminate the app with an error code, but either case it will call all the bound callbacks beforehand. 244 | // One can use this instead of `defer` if you need to care about errors and panics that always may happen. 245 | // This function optionally can emit log messages via standard `log` package. 246 | func Checked(target func() error, logging bool) { 247 | defer func() { 248 | // check if there was a panic 249 | if x := recover(); x != nil { 250 | if logging { 251 | log.Printf("run time panic: %v", x) 252 | } 253 | // close with an error 254 | c.closeErr() 255 | } 256 | }() 257 | if err := target(); err != nil { 258 | if logging { 259 | log.Println("error:", err) 260 | } 261 | // close with an error 262 | c.closeErr() 263 | } 264 | } 265 | 266 | // Hold is a helper that may be used to hold the main from returning, 267 | // until the closer will do a proper exit via `os.Exit`. 268 | func Hold() { 269 | <-c.holdChan 270 | } 271 | --------------------------------------------------------------------------------