diff --git a/README.md b/README.md index d1cf643..fad88cb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ -# os-signal-subscriber -Signal subscriber that allows you to attach a callback to an `os.Signal` notification +# signal +This repository provides helpers with signals + +## Subscriber +Signal subscriber that allows you to attach a callback to an `os.Signal` notification. + +Useful to react to any os.Signal. + +It returns an `unsubscribe` function that can gracefully stop some http server and clean allocated object diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c439fc8 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/gol4ng/signal + +go 1.12 + +require ( + bou.ke/monkey v1.0.1 + github.com/stretchr/testify v1.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..895284f --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +bou.ke/monkey v1.0.1 h1:zEMLInw9xvNakzUUPjfS4Ds6jYPqCFx3m7bRmG5NH2U= +bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/subscriber.go b/subscriber.go new file mode 100644 index 0000000..74422e7 --- /dev/null +++ b/subscriber.go @@ -0,0 +1,46 @@ +package signal + +import ( + "os" + "os/signal" +) + +// This is a really simple signal subscriber +// Signal subscriber that allows you to attach a callback to an `os.Signal` notification. +// Usefull to react to any os.Signal. +// It returns an `unsubscribe` function that stops the goroutine and clean allocated object +// +// Example: +// unsubscriber := signal.subscribe(func(s os.Signal) { +// fmt.Println("process as been asked to be stopped") +// }, os.SIGSTOP) +// +// call "unsubscriber()" in order to detach your callback and clean memory +// +// if no 2nd arg is passed, the `callback` func will be called everytime an os.Signal is triggered +// signal.subscribe(func(s os.Signal) {fmt.Println("called for any signal")}) +// +// /!\ CAUTION /!\ +// If you call it with second arg to `nil`, no signal will be listened +// signal.subscribe(func(s os.Signal) {fmt.Println("NEVER BE CALLED")}, nil) +func Subscribe(callback func(os.Signal), signals ...os.Signal) func() { + signalChan := make(chan os.Signal, 1) + stopChan := make(chan struct{}, 1) + signal.Notify(signalChan, signals...) + go func() { + for { + select { + case <-stopChan: + signal.Stop(signalChan) + close(stopChan) + close(signalChan) + return + case sig := <-signalChan: + go callback(sig) + } + } + }() + return func() { + stopChan <- struct{}{} + } +} diff --git a/subscriber_test.go b/subscriber_test.go new file mode 100644 index 0000000..ff0383a --- /dev/null +++ b/subscriber_test.go @@ -0,0 +1,74 @@ +package signal_test + +import ( + "os" + "os/signal" + "testing" + "time" + "unsafe" + + "bou.ke/monkey" + + "github.com/stretchr/testify/assert" + + sig "github.com/gol4ng/signal" +) + +func TestSubscribe(t *testing.T) { + var signalChan chan<- os.Signal + var subscribedSignals = []os.Signal{os.Interrupt, os.Kill} + var realSignal = os.Interrupt + var callbackCalled = false + + monkey.Patch(signal.Notify, func(c chan<- os.Signal, signals ...os.Signal) { + // get subscriber internal chan in order to simulate signal receive + signalChan = c + for _, s := range subscribedSignals { + assert.Contains(t, signals, s) + } + }) + defer monkey.UnpatchAll() + + sig.Subscribe(func(signal os.Signal) { + assert.Equal(t, realSignal, signal, "wrong signal ingested by subscriber.") + callbackCalled = true + }, subscribedSignals...) + + // simulate signal receive + signalChan <- realSignal + // wait for goroutine callback func fulfilment (@see subscriber.go `go callback(subscribedSignals)`) + time.Sleep(1 * time.Millisecond) + assert.True(t, callbackCalled, "callback func should be called.") +} + +func TestUnSubscribe(t *testing.T) { + var signalChan chan<- os.Signal + var subscribedSignal = os.Interrupt + var stopCalled = false + var callbackCalled = false + + monkey.Patch(signal.Notify, func(c chan<- os.Signal, signals ...os.Signal) { + // get subscriber internal chan in order to simulate signal receive + signalChan = c + assert.Equal(t, subscribedSignal, signals[0]) + }) + monkey.Patch(signal.Stop, func(c chan<- os.Signal) { + assert.Equal(t, signalChan, c) + stopCalled = true + }) + defer monkey.UnpatchAll() + + unsubscribeFunc := sig.Subscribe(func(signal os.Signal) { + assert.Equal(t, signal, subscribedSignal, "wrong signal ingested by subscriber.") + callbackCalled = true + }, subscribedSignal) + + unsubscribeFunc() + // wait for goroutine signal.Stop func call (@see subscriber.go `case <-stopChan:`) + time.Sleep(1 * time.Millisecond) + // `signalChan` is a bidirectional chan as it is equal to the `subscriber signalChan` value + _, ok := <-*(*chan os.Signal)(unsafe.Pointer(&signalChan)) + assert.False(t, ok, "signal channel should be closed.") + assert.True(t, stopCalled, "`signal.Stop` should be called when `unsubscribeFunc` is called.") + assert.False(t, callbackCalled, "callback func should not be called as `unsubscribeFunc` is called.") +}