Compare commits

..

5 Commits
exit ... main

10 changed files with 92 additions and 631 deletions

13
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,13 @@
# You can override the included template(s) by including variable overrides
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
# Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
# Note that environment variables can be set in several places
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
sast:
stage: test
include:
- template: Security/SAST.gitlab-ci.yml

View File

@ -1,7 +1,7 @@
[![GoDoc](http://godoc.org/gitlab.com/CRThaze/sugar?status.svg)](http://godoc.org/gitlab.com/CRThaze/sugar)
# sugar # sugar
[![GoDoc](http://godoc.org/gitlab.com/CRThaze/sugar?status.svg)](http://godoc.org/gitlab.com/CRThaze/sugar)
## Syntactic Sugar for Golang ## Syntactic Sugar for Golang
This package provides some convenience functions that help keep code clean. This package provides some convenience functions that help keep code clean.

51
errors.go Normal file
View File

@ -0,0 +1,51 @@
package sugar
import (
"fmt"
"os"
)
var (
ErrorWriter = os.Stderr
ErrorPrefix = "Error:"
ErrorFmt = "%s %+v"
StandardExitCode = 1
PanicFunc = func(err error) {
panic(err)
}
ErrorPrintFunc = func(err error) {
fmt.Fprintf(ErrorWriter, ErrorFmt, ErrorPrefix, err)
}
ExitFunc = func(code int) {
os.Exit(code)
}
)
// Check takes an error and prints it if it is not nil; but will continue.
func Check(err error) {
if err != nil {
ErrorPrintFunc(err)
}
}
// CheckPanic takes an error and will panic if it is not nil.
func CheckPanic(err error) {
if err != nil {
PanicFunc(err)
}
}
// CheckExit takes an error and will print it and exit if it is not nil.
// If no exit code is provided, the StandardExitCode will be used.
// Any exit codes beyond the first one one provided will be discarded.
func CheckExit(err error, code ...int) {
if err != nil {
ErrorPrintFunc(err)
exitCode := StandardExitCode
if len(code) > 0 {
exitCode = code[0]
}
ExitFunc(exitCode)
}
}

View File

@ -1,117 +0,0 @@
package exiter
import (
"errors"
"gitlab.com/CRThaze/cworthy/exit"
)
// ExitCode represents an unsigned 16 bit integer used to define exit statuses.
type ExitCode exit.ExitT
const (
ExitOK ExitCode = ExitCode(exit.EX_OK) // 0: successful termination
ExitError ExitCode = ExitCode(1) // 1: generic error
ExitUsage ExitCode = ExitCode(exit.EX_USAGE) // 64: command line usage error
ExitDataErr ExitCode = ExitCode(exit.EX_DATAERR) // 65: data format error
ExitNoInput ExitCode = ExitCode(exit.EX_NOINPUT) // 66: cannot open input
ExitNoUser ExitCode = ExitCode(exit.EX_NOUSER) // 67: addressee unknown
ExitNoHost ExitCode = ExitCode(exit.EX_NOHOST) // 68: host name unknown
ExitUnavailable ExitCode = ExitCode(exit.EX_UNAVAILABLE) // 69: service unavailable
ExitSoftware ExitCode = ExitCode(exit.EX_SOFTWARE) // 70: internal software error
ExitOSErr ExitCode = ExitCode(exit.EX_OSERR) // 71: system error (e.g., can't fork)
ExitOSFile ExitCode = ExitCode(exit.EX_OSFILE) // 72: critical OS file missing
ExitCantCreate ExitCode = ExitCode(exit.EX_CANTCREAT) // 73: can't create (user) output file
ExitIOErr ExitCode = ExitCode(exit.EX_IOERR) // 74: input/output error
ExitTempFail ExitCode = ExitCode(exit.EX_TEMPFAIL) // 75: temp failure; user is invited to retry
ExitProtocol ExitCode = ExitCode(exit.EX_PROTOCOL) // 76: remote error in protocol
ExitNoPerm ExitCode = ExitCode(exit.EX_NOPERM) // 77: permission denied
ExitConfig ExitCode = ExitCode(exit.EX_CONFIG) // 78: configuration error
ExitAbort ExitCode = ExitCode(exit.EX_ABORT) // 134: process was aborted
)
var exitCodeDescs [^ExitCode(0)]string
func init() {
exitCodeDescs = exit.GetAllExitCodeDescriptions()
exitCodeDescs[ExitError] = "generic error"
}
// Desc returns the description string for a given ExitCode.
func (e ExitCode) Desc() string {
return exitCodeDescs[e]
}
// RegisterNewExitCode reserves an ExitCode and stores its corresponding
// description string for recall.
// It returns an error if the given integer is already in use or outside the
// usable range. And also if the description string is empty.
func RegisterNewExitCode(num int, desc string) (ExitCode, error) {
if num < 0 || num > int(^ExitCode(0)) {
return 0, errors.New("New exit code out of range")
}
if num == 0 || num == int(exit.EX_ABORT) || (num >= int(exit.EX__BASE) && num <= int(exit.EX__MAX)) {
return 0, errors.New("New exit code not user reservable")
}
if exitCodeDescs[num] != "" {
return 0, errors.New("New exit code already defined")
}
if desc == "" {
return 0, errors.New("New exit code description cannot be blank")
}
exitCodeDescs[num] = desc
return ExitCode(num), nil
}
// RegisterNextExitCode find the next available ExitCode number and associates the
// given description string with it.
// It returns an error if there are no more available numbers to reserve.
func RegisterNextExitCode(desc string) (ExitCode, error) {
nextUsable := func(n int) int {
n++
for n == int(exit.EX_ABORT) || (n >= int(exit.EX__BASE) && n <= int(exit.EX__MAX)) {
n++
}
return n
}
for i := 1; i <= int(^ExitCode(0)); i = nextUsable(i) {
if exitCodeDescs[i] == "" {
exitCodeDescs[i] = desc
return ExitCode(i), nil
}
}
return 0, errors.New("Available exit codes exhausted")
}
func mapCodes(codes []string, offset int, m map[ExitCode]string) {
for i, desc := range codes {
if desc != "" {
m[ExitCode(i+offset)] = desc
}
}
}
// UserCodeMap returns a map of ExitCodes and their associated description strings
// with in the User-Definable range.
func UserCodeMap() (m map[ExitCode]string) {
m = map[ExitCode]string{}
mapCodes(exitCodeDescs[1:exit.EX__BASE], 1, m)
mapCodes(exitCodeDescs[exit.EX__MAX+1:exit.EX_ABORT], int(exit.EX__MAX)+1, m)
mapCodes(exitCodeDescs[exit.EX_ABORT+1:], int(exit.EX_ABORT)+1, m)
return
}
// FullCodeMap returns a map of all ExitCodes and their associated description
// strings.
func FullCodeMap() (m map[ExitCode]string) {
m = map[ExitCode]string{}
mapCodes(exitCodeDescs[:], 0, m)
return
}
// GetAllExitCodeDescriptions returns an array of all possible ExitCode description
// strings (where the index is the exit code). Any unreserved ExitCodes are also
// included with empty string descriptions.
func GetAllExitCodeDescriptions() [^ExitCode(0)]string {
return exitCodeDescs
}

View File

@ -1,373 +0,0 @@
package exiter
import (
"container/list"
"context"
"os"
"syscall"
"gitlab.com/CRThaze/sugar/sigar"
)
const (
exitSignalGroupName = "exitSignalGroup"
abortSignalGroupName = "abortSignalGroup"
)
// ExitTaskFn (`func()`) is a closure represening something that should be
// executed right before exiting.
type ExitTaskFn sigar.TaskFn
// WriteFn is a format function that will write output somewhere.
// This could be a custom function or simply a logger function pointer.
type WriteFn func(format string, a ...interface{})
// ExiterConfig contains options for the creation of an Exiter.
type ExiterConfig struct {
ParentCtx context.Context
ExitSignals []os.Signal
SuppressAllExitMsg bool
ShowOKExitMsg bool
}
// Exiter is a tool for cleanly and consistently exiting the program.
// Instead of creating the struct directly, use the NewExiter() or
// NewExiterWithConfig() factories.
type Exiter struct {
ctx context.Context
cancel context.CancelFunc
sigar *sigar.SignalCatcher
preCancelTasks *list.List
postCancelTasks *list.List
usagePrintFunc ExitTaskFn
exitInfoMsgWriteFunc WriteFn
exitErrMsgWriteFunc WriteFn
suppressAllExitMsg bool
showOKExitMsg bool
}
// DefaultExiterConfig is the ExiterConfig used when calling the
// NewExiter() factory.
var DefaultExiterConfig = ExiterConfig{
ParentCtx: context.Background(),
ExitSignals: []os.Signal{
syscall.SIGINT,
syscall.SIGTERM,
},
}
// NewExiterWithConfig is an Exiter factory that allows you to
// customize the configuration.
// Be sure to call (Exiter).ListenForSignals() once created to handle
// all exits properly.
func NewExiterWithConfig(config ExiterConfig) (e *Exiter) {
e = &Exiter{
suppressAllExitMsg: config.SuppressAllExitMsg,
showOKExitMsg: config.ShowOKExitMsg,
}
e.ctx, e.cancel = context.WithCancel(config.ParentCtx)
e.sigar = sigar.NewSignalCatcher(
sigar.SignalGroups{
exitSignalGroupName: config.ExitSignals,
abortSignalGroupName: []os.Signal{syscall.SIGABRT},
},
e.ctx,
)
e.sigar.RegisterTask(exitSignalGroupName, func() {
e.ExitWithOK()
})
e.sigar.RegisterTask(abortSignalGroupName, func() {
e.Exit(ExitAbort)
})
return
}
// NewExiter is an Exiter factory that uses the DefaultExiterConfig.
// Be sure to call (Exiter).ListenForSignals() once created to handle
// all exits properly.
func NewExiter() *Exiter { return NewExiterWithConfig(DefaultExiterConfig) }
// SpawnExiter is a convenience function that calls both NewExiter()
// and (Exiter).ListenForSignals().
func SpawnExiter() (e *Exiter) {
e = NewExiter()
e.ListenForSignals()
return
}
// SpawnExiterWithConfig is a convenience function that calls both
// NewExiterWithConfig() and (Exiter).ListenForSignals().
func SpawnExiterWithConfig(config ExiterConfig) (e *Exiter) {
e = NewExiterWithConfig(config)
e.ListenForSignals()
return
}
// ListenForSignals spawns a goroutine to catch signals so that the
// Exiter can gracefully shutdown the program.
func (e *Exiter) ListenForSignals() {
e.sigar.Listen()
}
// RegisterPreTask adds a task to the queue that will be executed
// prior to unblocking goroutines blocked at (Exiter).Wait().
func (e *Exiter) RegisterPreTask(fn ExitTaskFn) {
e.preCancelTasks.PushBack(fn)
}
// RegisterPostTask adds a task to the queue that will be executed
// after unblocking goroutines blocked at (Exiter).Wait().
func (e *Exiter) RegisterPostTask(fn ExitTaskFn) {
e.postCancelTasks.PushBack(fn)
}
// SetUsageFunc tells the Exiter what function to call to print the
// program's help/usage information.
func (e *Exiter) SetUsageFunc(usage ExitTaskFn) {
e.usagePrintFunc = usage
}
// ExitOnPanic is a convenience function that is meant to be defered
// as a means to gracefully exit in the event of a panic.
func (e *Exiter) ExitOnPanic() {
if r := recover(); r != nil {
e.exitErrMsgWriteFunc("Paniced: %v", r)
e.ExitWithSoftwareErr()
}
}
func (e *Exiter) exit(code ExitCode) {
defer func() {
if r := recover(); r != nil {
e.exitErrMsgWriteFunc(
"Paniced attempting to cleanup tasks (with exit code: %d): %v", code, r,
)
os.Exit(int(ExitSoftware))
}
}()
for x := e.preCancelTasks.Front(); x != nil; x = x.Next() {
x.Value.(ExitTaskFn)()
}
e.cancel()
for x := e.postCancelTasks.Front(); x != nil; x = x.Next() {
x.Value.(ExitTaskFn)()
}
os.Exit(int(code))
}
func (e *Exiter) writeExitMsg(code ExitCode, fn WriteFn, err ...error) {
if !e.suppressAllExitMsg && fn != nil {
if code == ExitOK && !e.showOKExitMsg {
return
}
if len(err) != 0 && err[0] != nil {
fn("%s: %v", ExitSoftware.Desc(), err[0])
} else {
fn("%s", ExitSoftware.Desc())
}
}
}
// ExitWithOK calls (Exiter).Exit() with ExitOK (0).
//
// It is recommended to use this for all normal exit conditions.
func (e *Exiter) ExitWithOK() {
e.Exit(ExitOK)
}
// ExitWithUsageOK prints the help/usage and calls (Exiter).Exit() with
// ExitOK (0). If no UsageFunc was set, it will not print anything.
//
// It is recommended to use this when responding to help arguments.
func (e *Exiter) ExitWithUsageOK() {
e.RegisterPostTask(e.usagePrintFunc)
e.Exit(ExitOK)
}
// ExitWithError calls (Exiter).Exit() with ExitError (1).
//
// It is recommended to use this when no other exit code makes sense.
func (e *Exiter) ExitWithError(err ...error) {
e.Exit(ExitError, err...)
}
// ExitWithUsageErr prints the help/usage and calls (Exiter).Exit() with
// ExitUsage (64). If no UsageFunc was set, it will not print anything.
//
// It is recommended to use this when bad arguments are passed.
func (e *Exiter) ExitWithUsageErr(err ...error) {
e.Exit(ExitUsage, err...)
}
// ExitWithDataErr calls (Exiter).Exit() with ExitDataErr (65).
//
// It is recommended to use this when data provided through any means
// is invalid but NOT when an argument fails basic validation
// (though schema errors in complex arguments could be appropriate UNLESS
// they are configuartion settings, in which case you should use
// (Exiter).ExitWithConfigErr()).
func (e *Exiter) ExitWithDataErr(err ...error) {
e.Exit(ExitDataErr, err...)
}
// ExitWithNoInputErr calls (Exiter).Exit() with ExitNoInput (66).
//
// It is recommended to use this only when an input channel (such as
// a pipe, os.STDIN, or virtual device) cannot be opened or read when
// needed.
// Permissions errors should use (Exiter).ExitWithNoPermErr() instead.
func (e *Exiter) ExitWithNoInputErr(err ...error) {
e.Exit(ExitNoInput, err...)
}
// ExitWithNoUserErr calls (Exiter).Exit() with ExitNoUser (67).
//
// It is recommended to use this only when attempting to address data to
// a local or network user, and no suitable match can be found. Mostly
// useful for multi-user machines or intranet communication not using
// a protocol that has its own suitable statuses for this.
func (e *Exiter) ExitWithNoUserErr(err ...error) {
e.Exit(ExitNoUser, err...)
}
// ExitWithNoHostErr calls (Exiter).Exit() with ExitNoHost (68).
//
// It is recommended to use this when a provided hostname cannot be resolved.
func (e *Exiter) ExitWithNoHostErr(err ...error) {
e.Exit(ExitNoHost, err...)
}
// ExitWithUnavailableErr calls (Exiter).Exit() with ExitUnavailable (69) (nice).
//
// It is recommended to use this for client programs which cannot communicate
// with another process or remote service.
func (e *Exiter) ExitWithUnavailableErr(err ...error) {
e.Exit(ExitUnavailable, err...)
}
// ExitWithSoftwareErr calls (Exiter).Exit() with ExitSoftware (70).
//
// It is recommended to use this as a more meaningful alternative to
// ExitWithError() or for more graceful exits in the event of a Panic.
// For the later case consider deferring (Exiter).ExitOnPanic() which
// will call this function in that event.
func (e *Exiter) ExitWithSoftwareErr(err ...error) {
e.Exit(ExitSoftware, err...)
}
// ExitWithOSErr calls (Exiter).Exit() with ExitOSErr (71).
//
// It is recommended to use this only when unable to continue due to
// encountering a generalized system error.
func (e *Exiter) ExitWithOSErr(err ...error) {
e.Exit(ExitOSErr, err...)
}
// ExitWithOSFile calls (Exiter).Exit() with ExitOSFile (72).
//
// It is recommended to use this only when unable to find a file or directory
// not owned by this or any othe program but is part of the general
// system configuration. Such as a standard FHS directory, or a device file
// in /dev.
func (e *Exiter) ExitWithOSFile(err ...error) {
e.Exit(ExitOSFile, err...)
}
// ExitWithCantCreateErr calls (Exiter).Exit() with ExitCantCreate (73).
//
// It is recommended to use this only when the creation of a user output
// file is impossible for any reason and the program is unable to continue.
func (e *Exiter) ExitWithCantCreateErr(err ...error) {
e.Exit(ExitCantCreate, err...)
}
// ExitWithIOErr calls (Exiter).Exit() with ExitIOErr (74).
//
// It is recommended to use this only when unable to interact with I/O
// hardware, failure to open or write to an output stream, or when
// encountering an interuption from an already open input stream
// (otherwise use (Exiter).ExitWithNoInputErr()).
// Permissions errors should use (Exiter).ExitWithNoPermErr() instead.
func (e *Exiter) ExitWithIOErr(err ...error) {
e.Exit(ExitIOErr, err...)
}
// ExitWithTempFailErr calls (Exiter).Exit() with ExitTempFail (75).
//
// It is recommended to use this when the program chooses (by design) to
// terminate instead of retrying some operation that is expected to have
// failed merely intermittently.
func (e *Exiter) ExitWithTempFailErr(err ...error) {
e.Exit(ExitTempFail, err...)
}
// ExitWithProtocolErr calls (Exiter).Exit() with ExitProtocol (76).
//
// It is recommended to use this only when some communication with a
// process or network location fails due to a protocol violation from
// the other communication participant. And example would be receiving
// a response that is not inline with the protocol (like a SYN or ACK
// in response to the same instead of a SYN/ACK when performing a TCP
// handshake).
func (e *Exiter) ExitWithProtocolErr(err ...error) {
e.Exit(ExitTempFail, err...)
}
// ExitWithNoPermErr calls (Exiter).Exit() with ExitNoPerm (77).
//
// It is recommended to use this whenever user or credential Permissions
// do not allow you to continue.
func (e *Exiter) ExitWithNoPermErr(err ...error) {
e.Exit(ExitNoPerm, err...)
}
// ExitWithConfigErr calls (Exiter).Exit() with ExitConfig (78).
//
// It is recommended to use this whenever a schema validation is encountered
// in a config file or other serialiezed configuarion source.
func (e *Exiter) ExitWithConfigErr(err ...error) {
e.Exit(ExitConfig, err...)
}
// ExitWithConfigErr exits with the given ExitCode, and prints the
// first error provided (if any).
func (e *Exiter) Exit(code ExitCode, err ...error) {
switch code {
case ExitOK:
e.RegisterPostTask(func() {
e.writeExitMsg(code, e.exitInfoMsgWriteFunc)
})
e.exit(code)
case ExitUsage:
e.RegisterPostTask(func() {
e.writeExitMsg(code, e.exitErrMsgWriteFunc, err...)
})
e.RegisterPostTask(e.usagePrintFunc)
e.exit(code)
default:
e.RegisterPostTask(func() {
e.writeExitMsg(code, e.exitErrMsgWriteFunc, err...)
})
e.exit(code)
}
}
// Wait blocks till (Exiter).Exit() is called.
// It is recommended to call this function in a select block case in
// all goroutines to ensure they are properly terminated.
func (e *Exiter) Wait() {
<-e.ctx.Done()
}
// GetCancelableContext returns the underlying cancelable context
// used by the Exiter.
func (e *Exiter) GetCancelableContext() context.Context {
return e.ctx
}
// GetSignalCatcher returns the underlying *sigar.SignalCatcher
// used by the Exiter.
func (e *Exiter) GetSignalCatcher() *sigar.SignalCatcher {
return e.sigar
}

2
go.mod
View File

@ -1,5 +1,3 @@
module gitlab.com/CRThaze/sugar module gitlab.com/CRThaze/sugar
go 1.17 go 1.17
require gitlab.com/CRThaze/cworthy v0.0.4

12
go.sum
View File

@ -1,12 +0,0 @@
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/novalagung/go-eek v1.0.1/go.mod h1:Q9MQtLRP21rO5/7UmIJUr5URe1ysI+K/wM3zYJCxPxo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
gitlab.com/CRThaze/cworthy v0.0.4 h1:fvbM0UMdYxhHSE19GyIFuGly/hM9UT3Pw3E1urHKt1I=
gitlab.com/CRThaze/cworthy v0.0.4/go.mod h1:eaZtqJCLikd8gegTlbxaOvo8623mHhIoOySY00RfrZQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

View File

@ -1,11 +1,6 @@
// Package sugar provides convenience functions for keeping code clean and less repetitive. // Package sugar provides convenience functions for keeping code clean and less repetitive.
package sugar package sugar
import (
"fmt"
"os"
)
/* /*
* Literal Addressers * Literal Addressers
*/ */
@ -104,36 +99,3 @@ func StrPtr(v string) *string {
func BoolPtr(v bool) *bool { func BoolPtr(v bool) *bool {
return &v return &v
} }
/*
* Error Handling
*/
// Check takes an error and prints it if it is not nil; but will continue.
func Check(err error) {
if err != nil {
fmt.Printf("%+v", err)
}
}
// CheckLog takes an error and a format function for leveraging a custom printer like a logger.
func CheckLog(err error, fn func(format string, a ...interface{})) {
if err != nil {
fn("%+v", err)
}
}
// CheckPanic takes an error and will panic if it is not nil.
func CheckPanic(err error) {
if err != nil {
panic(err)
}
}
// CheckExit takes an error and will print it and exit with status 1 if it is not nil.
func CheckExit(err error) {
if err != nil {
fmt.Printf("%+v", err)
os.Exit(1)
}
}

26
sets.go Normal file
View File

@ -0,0 +1,26 @@
package sugar
// StrSet a set type for strings.
type StrSet map[string]struct{}
// Add adds a new key to the set if it does not already exist.
func (s *StrSet) Add(str string) {
(*s)[str] = struct{}{}
}
// Has checks if the given string is present in the set.
func (s *StrSet) Has(str string) bool {
if _, ok := (*s)[str]; ok {
return true
}
return false
}
// AddHas behaves like Add, but returns true if the key already existed.
func (s *StrSet) AddHas(str string) bool {
if ok := s.Has(str); ok {
return ok
}
s.Add(str)
return false
}

View File

@ -1,87 +0,0 @@
package sigar
import (
"container/list"
"context"
"errors"
"os"
"os/signal"
)
type taskMap map[string]*list.List
type SignalGroups map[string][]os.Signal
func (sg SignalGroups) initTaskMap(tm taskMap) {
for groupName := range sg {
tm[groupName] = list.New()
}
}
type TaskFn func()
type SignalCatcher struct {
signals SignalGroups
tasks map[string]*list.List
exitCtx context.Context
}
func NewSignalCatcher(sg SignalGroups, exitCtx context.Context) (sc *SignalCatcher) {
if sg == nil {
sg = SignalGroups{}
}
sc = &SignalCatcher{
signals: sg,
exitCtx: exitCtx,
tasks: taskMap{},
}
sg.initTaskMap(sc.tasks)
if exitCtx == nil {
// Create a cancelable context and discard the cancel function, as we will never use it.
sc.exitCtx, _ = context.WithCancel(context.Background())
}
return
}
func SpawnSignalCatcher(sg SignalGroups, exitCtx context.Context) (sc *SignalCatcher) {
sc = NewSignalCatcher(sg, exitCtx)
sc.Listen()
return
}
func (sc *SignalCatcher) runTasks(group string) {
taskList := sc.tasks[group]
for e := taskList.Front(); e != nil; e = e.Next() {
e.Value.(TaskFn)()
}
}
func (sc *SignalCatcher) Listen() {
for groupName, signalSet := range sc.signals {
// Prevent loop closure by copying variables.
group := groupName
signals := signalSet
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, signals...)
select {
case <-sigChan:
sc.runTasks(group)
case <-sc.exitCtx.Done():
}
}()
}
}
func (sc *SignalCatcher) RegisterTaskGroup(group string, signals []os.Signal) {
sc.signals[group] = signals
sc.tasks[group] = list.New()
}
func (sc *SignalCatcher) RegisterTask(group string, fn TaskFn) error {
if _, ok := sc.tasks[group]; !ok {
return errors.New("No such task group")
}
sc.tasks[group].PushBack(fn)
return nil
}