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

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"fmt"
    10  	"runtime/debug"
    11  	"strings"
    12  
    13  	"github.com/haraldrudell/parl/perrors"
    14  	"github.com/haraldrudell/parl/perrors/errorglue"
    15  	"github.com/haraldrudell/parl/pruntime"
    16  )
    17  
    18  // Recover recovers panic using deferred annotation
    19  //   - Recover creates a single aggregate error of *errp and any panic
    20  //   - if onError non-nil, the function is invoked zero or one time with the aggregate error
    21  //   - if onError nil, the error is logged to standard error
    22  //   - if errp is non-nil, it is updated with any aggregate error
    23  //
    24  // Usage:
    25  //
    26  //	func someFunc() (err error) {
    27  //	  defer parl.Recover(func() parl.DA { return parl.A() }, &err, parl.NoOnError)
    28  func Recover(deferredLocation func() DA, errp *error, onError OnError) {
    29  	doRecovery(noAnnotation, deferredLocation, errp, onError, recoverOnErrrorOnce, noIsPanic, recover())
    30  }
    31  
    32  // Recover2 recovers panic using deferred annotation
    33  //   - if onError non-nil, the function is invoked zero, one or two times with any error in *errp and any panic
    34  //   - if onError nil, errors are logged to standard error
    35  //   - if errp is non-nil:
    36  //   - — if *errp was nil, it is updated with any panic
    37  //   - — if *errp was non-nil, it is updated with any panic as an aggregate error
    38  //
    39  // Usage:
    40  //
    41  //	func someFunc() (err error) {
    42  //	  defer parl.Recover2(func() parl.DA { return parl.A() }, &err, parl.NoOnError)
    43  func Recover2(deferredLocation func() DA, errp *error, onError OnError) {
    44  	doRecovery(noAnnotation, deferredLocation, errp, onError, recoverOnErrrorMultiple, noIsPanic, recover())
    45  }
    46  
    47  // RecoverAnnotation is like Recover but with fixed-string annotation
    48  func RecoverAnnotation(annotation string, errp *error, onError OnError) {
    49  	doRecovery(annotation, noDeferredAnnotation, errp, onError, recoverOnErrrorOnce, noIsPanic, recover())
    50  }
    51  
    52  // nil OnError function
    53  //   - public for RecoverAnnotation
    54  var NoOnError OnError
    55  
    56  // OnError is a function that receives error values from an errp error pointer or a panic
    57  type OnError func(err error)
    58  
    59  const (
    60  	// counts the frames in [parl.A]
    61  	parlAFrames = 1
    62  	// counts the stack-frame in [parl.processRecover]
    63  	processRecoverFrames = 1
    64  	// counts the stack-frame of [parl.doRecovery] and [parl.Recover] or [parl.Recover2]
    65  	//	- but for panic detector to work, there must be one frame after
    66  	//		runtime.gopanic, so remove one frame
    67  	doRecoveryFrames = 2 - 1
    68  	// fixed-string annotation is not present
    69  	noAnnotation = ""
    70  )
    71  
    72  const (
    73  	// indicates onError to be invoked once for all errors
    74  	recoverOnErrrorOnce OnErrorStrategy = iota
    75  	// indicates onError to be invoked once per error
    76  	recoverOnErrrorMultiple
    77  	// do not invoke onError
    78  	recoverOnErrrorNone
    79  )
    80  
    81  // how OnError is handled: recoverOnErrrorOnce recoverOnErrrorMultiple recoverOnErrrorNone
    82  type OnErrorStrategy uint8
    83  
    84  // indicates deferred annotation is not present
    85  var noDeferredAnnotation func() DA
    86  
    87  // DA is the value returned by a deferred code location function
    88  type DA *pruntime.CodeLocation
    89  
    90  // contains a deferred code location for annotation
    91  type annotationLiteral func() DA
    92  
    93  // A is a thunk returning a deferred code location
    94  func A() DA { return pruntime.NewCodeLocation(parlAFrames) }
    95  
    96  // noIsPanic is a stand-in nil value when noPanic is not present
    97  var noIsPanic *bool
    98  
    99  // doRecovery implements recovery for Recovery andd Recovery2
   100  func doRecovery(annotation string, deferredAnnotation annotationLiteral, errp *error, onError OnError, onErrorStrategy OnErrorStrategy, isPanic *bool, recoverValue interface{}) {
   101  	if onErrorStrategy == recoverOnErrrorNone {
   102  		if errp == nil {
   103  			panic(NilError("errp"))
   104  		}
   105  	} else if errp == nil && onError == nil {
   106  		panic(NilError("both errp and onError"))
   107  	}
   108  
   109  	// build aggregate error in err
   110  	var err error
   111  	if errp != nil {
   112  		err = *errp
   113  		// if onError is to be invoked multiple times,
   114  		// and *errp contains an error,
   115  		// invoke onError or Log to standard error
   116  		if err != nil && onErrorStrategy == recoverOnErrrorMultiple {
   117  			invokeOnError(onError, err) // invoke onError or parl.Log
   118  		}
   119  	}
   120  
   121  	// consume recover()
   122  	if recoverValue != nil {
   123  		if isPanic != nil {
   124  			*isPanic = true
   125  		}
   126  		if annotation == noAnnotation {
   127  			annotation = getDeferredAnnotation(deferredAnnotation)
   128  		}
   129  		var panicError = processRecoverValue(annotation, recoverValue, doRecoveryFrames)
   130  		err = perrors.AppendError(err, panicError)
   131  		if onErrorStrategy == recoverOnErrrorMultiple {
   132  			invokeOnError(onError, panicError)
   133  		}
   134  	}
   135  
   136  	// if err now contains any error
   137  	if err != nil {
   138  		// if errp non-nil:
   139  		//	- err was obtained from *errp
   140  		//	- err may now be panicError or have had panicError appended
   141  		//	- overwrite back to non-nil errp
   142  		if errp != nil && *errp != err {
   143  			*errp = err
   144  		}
   145  		// if OnError is once, invoke onError or Log with the aggregate error
   146  		if onErrorStrategy == recoverOnErrrorOnce {
   147  			invokeOnError(onError, err)
   148  		}
   149  	}
   150  }
   151  
   152  // getDeferredAnnotation obtains annotation from a deferred annotation function literal
   153  func getDeferredAnnotation(deferredAnnotation annotationLiteral) (annotation string) {
   154  	if deferredAnnotation != nil {
   155  		if da := deferredAnnotation(); da != nil {
   156  			var cL = (*pruntime.CodeLocation)(da)
   157  			// single word package name
   158  			var packageName = cL.Package()
   159  			// recoverDaPanic.func1: hosting function name and a derived name for the function literal
   160  			var funcName = cL.FuncIdentifier()
   161  			// removed “.func1” suffix
   162  			if index := strings.LastIndex(funcName, "."); index != -1 {
   163  				funcName = funcName[:index]
   164  			}
   165  			annotation = fmt.Sprintf("panic detected in %s.%s:",
   166  				packageName,
   167  				funcName,
   168  			)
   169  		}
   170  	}
   171  	if annotation == "" {
   172  		// default annotation cannot be obtained
   173  		//	- the deferred Recover function is invoked directly from rutine, eg. runtime.gopanic
   174  		//	- therefore, use fixed string
   175  		annotation = "recover from panic:"
   176  	}
   177  
   178  	return
   179  }
   180  
   181  // invokeOnError invokes an onError function or logs to standard error if onError is nil
   182  func invokeOnError(onError OnError, err error) {
   183  	if onError != nil {
   184  		onError(err)
   185  		return
   186  	}
   187  	Log("Recover: %+v\n", err)
   188  	Log("invokeOnError parl.recover %s", debug.Stack())
   189  }
   190  
   191  // processRecoverValue returns an error value with stack from annotation and panicValue
   192  //   - annotation is non-empty annotation indicating code loction or action
   193  //   - panicValue is non-nil value returned by built-in recover function
   194  func processRecoverValue(annotation string, panicValue interface{}, frames int) (err error) {
   195  	if frames < 0 {
   196  		frames = 0
   197  	}
   198  
   199  	// if panicValue is an error with attached stack,
   200  	// the panic detector will fail because
   201  	// that innermost stack does not include panic recovery
   202  	var hadPreRecoverStack bool
   203  	if e, ok := panicValue.(error); ok {
   204  		hadPreRecoverStack = errorglue.GetInnerMostStack(e) != nil
   205  	}
   206  	// ensure an error value is derived from panicValue
   207  	err = perrors.Errorf("%s “%w”",
   208  		annotation,
   209  		ensureError(panicValue, frames+processRecoverFrames),
   210  	)
   211  	// make sure err has a post-recover() stack
   212  	//	- this will allow the panic detector to succeed
   213  	if hadPreRecoverStack {
   214  		err = perrors.Stackn(err, frames)
   215  	}
   216  
   217  	return
   218  }