github.com/haraldrudell/parl@v0.4.176/perrors/errorglue/first-panic-stack.go (about)

     1  /*
     2  © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package errorglue
     7  
     8  import (
     9  	"errors"
    10  	"slices"
    11  
    12  	"github.com/haraldrudell/parl/perrors/panicdetector"
    13  	"github.com/haraldrudell/parl/pruntime"
    14  )
    15  
    16  // FirstPanicStack checks all stack traces, oldest first, for
    17  // a panic and returns:
    18  //   - stack:
    19  //   - — the panic stack if any
    20  //   - — otherwise, the oldest stack if any
    21  //   - — otherwise nil
    22  //   - isPanic true: a panic stack was found
    23  //   - — recoveryIndex panicIndex are valid
    24  //   - numberOfStacks: the number of stacks until the panic stack including it,
    25  //     or the total number of stacks
    26  //   - errorWithStack:
    27  //   - — if panic, the error providing the oldest panic stack
    28  //   - — otherwise, the oldest error with a stack
    29  //   - — otherwise, the oldest error
    30  //   - — otherwise nil
    31  //   - this enables consumer to print:
    32  //   - — panic location if any
    33  //   - — otherwise, oldest error location if any
    34  //   - — otherwise no location
    35  //   - err is an error chain that may contain stack traces and be nil
    36  //   - if any stack trace has a panic:
    37  //   - — isPanic is true
    38  //   - — stack is the oldest stack with a panic
    39  //   - — stack[panicIndex] is the code line causing the oldest panic
    40  //   - — stack[recoveryIndex] is the first code line in the deferred sequence.
    41  //     Typically the last line of the executing function
    42  //   - — numberOfStacks is 1 for the panic stack and added the number of stacks that are older
    43  //   - — errorWithStack is the error with panic, typically from [parl.Recover]
    44  //   - if isPanic is false:
    45  //   - — panicIndex recoveryIndex are zero
    46  //   - — stack and errorWithStack is the oldest stack if any
    47  //     -— numberOfStacks is total number of stacks in the error chain
    48  //   - this must be done beacuse maybe panic() was
    49  //     provided an error with stack trace
    50  //   - on err nil: all values false, nil or zero
    51  func FirstPanicStack(err error) (
    52  	isPanic bool, stack pruntime.Stack, recoveryIndex, panicIndex int,
    53  	numberOfStacks int,
    54  	errorWithStack error,
    55  ) {
    56  
    57  	// any panic stack need to be associated with its exact error
    58  	//	- normally, the returned stack is from any older error in the chain
    59  	//	- therefore, the err’s error chain must be traversed
    60  	//	- the error chain is a directed single-linked list newest error first
    61  	//	- —
    62  	//	- if there is no panic, the oldest stack trace is what will be displayed, ie. the source of the error
    63  	//	- a panic can be in a newer stack trace if an error with a stack trace is provided to panic
    64  	//	- therefore, the oldest panic stack trace should be provided
    65  
    66  	// errs is the expanded error chain
    67  	//	- err first that is newest, then older errrors
    68  	var errs []error
    69  	for e := err; e != nil; e = errors.Unwrap(e) {
    70  		errs = append(errs, e)
    71  	}
    72  	// errs now begin with oldest error, ending with err
    73  	slices.Reverse(errs)
    74  
    75  	// oldestStack contains the oldest stack trace
    76  	//	- if there is no panic, this should be returned
    77  	var oldestStack pruntime.Stack
    78  	// the oldest error, or
    79  	// if a stack was found, the oldest error with a stack
    80  	var oldestErr error
    81  
    82  	// scan for panic-stack, oldest error first
    83  	for _, e := range errs {
    84  
    85  		// check if e contains a stack trace
    86  		if errWithStack, ok := e.(ErrorCallStacker); ok {
    87  			// save stack
    88  			stack = errWithStack.StackTrace()
    89  			if oldestStack == nil {
    90  				// store oldest stack
    91  				oldestStack = stack
    92  				// store oldest error or oldest error with stack
    93  				oldestErr = e
    94  			}
    95  			// store the error providing stack
    96  			errorWithStack = e
    97  		} else {
    98  			continue // an error without stack
    99  		}
   100  		// increase number of stack until panic,
   101  		// or total number of stacks
   102  		numberOfStacks++
   103  
   104  		// check if the stack trace has a panic
   105  		isPanic, recoveryIndex, panicIndex = panicdetector.Indices(stack)
   106  		if !isPanic {
   107  			continue // this error did not have a panic stack-trace
   108  		}
   109  
   110  		return // found panic return
   111  	}
   112  	// no panic stack was found
   113  	stack = oldestStack
   114  	errorWithStack = oldestErr
   115  
   116  	return // no panic-stack return
   117  }