ctrl

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2021 License: MIT Imports: 5 Imported by: 3

README

ctrl: Non-local exit handling for Go main packages

In case of error, Go main packages typically call os.Exit or log.Fatal. This causes the process to terminate immediately, and deferred calls are not invoked. Calling log.Panicallows deferred calls to run, but leaves a noisy log trace.

the ctrl package provides a Run function that performs the main action of a program. Within its dynamic extent, calls to ctrl.Exit and ctrl.Exitf will panic back to Run, which will handle logging and exiting from the process as specified.

Usage example

The following code outlines the use of ctrl.Run:

import "github.com/creachadair/ctrl"

// A stub main to set up the control handlers.
func main() {
  // The default flagset hard-exits on error. You could set it to panic
  // on error instead, if you want to parse inside realMain.
  flag.Parse()

  ctrl.Run(realMain)

  // if realMain returns nil, control returns here.
  // if realMain returns a non-nil error or panics, this is not reached.
}

// The real program logic goes into this function.
func realMain() error {
  defer cleanup()  // some deferred cleanup task

  if err := doSomething(); err != nil {
     return err                          // failure, exit 1, no log
  } else if !stateIsvalid() {
     ctrl.Exitf(2, "State is invalid")   // failure, exit 2 with log
  }
  return nil                             // success
}

Documentation

Overview

Package ctrl manages passage of control through a main function.

In case of error main programs typically call os.Exit or log.Fatal. However, this causes the process to terminate immediately and deferred calls are not invoked. Calling log.Panic allows deferred calls to run, but makes a noisy log trace.

This package provides a Run function that performs the main action of a program. Within its dynamic extent, calls to ctrl.Exit and ctrl.Exitf will panic back to Run, which will handle logging and exiting from the process as specified.

Example:

import "github.com/creachadair/ctrl"

// A stub main to set up the control handlers.
func main() { ctrl.Run(realMain) }

// The real program logic goes into this function.
func realMain() error { ... }

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Exit

func Exit(code int) error

Exit returns control to the most recent invocation of Run, instructing it to exit the process silently with the specified exit status.

Control does not return from a call to Exit. The return type is error so that it can be called in an error return statement.

Example
package main

import (
	"fmt"

	"github.com/creachadair/ctrl"
)

func catchPanic(f func()) (val interface{}) {

	ctrl.SetPanic(true)

	ctrl.SetHook(func(code int, err error) {
		if code != 0 || err != nil {
			fmt.Printf("[exit] code=%d err=%v\n", code, err)
		}
	})

	defer func() { val = recover() }()
	f()
	return
}

func main() {
	// N.B. catchPanic prevents ctrl.Run from terminating the example
	// program. You do not need this in production.
	catchPanic(func() {
		ctrl.Run(func() error {
			fmt.Println("Hello")
			return ctrl.Exit(0)
		})
		fmt.Println("You don't see this")
	})
}
Output:

Hello

func Exitf

func Exitf(code int, msg string, args ...interface{}) error

Exitf returns control to the most recent invocation of Run, instructing it to exit the process with the specified exit status and formatted log.

Control does not return from a call to Exitf. The return type is error so that it can be called in an error return statement.

Example
package main

import (
	"fmt"

	"github.com/creachadair/ctrl"
)

func catchPanic(f func()) (val interface{}) {

	ctrl.SetPanic(true)

	ctrl.SetHook(func(code int, err error) {
		if code != 0 || err != nil {
			fmt.Printf("[exit] code=%d err=%v\n", code, err)
		}
	})

	defer func() { val = recover() }()
	f()
	return
}

func main() {
	// N.B. catchPanic prevents ctrl.Run from terminating the example
	// program. You do not need this in production.
	catchPanic(func() {
		ctrl.Run(func() error {
			fmt.Println("Hello")
			return ctrl.Exitf(5, "everything is bad")
		})
		fmt.Println("You don't see this")
	})
}
Output:

