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 }