├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── examples ├── add.go ├── add_with_param.go ├── minimal.go ├── sample.go └── signals_list.go ├── go.mod ├── shutdown.go └── shutdown_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage.out 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | before_install: 4 | - go get github.com/mattn/goveralls 5 | 6 | go: 7 | - "1.11.x" 8 | - "1.10.x" 9 | 10 | script: 11 | - go test -cover -v -covermode=count -coverprofile=coverage.out 12 | - $GOPATH/bin/goveralls -coverprofile=coverage.out -service=travis-ci 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.1] - 2019-02-09 4 | 5 | ### Added 6 | 7 | - Changelog. 8 | - `go.mod` file. 9 | - License. 10 | - Tests with 100% coverage. 11 | - Travis CI. 12 | - `New` to be able to create several `Shutdown` instances. 13 | - `Reset` and `Shutdown.Reset` to cancel all hooks. 14 | 15 | ### Changed 16 | 17 | - `DefaultShutdown` is now exported. 18 | 19 | ## [0.1.0] - 2018-11-17 20 | 21 | ### Added 22 | 23 | - Initial version. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 ztrue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | .PHONY: lint 4 | lint: 5 | go fmt . && \ 6 | go fmt ./examples && \ 7 | go vet && \ 8 | golint $$(go list ./...) 9 | 10 | .PHONY: doc 11 | doc: 12 | @echo GoDoc link: http://localhost:6060/pkg/github.com/ztrue/shutdown 13 | godoc -http=:6060 14 | 15 | .PHONY: test 16 | test: 17 | go test -cover -v 18 | 19 | .PHONY: coverage 20 | coverage: 21 | go test -coverprofile=coverage.out && \ 22 | go tool cover -func=coverage.out && \ 23 | go tool cover -html=coverage.out 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang App Shutdown Hooks 2 | 3 | [![GoDoc](https://godoc.org/github.com/ztrue/shutdown?status.svg)](https://godoc.org/github.com/ztrue/shutdown) 4 | [![Report](https://goreportcard.com/badge/github.com/ztrue/shutdown)](https://goreportcard.com/report/github.com/ztrue/shutdown) 5 | [![Coverage Status](https://coveralls.io/repos/github/ztrue/shutdown/badge.svg?branch=master)](https://coveralls.io/github/ztrue/shutdown?branch=master) 6 | [![Build Status](https://travis-ci.com/ztrue/shutdown.svg?branch=master)](https://travis-ci.com/ztrue/shutdown) 7 | 8 | This package provides convenient interface for working with `os.Signal`. 9 | 10 | Multiple hooks can be applied, they will be called simultaneously on app shutdown. 11 | 12 | ## Example 13 | 14 | ```go 15 | package main 16 | 17 | import ( 18 | "log" 19 | "time" 20 | 21 | "github.com/ztrue/shutdown" 22 | ) 23 | 24 | func main() { 25 | shutdown.Add(func() { 26 | // Write log. 27 | // Stop writing files. 28 | // Close connections. 29 | // Etc. 30 | log.Println("Stopping...") 31 | log.Println("3") 32 | time.Sleep(time.Second) 33 | log.Println("2") 34 | time.Sleep(time.Second) 35 | log.Println("1") 36 | time.Sleep(time.Second) 37 | log.Println("0, stopped") 38 | }) 39 | 40 | // App emulation. 41 | go func() { 42 | log.Println("App running, press CTRL + C to stop") 43 | select {} 44 | }() 45 | 46 | shutdown.Listen() 47 | } 48 | ``` 49 | 50 | Find more executable examples in [examples](examples) dir. 51 | 52 | ## How to Use 53 | 54 | ### Import 55 | 56 | ```go 57 | import "github.com/ztrue/shutdown" 58 | ``` 59 | 60 | ### Add Shutdown Hook 61 | 62 | ```go 63 | shutdown.Add(func() { 64 | log.Println("Stopping") 65 | }) 66 | ``` 67 | 68 | ### Remove Hook 69 | 70 | ```go 71 | key := shutdown.Add(func() { 72 | log.Println("Stopping") 73 | }) 74 | 75 | shutdown.Remove(key) 76 | ``` 77 | 78 | ### Hook With Custom Key 79 | 80 | ```go 81 | shutdown.AddWithKey("mykey", func() { 82 | log.Println("Stopping") 83 | }) 84 | 85 | shutdown.Remove("mykey") 86 | ``` 87 | 88 | ### Hook With Signal Parameter 89 | 90 | ```go 91 | shutdown.AddWithParam(func(os.Signal) { 92 | log.Println("Stopping because of", os.Signal) 93 | }) 94 | ``` 95 | 96 | ### Listen for Specific Signals 97 | 98 | ```go 99 | shutdown.Listen(syscall.SIGINT, syscall.SIGTERM) 100 | ``` 101 | 102 | ### Reload 103 | Reload service on `SIGHUP` 104 | ```go 105 | shutdown.AddWithParam(func(sig os.Signal) { 106 | if sig == syscall.SIGHUP { 107 | log.Println( "Reload conf...") 108 | reloadConf() 109 | // listen again signals to avoid shutdown 110 | shutdown.Listen(syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 111 | } else { // SIGINT, SIGTERM 112 | log.Println("Stopping...") 113 | } 114 | }) 115 | shutdown.Listen(syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 116 | ``` 117 | -------------------------------------------------------------------------------- /examples/add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/ztrue/shutdown" 8 | // "../../shutdown" 9 | ) 10 | 11 | func main() { 12 | shutdown.Add(func() { 13 | log.Println("foo stopping...") 14 | log.Println("foo stopped") 15 | }) 16 | 17 | bazKey := shutdown.Add(func() { 18 | log.Println("bar stopping...") 19 | time.Sleep(time.Second) 20 | log.Println("bar stopped") 21 | }) 22 | 23 | shutdown.Add(func() { 24 | log.Println("baz stopping...") 25 | time.Sleep(2 * time.Second) 26 | log.Println("baz stopped") 27 | }) 28 | 29 | // App emulation. 30 | go run() 31 | 32 | shutdown.Remove(bazKey) 33 | 34 | shutdown.Listen() 35 | } 36 | 37 | func run() { 38 | log.Println("App running, press CTRL + C to stop") 39 | select {} 40 | } 41 | -------------------------------------------------------------------------------- /examples/add_with_param.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/ztrue/shutdown" 9 | // "../../shutdown" 10 | ) 11 | 12 | func main() { 13 | shutdown.AddWithParam(func(sig os.Signal) { 14 | log.Println(sig, "foo stopping...") 15 | log.Println(sig, "foo stopped") 16 | }) 17 | 18 | bazKey := shutdown.AddWithParam(func(sig os.Signal) { 19 | log.Println(sig, "bar stopping...") 20 | time.Sleep(time.Second) 21 | log.Println(sig, "bar stopped") 22 | }) 23 | 24 | shutdown.AddWithParam(func(sig os.Signal) { 25 | log.Println(sig, "baz stopping...") 26 | time.Sleep(2 * time.Second) 27 | log.Println(sig, "baz stopped") 28 | }) 29 | 30 | // App emulation. 31 | go run() 32 | 33 | shutdown.Remove(bazKey) 34 | 35 | shutdown.Listen() 36 | } 37 | 38 | func run() { 39 | log.Println("App running, press CTRL + C to stop") 40 | select {} 41 | } 42 | -------------------------------------------------------------------------------- /examples/minimal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ztrue/shutdown" 7 | // "../../shutdown" 8 | ) 9 | 10 | func main() { 11 | shutdown.Add(func() { 12 | log.Println("Stopped") 13 | }) 14 | 15 | // App emulation. 16 | go func() { 17 | log.Println("App running, press CTRL + C to stop") 18 | select {} 19 | }() 20 | 21 | shutdown.Listen() 22 | } 23 | -------------------------------------------------------------------------------- /examples/sample.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/ztrue/shutdown" 8 | // "../../shutdown" 9 | ) 10 | 11 | func main() { 12 | shutdown.Add(func() { 13 | // Write log. 14 | // Stop writing files. 15 | // Close connections. 16 | // Etc. 17 | log.Println("Stopping...") 18 | log.Println("3") 19 | time.Sleep(time.Second) 20 | log.Println("2") 21 | time.Sleep(time.Second) 22 | log.Println("1") 23 | time.Sleep(time.Second) 24 | log.Println("0, stopped") 25 | }) 26 | 27 | // App emulation. 28 | go func() { 29 | log.Println("App running, press CTRL + C to stop") 30 | select {} 31 | }() 32 | 33 | shutdown.Listen() 34 | } 35 | -------------------------------------------------------------------------------- /examples/signals_list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "syscall" 6 | "time" 7 | 8 | "github.com/ztrue/shutdown" 9 | // "../../shutdown" 10 | ) 11 | 12 | func main() { 13 | shutdown.Add(func() { 14 | log.Println("Stopping...") 15 | time.Sleep(2 * time.Second) 16 | log.Println("Stopped") 17 | }) 18 | 19 | // App emulation. 20 | go run() 21 | 22 | // Handle only SIGINT and SIGTERM. 23 | shutdown.Listen(syscall.SIGINT, syscall.SIGTERM) 24 | } 25 | 26 | func run() { 27 | log.Println("App running, press CTRL + C to stop") 28 | select {} 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ztrue/shutdown 2 | -------------------------------------------------------------------------------- /shutdown.go: -------------------------------------------------------------------------------- 1 | // Package shutdown provides convenient interface for working with os.Signal. 2 | // 3 | // Multiple hooks can be applied, 4 | // they will be called simultaneously on app shutdown. 5 | package shutdown 6 | 7 | import ( 8 | "math/rand" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | ) 13 | 14 | // DefaultShutdown is a default instance. 15 | var DefaultShutdown = New() 16 | 17 | // Shutdown is an instance of shutdown handler. 18 | type Shutdown struct { 19 | hooks map[string]func(os.Signal) 20 | mutex *sync.Mutex 21 | } 22 | 23 | // New creates a new Shutdown instance. 24 | func New() *Shutdown { 25 | return &Shutdown{ 26 | hooks: map[string]func(os.Signal){}, 27 | mutex: &sync.Mutex{}, 28 | } 29 | } 30 | 31 | // Add adds a shutdown hook 32 | // and returns hook identificator (key). 33 | func Add(fn func()) string { 34 | return DefaultShutdown.Add(fn) 35 | } 36 | 37 | // AddWithKey adds a shutdown hook 38 | // with provided identificator (key). 39 | func AddWithKey(key string, fn func()) { 40 | DefaultShutdown.AddWithKey(key, fn) 41 | } 42 | 43 | // AddWithParam adds a shutdown hook with signal parameter 44 | // and returns hook identificator (key). 45 | func AddWithParam(fn func(os.Signal)) string { 46 | return DefaultShutdown.AddWithParam(fn) 47 | } 48 | 49 | // AddWithKeyWithParam adds a shutdown hook with signal parameter 50 | // with provided identificator (key). 51 | func AddWithKeyWithParam(key string, fn func(os.Signal)) { 52 | DefaultShutdown.AddWithKeyWithParam(key, fn) 53 | } 54 | 55 | // Hooks returns a copy of current hooks. 56 | func Hooks() map[string]func(os.Signal) { 57 | return DefaultShutdown.Hooks() 58 | } 59 | 60 | // Listen waits for provided OS signals. 61 | // It will wait for any signal if no signals provided. 62 | func Listen(signals ...os.Signal) { 63 | DefaultShutdown.Listen(signals...) 64 | } 65 | 66 | // Remove cancels hook by identificator (key). 67 | func Remove(key string) { 68 | DefaultShutdown.Remove(key) 69 | } 70 | 71 | // Reset cancels all hooks. 72 | func Reset() { 73 | DefaultShutdown.Reset() 74 | } 75 | 76 | // Add adds a shutdown hook 77 | // and returns hook identificator (key). 78 | func (s *Shutdown) Add(fn func()) string { 79 | return s.AddWithParam(func(os.Signal) { 80 | fn() 81 | }) 82 | } 83 | 84 | // AddWithKey adds a shutdown hook 85 | // with provided identificator (key). 86 | func (s *Shutdown) AddWithKey(key string, fn func()) { 87 | s.AddWithKeyWithParam(key, func(os.Signal) { 88 | fn() 89 | }) 90 | } 91 | 92 | // AddWithParam adds a shutdown hook with signal parameter 93 | // and returns hook identificator (key). 94 | func (s *Shutdown) AddWithParam(fn func(os.Signal)) string { 95 | key := randomKey() 96 | s.AddWithKeyWithParam(key, fn) 97 | return key 98 | } 99 | 100 | // AddWithKeyWithParam adds a shutdown hook with signal parameter 101 | // with provided identificator (key). 102 | func (s *Shutdown) AddWithKeyWithParam(key string, fn func(os.Signal)) { 103 | s.mutex.Lock() 104 | defer s.mutex.Unlock() 105 | s.hooks[key] = fn 106 | } 107 | 108 | // Hooks returns a copy of current hooks. 109 | func (s *Shutdown) Hooks() map[string]func(os.Signal) { 110 | s.mutex.Lock() 111 | defer s.mutex.Unlock() 112 | fns := map[string]func(os.Signal){} 113 | for key, cb := range s.hooks { 114 | fns[key] = cb 115 | } 116 | return fns 117 | } 118 | 119 | // Listen waits for provided OS signals. 120 | // It will wait for any signal if no signals provided. 121 | func (s *Shutdown) Listen(signals ...os.Signal) { 122 | ch := make(chan os.Signal, 1) 123 | signal.Notify(ch, signals...) 124 | sig := <-ch 125 | var wg sync.WaitGroup 126 | for _, fn := range s.Hooks() { 127 | wg.Add(1) 128 | go func(sig os.Signal, fn func(os.Signal)) { 129 | defer wg.Done() 130 | fn(sig) 131 | }(sig, fn) 132 | } 133 | wg.Wait() 134 | } 135 | 136 | // Remove cancels hook by identificator (key). 137 | func (s *Shutdown) Remove(key string) { 138 | s.mutex.Lock() 139 | defer s.mutex.Unlock() 140 | delete(s.hooks, key) 141 | } 142 | 143 | // Reset cancels all hooks. 144 | func (s *Shutdown) Reset() { 145 | s.mutex.Lock() 146 | defer s.mutex.Unlock() 147 | for key := range s.hooks { 148 | delete(s.hooks, key) 149 | } 150 | } 151 | 152 | // randomKey generates a random identificator (key) for hook. 153 | // 154 | // Do not use this identificator for purposes other then to remove a hook 155 | // as long as it's not fairly random without seed. 156 | func randomKey() string { 157 | runes := []rune("0123456789abcdefghijklmnopqrstuvwxyz") 158 | b := make([]rune, 16) 159 | for i := range b { 160 | b[i] = runes[rand.Intn(len(runes))] 161 | } 162 | return string(b) 163 | } 164 | -------------------------------------------------------------------------------- /shutdown_test.go: -------------------------------------------------------------------------------- 1 | package shutdown_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/ztrue/shutdown" 14 | ) 15 | 16 | func TestHooks(t *testing.T) { 17 | defer shutdown.Reset() 18 | 19 | data := map[string]int{} 20 | hook1 := func() { 21 | data["1:foo"]++ 22 | } 23 | hook2 := func() { 24 | data["2:bar"]++ 25 | } 26 | hook3 := func() { 27 | data["3:baz"]++ 28 | } 29 | hook4 := func() { 30 | data["4:qux"]++ 31 | } 32 | hook5 := func(sig os.Signal) { 33 | data["5:"+sig.String()]++ 34 | } 35 | hook6 := func(sig os.Signal) { 36 | data["6:"+sig.String()]++ 37 | } 38 | hook7 := func(sig os.Signal) { 39 | data["7:"+sig.String()]++ 40 | } 41 | hook8 := func(sig os.Signal) { 42 | data["8:"+sig.String()]++ 43 | } 44 | 45 | // Add hooks. 46 | shutdown.Add(hook1) 47 | key2 := shutdown.Add(hook2) 48 | key3 := "hook3" 49 | shutdown.AddWithKey(key3, hook3) 50 | key4 := "hook4" 51 | shutdown.AddWithKey(key4, hook4) 52 | shutdown.AddWithParam(hook5) 53 | key6 := shutdown.AddWithParam(hook6) 54 | key7 := "hook7" 55 | shutdown.AddWithKeyWithParam(key7, hook7) 56 | key8 := "hook8" 57 | shutdown.AddWithKeyWithParam(key8, hook8) 58 | 59 | hooks := shutdown.Hooks() 60 | if len(hooks) != 8 { 61 | t.Errorf( 62 | "len(shutdown.Hooks()) = %#v; want %#v", 63 | len(hooks), 8, 64 | ) 65 | } 66 | 67 | // Assert data is not added yet. 68 | if len(data) != 0 { 69 | t.Errorf( 70 | "len(data) = %#v; want %#v", 71 | len(data), 0, 72 | ) 73 | } 74 | // Run hooks. 75 | for _, hook := range hooks { 76 | hook(os.Interrupt) 77 | } 78 | 79 | dataKeys := []string{ 80 | "1:foo", 81 | "2:bar", 82 | "3:baz", 83 | "4:qux", 84 | "5:interrupt", 85 | "6:interrupt", 86 | "7:interrupt", 87 | "8:interrupt", 88 | } 89 | for _, dataKey := range dataKeys { 90 | if data[dataKey] != 1 { 91 | t.Errorf( 92 | "data[%#v] = %#v; want %#v", 93 | dataKey, data[dataKey], 1, 94 | ) 95 | } 96 | } 97 | // Assert no extra data keys. 98 | if len(data) != 8 { 99 | t.Errorf( 100 | "len(data) = %#v; want %#v", 101 | len(data), 8, 102 | ) 103 | } 104 | 105 | // Reset data. 106 | data = map[string]int{} 107 | // Remove some hooks. 108 | shutdown.Remove(key2) 109 | shutdown.Remove(key4) 110 | shutdown.Remove(key6) 111 | shutdown.Remove(key8) 112 | 113 | hooks = shutdown.Hooks() 114 | if len(hooks) != 4 { 115 | t.Errorf( 116 | "len(shutdown.Hooks()) = %#v; want %#v", 117 | len(hooks), 4, 118 | ) 119 | } 120 | 121 | // Assert data is not added yet. 122 | if len(data) != 0 { 123 | t.Errorf( 124 | "len(data) = %#v; want %#v", 125 | len(data), 0, 126 | ) 127 | } 128 | // Run hooks. 129 | for _, hook := range hooks { 130 | hook(os.Interrupt) 131 | } 132 | 133 | dataKeys = []string{ 134 | "1:foo", 135 | "3:baz", 136 | "5:interrupt", 137 | "7:interrupt", 138 | } 139 | for _, dataKey := range dataKeys { 140 | if data[dataKey] != 1 { 141 | t.Errorf( 142 | "data[%#v] = %#v; want %#v", 143 | dataKey, data[dataKey], 1, 144 | ) 145 | } 146 | } 147 | // Assert no extra data keys. 148 | if len(data) != 4 { 149 | t.Errorf( 150 | "len(data) = %#v; want %#v", 151 | len(data), 4, 152 | ) 153 | } 154 | } 155 | 156 | func TestListenSameProcess(t *testing.T) { 157 | defer shutdown.Reset() 158 | 159 | data := map[string]int{} 160 | var mutex sync.Mutex 161 | 162 | // Add 3 hooks. 163 | shutdown.Add(func() { 164 | mutex.Lock() 165 | defer mutex.Unlock() 166 | data["foo"]++ 167 | }) 168 | key := shutdown.Add(func() { 169 | mutex.Lock() 170 | defer mutex.Unlock() 171 | data["bar"]++ 172 | }) 173 | shutdown.Add(func() { 174 | mutex.Lock() 175 | defer mutex.Unlock() 176 | data["baz"]++ 177 | }) 178 | // Remove one of them. 179 | shutdown.Remove(key) 180 | 181 | go func() { 182 | // TODO Is there a better solution to make sure listening is started? 183 | time.Sleep(10 * time.Millisecond) 184 | p, err := os.FindProcess(os.Getpid()) 185 | if err != nil { 186 | panic(err.Error()) 187 | } 188 | err = p.Signal(os.Interrupt) 189 | if err != nil { 190 | panic(err.Error()) 191 | } 192 | }() 193 | 194 | shutdown.Listen() 195 | 196 | if len(data) != 2 { 197 | t.Errorf( 198 | "len(data) = %#v; want %#v", 199 | len(data), 2, 200 | ) 201 | } 202 | 203 | dataKeys := []string{"foo", "baz"} 204 | for _, dataKey := range dataKeys { 205 | if data[dataKey] != 1 { 206 | t.Errorf( 207 | "data[%#v] = %#v; want %#v", 208 | dataKey, data[dataKey], 1, 209 | ) 210 | } 211 | } 212 | } 213 | 214 | func TestListenSeparateProcess(t *testing.T) { 215 | // TODO "Listen" coverage does not count as 216 | // far as code executed in a separate process, 217 | // no matter if it's actually tested and covered, 218 | // how to make it count? 219 | if os.Getenv("LISTEN") == "1" { 220 | // Add 3 hooks. 221 | shutdown.Add(func() { 222 | fmt.Println("foo") 223 | }) 224 | key := shutdown.Add(func() { 225 | fmt.Println("bar") 226 | }) 227 | shutdown.Add(func() { 228 | fmt.Println("baz") 229 | }) 230 | // Remove one of them. 231 | shutdown.Remove(key) 232 | shutdown.Listen() 233 | return 234 | } 235 | 236 | var buf bytes.Buffer 237 | cmd := exec.Command(os.Args[0], "-test.run=TestListenSeparateProcess") 238 | cmd.Env = append(os.Environ(), "LISTEN=1") 239 | cmd.Stdout = &buf 240 | err := cmd.Start() 241 | if err != nil { 242 | panic(err.Error()) 243 | } 244 | 245 | // TODO Better solution to wait for programm launch? 246 | time.Sleep(10 * time.Millisecond) 247 | 248 | err = cmd.Process.Signal(os.Interrupt) 249 | if err != nil { 250 | panic(err.Error()) 251 | } 252 | err = cmd.Wait() 253 | if err != nil { 254 | panic(err.Error()) 255 | } 256 | lines := strings.Split(buf.String(), "\n") 257 | if len(lines) < 3 { 258 | t.Errorf( 259 | "len(lines) = %#v; want >= %#v", 260 | len(lines), 3, 261 | ) 262 | } 263 | 264 | fooFirst := lines[0] == "foo" && lines[1] == "baz" 265 | bazFirst := lines[0] == "baz" && lines[1] == "foo" 266 | if !fooFirst && !bazFirst { 267 | t.Errorf( 268 | "lines[0] = %#v; lines[1] = %#v; want %#v and %#v", 269 | lines[0], lines[1], "foo", "bar", 270 | ) 271 | } 272 | 273 | if lines[2] != "PASS" { 274 | t.Errorf( 275 | "line[2] = %#v; want %#v", 276 | lines[2], "PASS", 277 | ) 278 | } 279 | } 280 | 281 | func TestNew(t *testing.T) { 282 | data := map[string]int{} 283 | 284 | hook1 := func() { 285 | data["foo"]++ 286 | } 287 | hook2 := func() { 288 | data["bar"]++ 289 | } 290 | 291 | s1 := shutdown.New() 292 | s2 := shutdown.New() 293 | 294 | // Add hooks with the same key. 295 | s1.AddWithKey("hook", hook1) 296 | s2.AddWithKey("hook", hook2) 297 | 298 | if len(s1.Hooks()) != 1 { 299 | t.Errorf( 300 | "len(s1.Hooks()) = %#v; want %#v", 301 | len(s1.Hooks()), 1, 302 | ) 303 | } 304 | if len(s2.Hooks()) != 1 { 305 | t.Errorf( 306 | "len(s2.Hooks()) = %#v; want %#v", 307 | len(s2.Hooks()), 1, 308 | ) 309 | } 310 | 311 | // Run both hooks. 312 | s1.Hooks()["hook"](os.Interrupt) 313 | s2.Hooks()["hook"](os.Interrupt) 314 | 315 | // Make sure both hooks executed once. 316 | if len(data) != 2 { 317 | t.Errorf( 318 | "len(data) = %#v; want %#v", 319 | len(data), 2, 320 | ) 321 | } 322 | dataKeys := []string{"foo", "bar"} 323 | for _, dataKey := range dataKeys { 324 | if data[dataKey] != 1 { 325 | t.Errorf( 326 | "data[%#v] = %#v; want %#v", 327 | dataKey, data[dataKey], 1, 328 | ) 329 | } 330 | } 331 | } 332 | --------------------------------------------------------------------------------