diff --git a/exiter/exitCodes.go b/exiter/exitCodes.go new file mode 100644 index 0000000..b64c6a1 --- /dev/null +++ b/exiter/exitCodes.go @@ -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 +} diff --git a/exiter/exiter.go b/exiter/exiter.go index 7178e50..beb9b3e 100644 --- a/exiter/exiter.go +++ b/exiter/exiter.go @@ -9,68 +9,143 @@ import ( "gitlab.com/CRThaze/sugar/sigar" ) -const exitSignalGroupName = "exitSignalGroup" +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 + 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 + 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, - syscall.SIGABRT, }, } +// 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{} + 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, + exitSignalGroupName: config.ExitSignals, + abortSignalGroupName: []os.Signal{syscall.SIGABRT}, }, e.ctx, ) e.sigar.RegisterTask(exitSignalGroupName, func() { - e.Exit(0) + 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) } -func (e *Exiter) Exit(code ...int) { +// 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)() } @@ -78,20 +153,221 @@ func (e *Exiter) Exit(code ...int) { for x := e.postCancelTasks.Front(); x != nil; x = x.Next() { x.Value.(ExitTaskFn)() } - if len(code) == 0 { - code = make([]int, 1, 1) - } - os.Exit(code[0]) + 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 } diff --git a/go.mod b/go.mod index 099ab01..99b253e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module gitlab.com/CRThaze/sugar go 1.17 + +require gitlab.com/CRThaze/cworthy v0.0.4 diff --git a/go.sum b/go.sum index e69de29..8024996 100644 --- a/go.sum +++ b/go.sum @@ -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=