forked from jshiffer/matterbridge
103 lines
2.9 KiB
Go
103 lines
2.9 KiB
Go
|
package panics
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"runtime"
|
||
|
"runtime/debug"
|
||
|
"sync/atomic"
|
||
|
)
|
||
|
|
||
|
// Catcher is used to catch panics. You can execute a function with Try,
|
||
|
// which will catch any spawned panic. Try can be called any number of times,
|
||
|
// from any number of goroutines. Once all calls to Try have completed, you can
|
||
|
// get the value of the first panic (if any) with Recovered(), or you can just
|
||
|
// propagate the panic (re-panic) with Repanic().
|
||
|
type Catcher struct {
|
||
|
recovered atomic.Pointer[Recovered]
|
||
|
}
|
||
|
|
||
|
// Try executes f, catching any panic it might spawn. It is safe
|
||
|
// to call from multiple goroutines simultaneously.
|
||
|
func (p *Catcher) Try(f func()) {
|
||
|
defer p.tryRecover()
|
||
|
f()
|
||
|
}
|
||
|
|
||
|
func (p *Catcher) tryRecover() {
|
||
|
if val := recover(); val != nil {
|
||
|
rp := NewRecovered(1, val)
|
||
|
p.recovered.CompareAndSwap(nil, &rp)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Repanic panics if any calls to Try caught a panic. It will panic with the
|
||
|
// value of the first panic caught, wrapped in a panics.Recovered with caller
|
||
|
// information.
|
||
|
func (p *Catcher) Repanic() {
|
||
|
if val := p.Recovered(); val != nil {
|
||
|
panic(val)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Recovered returns the value of the first panic caught by Try, or nil if
|
||
|
// no calls to Try panicked.
|
||
|
func (p *Catcher) Recovered() *Recovered {
|
||
|
return p.recovered.Load()
|
||
|
}
|
||
|
|
||
|
// NewRecovered creates a panics.Recovered from a panic value and a collected
|
||
|
// stacktrace. The skip parameter allows the caller to skip stack frames when
|
||
|
// collecting the stacktrace. Calling with a skip of 0 means include the call to
|
||
|
// NewRecovered in the stacktrace.
|
||
|
func NewRecovered(skip int, value any) Recovered {
|
||
|
// 64 frames should be plenty
|
||
|
var callers [64]uintptr
|
||
|
n := runtime.Callers(skip+1, callers[:])
|
||
|
return Recovered{
|
||
|
Value: value,
|
||
|
Callers: callers[:n],
|
||
|
Stack: debug.Stack(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Recovered is a panic that was caught with recover().
|
||
|
type Recovered struct {
|
||
|
// The original value of the panic.
|
||
|
Value any
|
||
|
// The caller list as returned by runtime.Callers when the panic was
|
||
|
// recovered. Can be used to produce a more detailed stack information with
|
||
|
// runtime.CallersFrames.
|
||
|
Callers []uintptr
|
||
|
// The formatted stacktrace from the goroutine where the panic was recovered.
|
||
|
// Easier to use than Callers.
|
||
|
Stack []byte
|
||
|
}
|
||
|
|
||
|
// String renders a human-readable formatting of the panic.
|
||
|
func (p *Recovered) String() string {
|
||
|
return fmt.Sprintf("panic: %v\nstacktrace:\n%s\n", p.Value, p.Stack)
|
||
|
}
|
||
|
|
||
|
// AsError casts the panic into an error implementation. The implementation
|
||
|
// is unwrappable with the cause of the panic, if the panic was provided one.
|
||
|
func (p *Recovered) AsError() error {
|
||
|
if p == nil {
|
||
|
return nil
|
||
|
}
|
||
|
return &ErrRecovered{*p}
|
||
|
}
|
||
|
|
||
|
// ErrRecovered wraps a panics.Recovered in an error implementation.
|
||
|
type ErrRecovered struct{ Recovered }
|
||
|
|
||
|
var _ error = (*ErrRecovered)(nil)
|
||
|
|
||
|
func (p *ErrRecovered) Error() string { return p.String() }
|
||
|
|
||
|
func (p *ErrRecovered) Unwrap() error {
|
||
|
if err, ok := p.Value.(error); ok {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|