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 }