Hello
[exit] code=5 err=everything is bad

func Fatalf

func Fatalf(msg string, args ...interface{}) error

Fatalf is a shorthand for ctrl.Exitf(1, ...), that can be used as a drop-in replacement for calls to log.Fatalf.

Example
package main

import (
	"fmt"

	"github.com/creachadair/ctrl"
)

func catchPanic(f func()) (val interface{}) {

	ctrl.SetPanic(true)

	ctrl.SetHook(func(code int, err error) {
		if code != 0 || err != nil {
			fmt.Printf("[exit] code=%d err=%v\n", code, err)
		}
	})

	defer func() { val = recover() }()
	f()
	return
}

func main() {
	// N.B. catchPanic prevents ctrl.Run from terminating the example
	// program. You do not need this in production.
	catchPanic(func() {
		ctrl.Run(func() error {
			fmt.Println("Hello")
			return ctrl.Fatalf("badness: %d", 25)
		})
		fmt.Println("You don't see this")
	})
}
Output:

Hello
[exit] code=1 err=badness: 25

func Run

func Run(main func() error)

Run invokes main. If main returns without error, control returns from Run normally. If main reports an error, it is logged and Run calls os.Exit(1).

During the execution of main a call to ctrl.Exit or ctrl.Exitf returns control to Run, which logs a message and calls os.Exit with the specified return code. Control is handled via the panic mechanism, so deferred calls within main will be executed normally. Panics not generated by this library are propagated normally.

Example (Failure)
package main

import (
	"errors"
	"fmt"

	"github.com/creachadair/ctrl"
)

func catchPanic(f func()) (val interface{}) {

	ctrl.SetPanic(true)

	ctrl.SetHook(func(code int, err error) {
		if code != 0 || err != nil {
			fmt.Printf("[exit] code=%d err=%v\n", code, err)
		}
	})

	defer func() { val = recover() }()
	f()
	return
}

func main() {
	// N.B. catchPanic prevents ctrl.Run from terminating the example
	// program. You do not need this in production.
	catchPanic(func() {
		ctrl.Run(func() error {
			fmt.Println("Hello")
			return errors.New("goodbye")
		})
		fmt.Println("You do not see this")
	})
}
Output:

Hello
[exit] code=1 err=goodbye
Example (Panic)
package main

import (
	"fmt"

	"github.com/creachadair/ctrl"
)

func catchPanic(f func()) (val interface{}) {

	ctrl.SetPanic(true)

	ctrl.SetHook(func(code int, err error) {
		if code != 0 || err != nil {
			fmt.Printf("[exit] code=%d err=%v\n", code, err)
		}
	})

	defer func() { val = recover() }()
	f()
	return
}

func main() {
	// N.B. catchPanic prevents ctrl.Run from terminating the example
	// program. You do not need this in production.
	v := catchPanic(func() {
		ctrl.Run(func() error {
			fmt.Println("Hello")
			panic("omgwtfbbq")
		})
		fmt.Println("You do not see this")
	})
	fmt.Println("panic:", v)
}
Output:

Hello
panic: omgwtfbbq
Example (Success)
package main

import (
	"fmt"

	"github.com/creachadair/ctrl"
)

func main() {
	ctrl.Run(func() error {
		fmt.Println("This is main")
		return nil
	})
	fmt.Println("That was main")
}
Output:

This is main
That was main

func SetHook

func SetHook(f func(code int, err error))

SetHook sets f as an exit hook.

Before Run exits the program, it will call f synchronously with the code and error that motivated the exit. Once the hook returns, Run will exit.

If f == nil, the hook is removed. There is only one exit hook; subsequent calls to SetHook will replace any previous values.

func SetPanic

func SetPanic(panicToExit bool)

SetPanic sets whether the program should exit by panicking (true) or by calling os.Exit (false). The default is false.

This is mainly intended to support testing.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL