This commit is contained in:
Diego Fernando Carrión 2023-10-30 12:18:16 +01:00
parent 5f8b8fd52b
commit e5c7fb19e6
No known key found for this signature in database
GPG Key ID: 084B635B53D777CB
5 changed files with 503 additions and 91 deletions

View File

@ -1,93 +1,3 @@
# exiter
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.com/CRThaze/exiter.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](https://gitlab.com/CRThaze/exiter/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
Go program clean exit handling.

117
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.go Normal file
View File

@ -0,0 +1,373 @@
package exiter
import (
"container/list"
"context"
"os"
"syscall"
"gitlab.com/CRThaze/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
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module gitlab.com/CRThaze/exiter
go 1.21.3
require (
gitlab.com/CRThaze/cworthy v0.0.4
gitlab.com/CRThaze/sigar v0.0.0-20231030111337-e540ad5f924d
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
gitlab.com/CRThaze/cworthy v0.0.4 h1:fvbM0UMdYxhHSE19GyIFuGly/hM9UT3Pw3E1urHKt1I=
gitlab.com/CRThaze/cworthy v0.0.4/go.mod h1:eaZtqJCLikd8gegTlbxaOvo8623mHhIoOySY00RfrZQ=
gitlab.com/CRThaze/sigar v0.0.0-20231030111337-e540ad5f924d h1:IFka+eC3Lg37om6B1HhLHbfQz4mH4lZLPWJoyjbb76I=
gitlab.com/CRThaze/sigar v0.0.0-20231030111337-e540ad5f924d/go.mod h1:mO4P1lmhXLqZabOJqebyTj2cYE3ul2r8l4EKoSnvVCI=