From 4ad5470a7de7af1c1e98ddd8c60ccc59ed8ded2d Mon Sep 17 00:00:00 2001 From: Jayson Date: Mon, 13 Dec 2021 22:28:15 +0800 Subject: [PATCH] first go-signal --- .editorconfig | 14 +++++ .gitignore | 15 ++++++ README.md | 52 +++++++++++++++++- go.mod | 9 ++++ go.sum | 13 +++++ signal.go | 118 +++++++++++++++++++++++++++++++++++++++++ signal_test.go | 75 ++++++++++++++++++++++++++ signal_unix_test.go | 21 ++++++++ signal_windows_test.go | 22 ++++++++ 9 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 signal.go create mode 100644 signal_test.go create mode 100644 signal_unix_test.go create mode 100644 signal_windows_test.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..10f7b6e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 120 +tab_width = 4 + +[*.go] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6623bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# JetBrains +.idea diff --git a/README.md b/README.md index dcdad41..89fede4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ # go-signal -provides simple, semantic manipulation of the operating system's signal processing. +[![GoDoc](https://godoc.org/github.com/wjiec/go-signal?status.svg)](https://godoc.org/github.com/wjiec/go-signal) + +Package signal provides simple, semantic manipulation of the operating system's signal processing. + + +### Installation + +```bash +go get -u github.com/wjiec/go-signal +``` + + +### Quick Start + +Listens to the user's signal to exit the program and performs cleanup +```go +func main() { + f, _ := os.Open("path/to/your/config") + signal.Once(syscall.SIGTERM).Notify(context.TODO(), func(sig os.Signal) { + _ = f.Close() + }) +} +``` + +Listening for `SIGUSR1` signals from users and performing services reload +```go +var srv Reloadable + +func main() { + signal.When(syscall.SIGUSR1).Notify(context.TODO(), func(sig os.Signal) { + _ = srv.Reload() + }) +} +``` + +Create a context object using the specified signals and cancel the current context when the signal arrived +```go +var db *sql.DB + +func main() { + ctx, cancel := signal.With(context.TODO(), syscall.SIGTERM) + defer cancel() + + _, _ = db.QueryContext(ctx, "select id,username,password from `user`") +} +``` + + +### License + +Released under the [MIT License](LICENSE). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4fcefdf --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/wjiec/go-signal + +go 1.13 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c221f64 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/signal.go b/signal.go new file mode 100644 index 0000000..342370b --- /dev/null +++ b/signal.go @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021 Jayson Wang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Package signal provides simple, semantic manipulation of the operating system's +// signal processing. +package signal + +import ( + "context" + "os" + "os/signal" + "syscall" +) + +const ( + // SigCtx represents signal from cancelled context + SigCtx = syscall.Signal(0xff) +) + +// Notifier creates and listens to a specified system signal and calls a handler +// when the signal is received or when the context is cancelled. +// +// Notifier will be closed when the context is cancelled and the +// handler can get an instance of SigCtx (number = 0xff). +// +// methods of the Notifier can be safely called multiple times. +type Notifier interface { + Notify(context.Context, func(sig os.Signal)) +} + +// When perform the handler function when one of the listed signals arrives. +// +// Notifier created by When can only be closed by canceling the context object. +func When(signals ...os.Signal) Notifier { + return ¬ifier{ + once: false, + signals: signals, + } +} + +// Once perform the handler once only when the context is cancelled or +// when one of the listed signals arrives, after which the Notifier +// will be closed directly. +func Once(signals ...os.Signal) Notifier { + return ¬ifier{ + once: true, + signals: signals, + } +} + +// notifier implements the Notifier interface +// +// The internal once state determines whether the handler +// can be performed multiple. +// +// signals indicate the list of operating system's signal +// to be listened to. +type notifier struct { + once bool + signals []os.Signal +} + +// Notify creates a channel to receive signals from the operating system, passed +// the signal to the handler when it is received. +// +// when the context object is cancelled, the SigCtx is passed to the handler and +// the channel and goroutine are cleaned up and exited. +func (n *notifier) Notify(ctx context.Context, handler func(sig os.Signal)) { + signals := make(chan os.Signal, 1) + signal.Notify(signals, n.signals...) + + go func() { + defer func() { signal.Stop(signals); close(signals) }() + + for { + select { + case sig := <-signals: + if handler(sig); n.once { + return + } + case <-ctx.Done(): + handler(SigCtx) + return + } + } + }() +} + +// With returns a copy of the parent context that is marked done +// when one of the listed signals arrives, when the returned cancel +// function is called, or when the parent context's canceled, +// whichever happens first. +func With(parent context.Context, signals ...os.Signal) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(parent) + Once(signals...).Notify(parent, func(sig os.Signal) { + cancel() + }) + return ctx, cancel +} diff --git a/signal_test.go b/signal_test.go new file mode 100644 index 0000000..4969ed7 --- /dev/null +++ b/signal_test.go @@ -0,0 +1,75 @@ +package signal + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func WaitAny(d time.Duration, wait, timeout func()) { + ch := make(chan struct{}) + + go func() { + wait() + ch <- struct{}{} + close(ch) + }() + + select { + case <-time.After(d): + timeout() + case <-ch: + } +} + +func NoSignalArrived(t *testing.T) func() { + return func() { + t.Error("no signal arrived") + } +} + +func TestOnce(t *testing.T) { + var wg sync.WaitGroup + + wg.Add(1) + Once(SigUsr1).Notify(context.TODO(), func(sig os.Signal) { + if assert.Equal(t, SigUsr1, sig) { + wg.Done() + } + }) + + if pid := os.Getpid(); assert.Greater(t, pid, 0) { + if err := SendSignalUser1(pid); assert.NoError(t, err) { + WaitAny(time.Second, wg.Wait, NoSignalArrived(t)) + } + + assert.NoError(t, SendSignalUser1(pid)) + } +} + +func TestWhen(t *testing.T) { + var wg sync.WaitGroup + + wg.Add(1) + When(SigUsr2).Notify(context.TODO(), func(sig os.Signal) { + if assert.Equal(t, SigUsr2, sig) { + wg.Done() + } + }) + + if pid := os.Getpid(); assert.Greater(t, pid, 0) { + if assert.NoError(t, SendSignalUser2(pid)) { + WaitAny(time.Second, func() { + wg.Wait() + wg.Add(1) + if assert.NoError(t, SendSignalUser2(pid)) { + wg.Wait() + } + }, NoSignalArrived(t)) + } + } +} diff --git a/signal_unix_test.go b/signal_unix_test.go new file mode 100644 index 0000000..539ecfd --- /dev/null +++ b/signal_unix_test.go @@ -0,0 +1,21 @@ +//go:build linux || unix || openbsd || darwin +// +build linux unix openbsd darwin + +package signal + +import ( + "syscall" +) + +const ( + SigUsr1 = syscall.SIGUSR1 + SigUsr2 = syscall.SIGUSR2 +) + +func SendSignalUser1(pid int) error { + return syscall.Kill(pid, SigUsr1) +} + +func SendSignalUser2(pid int) error { + return syscall.Kill(pid, SigUsr2) +} diff --git a/signal_windows_test.go b/signal_windows_test.go new file mode 100644 index 0000000..f2af89c --- /dev/null +++ b/signal_windows_test.go @@ -0,0 +1,22 @@ +//go:build windows +// +build windows + +package signal + +import ( + "errors" + "syscall" +) + +const ( + SigUsr1 = syscall.SIGHUP + SigUsr2 = syscall.SIGINT +) + +func SendSignalUser1(_ int) error { + return errors.New("testing on windows is not supported for now") +} + +func SendSignalUser2(_ int) error { + return errors.New("testing on windows is not supported for now") +}