Compare commits

...

2 Commits
main ... exit

5 changed files with 591 additions and 0 deletions

117
exiter/exitCodes.go Normal file
View File

@ -0,0 +1,117 @@
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
}

373
exiter/exiter.go Normal file
View File

@ -0,0 +1,373 @@
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,3 +1,5 @@
module gitlab.com/CRThaze/sugar
go 1.17
require gitlab.com/CRThaze/cworthy v0.0.4

12
go.sum
View File

@ -0,0 +1,12 @@
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=

87
sigar/signalHandler.go Normal file
View File

@ -0,0 +1,87 @@
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
}