From 8cf685c73e37c718d1dcfe057caad080196f3e18 Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Sat, 13 Jun 2020 17:12:54 +0200 Subject: [PATCH] =?utf8?q?Remove=20=E2=80=98Signal=E2=80=99=20from=20struc?= =?utf8?q?ture=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit The package is already named ‘signalcontext’, there’s no reason to further scope `SignalError` or `SignalContext`. Split error code and tests into a separate file. --- context.go | 105 ++++++++++++++++++++ signalcontext_test.go => context_test.go | 12 +-- error.go | 20 ++++ error_test.go | 13 +++ signalcontext.go | 121 ----------------------- 5 files changed, 143 insertions(+), 128 deletions(-) create mode 100644 context.go rename signalcontext_test.go => context_test.go (89%) create mode 100644 error.go create mode 100644 error_test.go delete mode 100644 signalcontext.go diff --git a/context.go b/context.go new file mode 100644 index 0000000..1bd24d3 --- /dev/null +++ b/context.go @@ -0,0 +1,105 @@ +package signalcontext + +import ( + "context" + "os" + "os/signal" + "sync" + "time" +) + +// A Context is an implementation of the context.Context interface which +// completes when a signal (e.g. os.Interrupt) is received. +// +// Contexts should be created via the UntilSignal function. +type Context struct { + parent context.Context + done chan struct{} + err error + + // The mutex synchronizes access to err and clearing the + // internal Signal channel after initialization. + m sync.Mutex + c chan os.Signal +} + +// UntilSignal returns a new Context which will complete when the parent +// does or when any of the specified signals are received. +func UntilSignal(parent context.Context, sig ...os.Signal) *Context { + ctx := new(Context) + ctx.parent = parent + ctx.done = make(chan struct{}) + + if err := parent.Err(); err != nil { + close(ctx.done) + ctx.err = err + return ctx + } + + ctx.c = make(chan os.Signal, 1) + signal.Notify(ctx.c, sig...) + go ctx.wait(sig...) + return ctx +} + +func (s *Context) wait(sig ...os.Signal) { + var err error + select { + case <-s.parent.Done(): + err = s.parent.Err() + case v := <-s.c: + if v != nil { + err = Error{v} + } + } + signal.Stop(s.c) + s.m.Lock() + if s.err == nil { + s.err = err + } + close(s.c) + s.c = nil + s.m.Unlock() + close(s.done) +} + +// Cancel cancels this context, if it hasn’t already been completed. (If +// it has, this is safe but has no effect.) +func (s *Context) Cancel() { + s.m.Lock() + if s.c != nil { + s.err = context.Canceled + select { + case s.c <- nil: + default: + } + } + s.m.Unlock() +} + +// Deadline implements context.Context; a Context’s deadline is that of +// its parent. +func (s *Context) Deadline() (time.Time, bool) { + return s.parent.Deadline() +} + +// Value implements context.Context; any value is that of its parent. +func (s *Context) Value(key interface{}) interface{} { + return s.parent.Value(key) +} + +// Done implements context.Context. +func (s *Context) Done() <-chan struct{} { + return s.done +} + +// Err implements context.Context; it returns context.Canceled if the +// context was canceled; an Error if the context completed due to a +// signal; the parent’s error if the parent was done before either of +// those; or nil if the context is not yet done. +func (s *Context) Err() error { + s.m.Lock() + err := s.err + s.m.Unlock() + return err +} diff --git a/signalcontext_test.go b/context_test.go similarity index 89% rename from signalcontext_test.go rename to context_test.go index 07109c5..a7b30a4 100644 --- a/signalcontext_test.go +++ b/context_test.go @@ -14,9 +14,7 @@ func TestReceivesSignal(t *testing.T) { assert.NoError(t, ctx.Err()) syscall.Kill(syscall.Getpid(), syscall.SIGUSR2) <-ctx.Done() - assert.Equal(t, SignalError{syscall.SIGUSR2}, ctx.Err()) - assert.EqualError(t, ctx.Err(), - "received signal: "+syscall.SIGUSR2.String()) + assert.Equal(t, Error{syscall.SIGUSR2}, ctx.Err()) } func TestForwardsParent(t *testing.T) { @@ -40,8 +38,8 @@ func TestChildForwardsErr(t *testing.T) { <-child.Done() <-ctx.Done() cancel() - assert.Equal(t, SignalError{syscall.SIGUSR2}, ctx.Err()) - assert.Equal(t, SignalError{syscall.SIGUSR2}, child.Err()) + assert.Equal(t, Error{syscall.SIGUSR2}, ctx.Err()) + assert.Equal(t, Error{syscall.SIGUSR2}, child.Err()) } func TestSignalAfterCancel(t *testing.T) { @@ -60,10 +58,10 @@ func TestCancelAfterSignal(t *testing.T) { assert.NoError(t, ctx.Err()) syscall.Kill(syscall.Getpid(), syscall.SIGUSR2) <-ctx.Done() - assert.Equal(t, SignalError{syscall.SIGUSR2}, ctx.Err()) + assert.Equal(t, Error{syscall.SIGUSR2}, ctx.Err()) ctx.Cancel() time.Sleep(5 * time.Millisecond) - assert.Equal(t, SignalError{syscall.SIGUSR2}, ctx.Err()) + assert.Equal(t, Error{syscall.SIGUSR2}, ctx.Err()) } func TestImmediateCompletion(t *testing.T) { diff --git a/error.go b/error.go new file mode 100644 index 0000000..c0a4f97 --- /dev/null +++ b/error.go @@ -0,0 +1,20 @@ +package signalcontext + +import ( + "fmt" + "os" +) + +// A Error will be returned by a SignalContext’s Err() method when it +// was finished due to a signal (rather than e.g. parent cancellation). +type Error struct { + os.Signal +} + +func (e Error) Error() string { + return e.String() +} + +func (e Error) String() string { + return fmt.Sprintf("received signal: %s", e.Signal) +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..eb347ef --- /dev/null +++ b/error_test.go @@ -0,0 +1,13 @@ +package signalcontext + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestError(t *testing.T) { + assert.EqualError(t, Error{os.Interrupt}, + "received signal: "+os.Interrupt.String()) +} diff --git a/signalcontext.go b/signalcontext.go deleted file mode 100644 index d718e11..0000000 --- a/signalcontext.go +++ /dev/null @@ -1,121 +0,0 @@ -package signalcontext - -import ( - "context" - "fmt" - "os" - "os/signal" - "sync" - "time" -) - -// A SignalContext is an implementation of the context.Context interface -// which completes when a signal (e.g. os.Interrupt) is received. -// -// SignalContexts should be created via the UntilSignal function. -type SignalContext struct { - parent context.Context - done chan struct{} - err error - - // The mutex synchronizes access to err and clearing the - // internal Signal channel after initialization. - m sync.Mutex - c chan os.Signal -} - -// UntilSignal returns a new SignalContext which will complete when the -// parent does or when any of the specified signals are received. -func UntilSignal(parent context.Context, sig ...os.Signal) *SignalContext { - ctx := new(SignalContext) - ctx.parent = parent - ctx.done = make(chan struct{}) - - if err := parent.Err(); err != nil { - close(ctx.done) - ctx.err = err - return ctx - } - - ctx.c = make(chan os.Signal, 1) - signal.Notify(ctx.c, sig...) - go ctx.wait(sig...) - return ctx -} - -func (s *SignalContext) wait(sig ...os.Signal) { - var err error - select { - case <-s.parent.Done(): - err = s.parent.Err() - case v := <-s.c: - if v != nil { - err = SignalError{v} - } - } - signal.Stop(s.c) - s.m.Lock() - if s.err == nil { - s.err = err - } - close(s.c) - s.c = nil - s.m.Unlock() - close(s.done) -} - -// Cancel cancels this context, if it hasn’t already been canceled or -// received a signal. (If it has, this is safe but has no effect.) -func (s *SignalContext) Cancel() { - s.m.Lock() - if s.c != nil { - s.err = context.Canceled - select { - case s.c <- nil: - default: - } - } - s.m.Unlock() -} - -// Deadline implements context.Context; a SignalContext’s deadline is -// that of its parent. -func (s *SignalContext) Deadline() (time.Time, bool) { - return s.parent.Deadline() -} - -// Value implements context.Context; any value is that of its parent. -func (s *SignalContext) Value(key interface{}) interface{} { - return s.parent.Value(key) -} - -// Done implements context.Context. -func (s *SignalContext) Done() <-chan struct{} { - return s.done -} - -// Err implements context.Context; it returns context.Canceled if the -// context was canceled; a SignalError if the context completed due to a -// signal; the parent’s error if the parent was done before either of -// those; or nil if the context is not yet done. -func (s *SignalContext) Err() error { - s.m.Lock() - err := s.err - s.m.Unlock() - return err -} - -// A SignalError will be returned by a SignalContext’s Err() method when -// it was finished due to a signal (rather than e.g. parent -// cancellation). -type SignalError struct { - os.Signal -} - -func (e SignalError) Error() string { - return e.String() -} - -func (e SignalError) String() string { - return fmt.Sprintf("received signal: %s", e.Signal) -} -- 2.20.1