github.com/haraldrudell/parl@v0.4.176/mains/executable.go (about)

     1  /*
     2  © 2020–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  // Package mains contains functions for implementing a service or command-line utility
     7  package mains
     8  
     9  import (
    10  	"errors"
    11  	"flag"
    12  	"fmt"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  	"sync/atomic"
    17  	"time"
    18  
    19  	"github.com/haraldrudell/parl"
    20  	"github.com/haraldrudell/parl/perrors"
    21  	"github.com/haraldrudell/parl/perrors/errorglue"
    22  	"github.com/haraldrudell/parl/pflags"
    23  	"github.com/haraldrudell/parl/plog"
    24  	"github.com/haraldrudell/parl/pos"
    25  	"github.com/haraldrudell/parl/pruntime"
    26  	"github.com/haraldrudell/parl/pstrings"
    27  )
    28  
    29  const (
    30  	// if [Executable.OKtext] is assigned NoOK there ir no successful message on app exit
    31  	NoOK = "-"
    32  	// displays error location for errors printed without stack trace
    33  	//	- second argument to [Executable.LongErrors]
    34  	OutputErrorLocationTrue = true
    35  	// error location is not appended to errors printed without stack trace
    36  	//	- second argument to [Executable.LongErrors]
    37  	NoErrorLocationTrue = false
    38  	// always output error stack traces
    39  	//	- first argument to [Executable.LongErrors]
    40  	AlwaysStackTrace = true
    41  	// stack traces are not output for errors
    42  	//	- stack traces are printed for panic
    43  	//	- first argument to [Executable.LongErrors]
    44  	NoStackTrace = false
    45  )
    46  
    47  const (
    48  	rfcTimeFormat = "2006-01-02 15:04:05-07:00"
    49  	usageHeader   = "Usage:"
    50  	optionsSyntax = "[options…]"
    51  	helpHelp      = "\x20\x20-help -h --help\n\x20\x20\tShows this help"
    52  	timeHeader    = "time: %s"
    53  	hostHeader    = "host: %s"
    54  	defaultOK     = "completed successfully"
    55  	// count (Recover or EarlyPanic) and doPanicFrames
    56  	//	- must have at least one of the two, so use 1
    57  	doPanicFrames = 1
    58  )
    59  
    60  const (
    61  	// NoArguments besides switches, zero trailing arguments is allowed
    62  	NoArguments = 1 << iota
    63  	// OneArgument besides switches, exactly one trailing arguments is allowed
    64  	OneArgument
    65  	// ManyArguments besides switches, one or more trailing arguments is allowed
    66  	ManyArguments
    67  )
    68  
    69  // ArgumentSpec bitfield for 0, 1, many arguments following command-line switches
    70  type ArgumentSpec uint32
    71  
    72  // Executable constant strings that describes an executable
    73  // advisable static values include Program Version Comment Description Copyright License Arguments
    74  // like:
    75  //
    76  //	var ex = mains.Executable{
    77  //	  Program:     "getip",
    78  //	  Version:     "0.0.1",
    79  //	  Comment:     "first version",
    80  //	  Description: "finds ip address for hostname",
    81  //	  Copyright:   "© 2020-present Harald Rudell <harald.rudell@gmail.com> (http://www.haraldrudell.com)",
    82  //	  License:     "All rights reserved",
    83  //	  Arguments:   mains.NoArguments | mains.OneArgument,
    84  //	}
    85  type Executable struct {
    86  
    87  	// fields typically statically assigned in main
    88  
    89  	Program        string       // “gonet”
    90  	Version        string       // “0.0.1”
    91  	Comment        string       // [ banner text after program and version] “options parsing” changes in last version
    92  	Description    string       // [Description part of usage] “configures firewall and routing”
    93  	Copyright      string       // “© 2020…”
    94  	License        string       // “ISC License”
    95  	OKtext         string       // “Completed successfully”
    96  	ArgumentsUsage string       // usage help text for arguments after options
    97  	Arguments      ArgumentSpec // eg. mains.NoArguments
    98  
    99  	// fields below popualted by .Init()
   100  
   101  	Launch       time.Time // process start time
   102  	LaunchString string    // Launch as printable rfc 3339 time string
   103  	Host         string    // short hostname, ie. no dots “mymac”
   104  
   105  	// additional fields
   106  
   107  	// errors addded by AddErr and other methods
   108  	//	- because an error added may have associated errors,
   109  	//		err must be a slice, to distinguish indivdual error adds
   110  	//	- that slice must be thread-safe
   111  	err      errStore
   112  	ArgCount int      // number of post-options strings during parse
   113  	Arg      string   // if one post-options string and that is allowed, this is the string
   114  	Args     []string // any post-options strings if allowed
   115  	// errors are printed with stack traces, associated values and errors
   116  	//	- panics are always printed long
   117  	//	- if errors long or more than 1 error, the first error is repeated last as a one-liner
   118  	IsLongErrors bool
   119  	// adds a code location to errors if not IsLongErrors
   120  	IsErrorLocation bool
   121  	// optionsWereParsed signals that parsing completed without panic
   122  	optionsWereParsed atomic.Bool
   123  }
   124  
   125  // Init initializes a created [mains.Executable] value
   126  //   - the value should have relevant fields populates such as exeuctable name and more
   127  //   - — Program Version Comment Copyright License Arguments
   128  //   - populates launch time and sets silence if first os.Args argument is “-silent.”
   129  //   - Init supports function chaining like:
   130  //
   131  // typical code:
   132  //
   133  //	ex.Init().
   134  //	  PrintBannerAndParseOptions(optionData).
   135  //	  LongErrors(options.Debug, options.Verbosity != "").
   136  //	  ConfigureLog()
   137  //	applyYaml(options.YamlFile, options.YamlKey, applyYaml, optionData)
   138  //	…
   139  func (x *Executable) Init() (ex2 *Executable) {
   140  	ex2 = x
   141  	var now = ProcessStartTime()
   142  	x.Launch = now
   143  	x.LaunchString = now.Format(rfcTimeFormat)
   144  	x.Host = pos.ShortHostname()
   145  	if len(os.Args) > 1 && os.Args[1] == SilentString {
   146  		parl.SetSilent(true)
   147  	}
   148  	return
   149  }
   150  
   151  // LongErrors sets if errors are printed with stack trace and values. LongErrors
   152  // supports functional chaining:
   153  //
   154  //	exe.Init().
   155  //	  …
   156  //	  LongErrors(options.Debug, options.Verbosity != "").
   157  //	  ConfigureLog()…
   158  //
   159  // isLongErrors prints full stack traces, related errors and error data in string
   160  // lists and string maps.
   161  //
   162  // isErrorLocation appends the innermost location to the error message when isLongErrors
   163  // is not set:
   164  //
   165  //	error-message at error116.(*csTypeName).FuncName-chainstring_test.go:26
   166  func (x *Executable) LongErrors(isLongErrors bool, isErrorLocation bool) *Executable {
   167  	parl.Debug("exe.LongErrors long: %t location: %t", isLongErrors, isErrorLocation)
   168  	x.IsLongErrors = isLongErrors
   169  	x.IsErrorLocation = isErrorLocation
   170  	return x
   171  }
   172  
   173  // PrintBannerAndParseOptions prints greeting like:
   174  //
   175  //	parl 0.1.0 parlca https server/client udp server
   176  //
   177  // It then parses options described by []OptionData stroing the values at OptionData.P.
   178  // If options fail to parse, a proper message is printed to stderr and the process exits
   179  // with status code 2. PrintBannerAndParseOptions supports functional chaining like:
   180  //
   181  //	exe.Init().
   182  //	  PrintBannerAndParseOptions(…).
   183  //	  LongErrors(…
   184  //
   185  // Options and yaml is configured likeso:
   186  //
   187  //	var options = &struct {
   188  //	  noStdin bool
   189  //	  *mains.BaseOptionsType
   190  //	}{BaseOptionsType: &mains.BaseOptions}
   191  //	var optionData = append(mains.BaseOptionData(exe.Program, mains.YamlYes), []mains.OptionData{
   192  //	  {P: &options.noStdin, Name: "no-stdin", Value: false, Usage: "Service: do not use standard input", Y: mains.NewYamlValue(&y, &y.NoStdin)},
   193  //	}...)
   194  //	type YamlData struct {
   195  //	  NoStdin bool // nostdin: true
   196  //	}
   197  //	var y YamlData
   198  func (x *Executable) PrintBannerAndParseOptions(optionsList []pflags.OptionData) (ex1 *Executable) {
   199  	ex1 = x
   200  
   201  	// print program name and populated details
   202  	var banner = pstrings.FilteredJoin([]string{
   203  		pstrings.FilteredJoinWithHeading([]string{
   204  			"", x.Program,
   205  			"version", x.Version,
   206  			"comment", x.Comment,
   207  		}, "\x20"),
   208  		x.Copyright,
   209  		fmt.Sprintf(timeHeader, x.LaunchString),
   210  		fmt.Sprintf(hostHeader, x.Host),
   211  	}, "\n")
   212  	if len(banner) != 0 {
   213  		parl.Info(banner)
   214  	}
   215  
   216  	pflags.NewArgParser(optionsList, x.usage).Parse()
   217  	if BaseOptions.Version {
   218  		os.Exit(0)
   219  	}
   220  
   221  	// parse arguments
   222  	args := flag.Args() // command-line arguments not part of flags
   223  	count := len(args)
   224  	argsOk :=
   225  		count == 0 && (x.Arguments&NoArguments != 0) ||
   226  			count == 1 && (x.Arguments&OneArgument != 0) ||
   227  			count > 0 && (x.Arguments&ManyArguments != 0)
   228  	if !argsOk {
   229  		if count == 0 {
   230  			if x.Arguments&ManyArguments != 0 {
   231  				parl.Log("There must be one or more arguments")
   232  			} else {
   233  				parl.Log("There must be one argument")
   234  			}
   235  		} else {
   236  			for i, v := range args {
   237  				args[i] = fmt.Sprintf("%q", v)
   238  			}
   239  			parl.Log("Unknown parameters: %s\n", strings.Join(args, "\x20"))
   240  		}
   241  		x.usage()
   242  		pos.Exit(pos.StatusCodeUsage, nil)
   243  	}
   244  	x.ArgCount = count
   245  	if count == 1 && (x.Arguments&OneArgument != 0) {
   246  		x.Arg = args[0]
   247  	}
   248  	if count > 0 && (x.Arguments&ManyArguments != 0) {
   249  		x.Args = args
   250  	}
   251  
   252  	x.optionsWereParsed.Store(true)
   253  
   254  	return
   255  }
   256  
   257  // ConfigureLog configures the default log such as parl.Log parl.Out parl.D
   258  // for silent, debug and regExp.
   259  // Settings come from BaseOptions.Silent and BaseOptions.Debug.
   260  //
   261  // ConfigureLog supports functional chaining like:
   262  //
   263  //	exe.Init().
   264  //	  …
   265  //	  ConfigureLog().
   266  //	  ApplyYaml(…)
   267  func (x *Executable) ConfigureLog() (ex1 *Executable) {
   268  	if BaseOptions.Silent {
   269  		parl.SetSilent(true)
   270  	}
   271  	if BaseOptions.Debug {
   272  		parl.SetDebug(true)
   273  	}
   274  	if BaseOptions.Verbosity != "" {
   275  		if err := parl.SetRegexp(BaseOptions.Verbosity); err != nil {
   276  			pos.Exit(pos.StatusCodeUsage, err)
   277  		}
   278  	}
   279  	parl.Debug("exe.ConfigureLog silent: %t debug: %t verbosity: %q\n",
   280  		BaseOptions.Silent, BaseOptions.Debug, BaseOptions.Verbosity)
   281  	return x
   282  }
   283  
   284  // Recover function to be used in main.main:
   285  //
   286  //	func main() {
   287  //	  defer Recover()
   288  //	  …
   289  //
   290  // On panic, the function prints to stderr: "Unhandled panic invoked exe.Recover: stack:"
   291  // followed by a stack trace. It then adds an error to mains.Executable and terminates
   292  // the process with status code 1
   293  func (x *Executable) Recover(errp ...*error) {
   294  
   295  	// get error from *errp and store in ex.err
   296  	if len(errp) > 0 {
   297  		if errp0 := errp[0]; errp0 != nil {
   298  			if err := *errp0; err != nil && err != errEarlyPanicError {
   299  				x.AddErr(err)
   300  			}
   301  		}
   302  	}
   303  
   304  	x.doPanic(recover())
   305  
   306  	// ex.err now contains program result
   307  
   308  	// print completed successfully
   309  	if x.err.Count() == 0 && x.OKtext != NoOK {
   310  		var program string
   311  		var completedSuccessfully string
   312  		now := "at " + parl.ShortSpace() // time now second precision
   313  		if x.OKtext != "" {
   314  			completedSuccessfully = x.OKtext // custom "Completed successfully"
   315  		} else {
   316  			program = x.Program
   317  			completedSuccessfully = defaultOK // "<executable> completed successfully
   318  		}
   319  		sList := []string{program, completedSuccessfully, now}
   320  		parl.Log(pstrings.FilteredJoin(sList)) // to stderr
   321  	}
   322  
   323  	// will print any errors
   324  	x.Exit()
   325  }
   326  
   327  var errEarlyPanicError = errors.New("mains stored a panic")
   328  
   329  func (x *Executable) EarlyPanic(errp *error) {
   330  
   331  	// store the panic
   332  	x.doPanic(recover())
   333  
   334  	// since the panic was stored and printed:
   335  	//	- an error must be returned to maintain the error condition
   336  	//	- panic cannot be invoked again because it would cancel other deferred functions
   337  	//	- if the stored error is used, this may be modified and lead to error duplication
   338  	//	- therefore, a new simple error is returned
   339  	//	- if a stack trace is added to the simple error, it will appear as a panic
   340  	if *errp == nil {
   341  		*errp = errEarlyPanicError
   342  	}
   343  }
   344  
   345  func (x *Executable) doPanic(panicValue any) {
   346  
   347  	// ensure -debug honored if panic before options parsing
   348  	if !x.optionsWereParsed.Load() {
   349  		for _, option := range os.Args {
   350  			if option == pflags.DebugOption { // -debug
   351  				parl.SetDebug(true)
   352  			}
   353  		}
   354  	}
   355  
   356  	// check for panic
   357  	if panicValue != nil {
   358  
   359  		// determine if v is error
   360  		err, recoverValueIsError := panicValue.(error)
   361  		var error0 error
   362  
   363  		// debug print
   364  		isDebug := parl.IsThisDebug()
   365  		if isDebug {
   366  			hasStack := false
   367  			var valueString string
   368  			var error0type string
   369  			if !recoverValueIsError {
   370  				valueString = parl.Sprintf(" '%+v'", panicValue)
   371  			} else {
   372  				error0 = perrors.Error0(err)
   373  				error0type = parl.Sprintf(" panic error type: %T", error0)
   374  				hasStack = perrors.HasStack(err)
   375  				error0value := fmt.Sprintf("error0: %+v", error0)
   376  				if hasStack {
   377  					valueString = parl.Sprintf(" error-value:\n\n%s\n\n%s\n\n", perrors.Long(err), error0value)
   378  				} else {
   379  					valueString = err.Error() + "\n" + error0value
   380  				}
   381  			}
   382  			parl.Debug("%s: panic with -debug: recover-value type: %T%s hasStack: %t%s",
   383  				pruntime.NewCodeLocation(0).PackFunc(),
   384  				panicValue, error0type, hasStack, valueString)
   385  		}
   386  
   387  		// print panic message and invocation stack
   388  		var stackString string
   389  		if isDebug {
   390  			stackString = " recovery stack trace:\n\n" + pruntime.DebugStack(0) + "\n\n"
   391  		}
   392  		var programString string
   393  		if x.Program != "" {
   394  			programString = "\x20" + x.Program
   395  		}
   396  		parl.Log("\n\nProgram%s Recovered a Main-Thread panic:%s", programString, stackString)
   397  
   398  		// store recovery value as error
   399  		var prepend string
   400  		var postpend string
   401  		if !recoverValueIsError {
   402  			err = perrors.Errorf("panic: non-error value: %T %[1]v", panicValue)
   403  		} else {
   404  			prepend = "panic: “"
   405  			postpend = "”"
   406  			if isDebug {
   407  				// put error0 type name in error message
   408  				postpend += parl.Sprintf(" type: %T", error0)
   409  			}
   410  		}
   411  		// always add a stack trace after panic
   412  		//	- must contain one frame after panic
   413  		err = perrors.Stackn(err, doPanicFrames)
   414  		err = perrors.Errorf("main-thread %s%w%s", prepend, err, postpend)
   415  		x.AddErr(err)
   416  	}
   417  }
   418  
   419  // AddErr extended with immediate printing of first error
   420  //   - if err is the first error, it is immediately printed
   421  //   - subsequent errors are appended to x.err
   422  //   - err nil: ignored
   423  func (x *Executable) AddErr(err error) {
   424  
   425  	// debug printing
   426  	if parl.IsThisDebug() {
   427  		packFunc := perrors.PackFunc()
   428  		var errS string
   429  		if err != nil {
   430  			errS = "\x27" + err.Error() + "\x27"
   431  		} else {
   432  			errS = "nil"
   433  		}
   434  		parl.Debug("\n%s(error: %s)\n%[1]s invocation:\n%[3]s", packFunc, errS, pruntime.Invocation(0))
   435  		plog.GetLog(os.Stderr).Output(0, "") // newline after debug location. No location appended to this printout
   436  	}
   437  
   438  	// if AddErr with no error, do nothing
   439  	if err == nil {
   440  		return // no error do nothing return
   441  	}
   442  
   443  	// if the first error, immediately print it
   444  	if x.err.Count() == 0 {
   445  
   446  		// print and store the first error
   447  		if x.printErr(err, checkForPanic(err)) {
   448  			x.err.IsFirstLong.Store(true)
   449  		}
   450  	}
   451  
   452  	// append subsequent error
   453  	x.err.Add(err)
   454  }
   455  
   456  // Exit terminate from mains.err: exit 0 or echo to stderr and status code 1
   457  //   - Usually invoked for all app terminations
   458  //   - — either by defer ex.Recover(&err) at beginning fo main
   459  //   - — or rarely by direct invocation in program code
   460  //   - when invoked, errors are expected to be in ex.err from:
   461  //   - — ex.AddErr or
   462  //   - — ex.Recover
   463  //   - Exit does not return from invoking os.Exit
   464  func (x *Executable) Exit(stausCode ...int) {
   465  
   466  	// get requested status code
   467  	var statusCode0 int
   468  	if len(stausCode) > 0 {
   469  		statusCode0 = stausCode[0]
   470  	}
   471  
   472  	// printouts when IsDebug
   473  	if x.err.Count() == 0 {
   474  		parl.Debug("\nexe.Exit: no error")
   475  	} else {
   476  		parl.Debug("\nexe.Exit: err: %T '%[1]v'", x.err.GetN())
   477  	}
   478  	parl.Debug("\nexe.Exit invocation:\n%s\n", pruntime.NewStack(0))
   479  	if parl.IsThisDebug() { // add newline during debug without location
   480  		plog.GetLog(os.Stderr).Output(0, "") // newline after debug location. No location appended to this printout
   481  	}
   482  
   483  	// terminate when there are no errors
   484  	var errCount = x.err.Count()
   485  	if errCount == 0 {
   486  		if statusCode0 != 0 {
   487  			pos.OsExit(statusCode0)
   488  		}
   489  		pos.Exit0()
   490  	}
   491  	var err0 error
   492  
   493  	// print all errors except the very first
   494  	//	- if first error value was printed long, it doesnot have to be printed
   495  	//	- otherwise, print its associated errors
   496  	//	- subsequent errors printed in full
   497  	for i, err := range x.err.Get() {
   498  		if i == 0 {
   499  			err0 = err
   500  			// if first error was printed long, ignore it alltogether
   501  			if x.err.IsFirstLong.Load() {
   502  				continue
   503  			}
   504  		} else {
   505  			// print a subsequent error
   506  			if x.printErr(err, checkForPanic(err)) {
   507  				continue // printed long means it’s complete
   508  			}
   509  		}
   510  
   511  		// now recursively print all associated errors
   512  		x.printAssociated(strconv.Itoa(i+1), err)
   513  	}
   514  
   515  	// just before exit, print the one-liner message of the first occurring error again
   516  	if x.IsLongErrors || errCount > 1 {
   517  		fmt.Fprintln(os.Stderr, err0)
   518  	}
   519  
   520  	// exit 1
   521  	if statusCode0 == 0 {
   522  		statusCode0 = pos.StatusCodeErr
   523  	}
   524  	parl.Log(parl.ShortSpace() + "\x20" + x.Program + ": exit status " + strconv.Itoa(statusCode0)) // outputs "060102 15:04:05Z07 " without newline to stderr
   525  	pos.Exit(statusCode0, nil)                                                                      // os.Exit(1) outputs "exit status 1" to stderr
   526  }
   527  
   528  func (x *Executable) printAssociated(i string, err error) {
   529  	var associatedErrors = errorglue.ErrorList(err)
   530  	// 1 means no associated errors
   531  	if len(associatedErrors) < 1 {
   532  		return
   533  	}
   534  	for j, e := range associatedErrors[1:] {
   535  		var label = parl.Sprintf("error#%s-associated#%d", i, j+1)
   536  		parl.Log(label)
   537  		if x.printErr(err, checkForPanic(err)) {
   538  			continue // printed long means it’s complete
   539  		}
   540  		x.printAssociated(label, e)
   541  	}
   542  }
   543  
   544  // printErr prints error to stderr
   545  //   - err is printed using perrors.Long or Short
   546  //   - panicString non-empty: with stack trace, append this panic description
   547  //   - long with stack trace if panicString non-empty or x.IsLongErrors
   548  //   - short has location if x.IsErrorLocation
   549  func (x *Executable) printErr(err error, panicString ...string) (printedLong bool) {
   550  	var s string
   551  
   552  	// get panic string
   553  	if len(panicString) > 0 {
   554  		s = panicString[0]
   555  	}
   556  
   557  	// print the error
   558  	if x.IsLongErrors || s != "" {
   559  		printedLong = true
   560  		s += perrors.Long(err) + "\n— — —"
   561  	} else if x.IsErrorLocation {
   562  		s = perrors.Short(err)
   563  	} else if err != nil {
   564  		s = err.Error()
   565  	}
   566  	parl.Log(s)
   567  	return
   568  }
   569  
   570  // usage prints options usage
   571  func (x *Executable) usage() {
   572  	writer := flag.CommandLine.Output()
   573  	var license string
   574  	if x.License != "" {
   575  		license = "License: " + x.License
   576  	}
   577  	fmt.Fprintln(
   578  		writer,
   579  		pstrings.FilteredJoin([]string{
   580  			license,
   581  			pstrings.FilteredJoin([]string{
   582  				x.Program,
   583  				x.Description,
   584  			}, "\x20"),
   585  			usageHeader,
   586  			pstrings.FilteredJoin([]string{
   587  				x.Program,
   588  				optionsSyntax,
   589  				x.ArgumentsUsage,
   590  			}, "\x20"),
   591  		}, "\n"))
   592  	flag.PrintDefaults()
   593  	fmt.Fprintln(writer, helpHelp)
   594  }