Skip to content

Commit

Permalink
first go-signal
Browse files Browse the repository at this point in the history
  • Loading branch information
wjiec committed Dec 13, 2021
1 parent 2abe32c commit 4ad5470
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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).
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
118 changes: 118 additions & 0 deletions signal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2021 Jayson Wang <[email protected]>
*
* 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 &notifier{
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 &notifier{
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
}
75 changes: 75 additions & 0 deletions signal_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
21 changes: 21 additions & 0 deletions signal_unix_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
22 changes: 22 additions & 0 deletions signal_windows_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

0 comments on commit 4ad5470

Please sign in to comment.