feat: add core.Crash() — panic recovery and crash reporting
Adfer (Welsh: recover). Built into the Core struct: defer c.Crash().Recover() // capture panics c.Crash().SafeGo(fn) // safe goroutine c.Crash().Reports(5) // last 5 crash reports CrashReport includes: timestamp, error, stack trace, system info (OS/arch/Go version), custom metadata. Optional file output: JSON array of crash reports. Zero external dependencies. Based on leaanthony/adfer (168 lines), integrated into pkg/core. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
66b4b08600
commit
8765458bc6
3 changed files with 157 additions and 5 deletions
|
|
@ -27,8 +27,9 @@ var (
|
|||
// )
|
||||
func New(opts ...Option) (*Core, error) {
|
||||
c := &Core{
|
||||
etc: NewEtc(),
|
||||
svc: newServiceManager(),
|
||||
etc: NewEtc(),
|
||||
crash: NewCrashHandler(),
|
||||
svc: newServiceManager(),
|
||||
}
|
||||
c.bus = newMessageBus(c)
|
||||
|
||||
|
|
|
|||
142
pkg/core/crash.go
Normal file
142
pkg/core/crash.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// Crash recovery and reporting for the Core framework.
|
||||
// Named after adfer (Welsh for "recover"). Captures panics,
|
||||
// writes JSON crash reports, and provides safe goroutine wrappers.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CrashReport represents a single crash event.
|
||||
type CrashReport struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Error string `json:"error"`
|
||||
Stack string `json:"stack"`
|
||||
System CrashSystem `json:"system,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// CrashSystem holds system information at crash time.
|
||||
type CrashSystem struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Version string `json:"go_version"`
|
||||
}
|
||||
|
||||
// CrashHandler manages panic recovery and crash reporting.
|
||||
type CrashHandler struct {
|
||||
filePath string
|
||||
meta map[string]string
|
||||
onCrash func(CrashReport)
|
||||
}
|
||||
|
||||
// CrashOption configures a CrashHandler.
|
||||
type CrashOption func(*CrashHandler)
|
||||
|
||||
// WithCrashFile sets the path for crash report JSON output.
|
||||
func WithCrashFile(path string) CrashOption {
|
||||
return func(h *CrashHandler) { h.filePath = path }
|
||||
}
|
||||
|
||||
// WithCrashMeta adds metadata included in every crash report.
|
||||
func WithCrashMeta(meta map[string]string) CrashOption {
|
||||
return func(h *CrashHandler) { h.meta = meta }
|
||||
}
|
||||
|
||||
// WithCrashHandler sets a callback invoked on every crash.
|
||||
func WithCrashHandler(fn func(CrashReport)) CrashOption {
|
||||
return func(h *CrashHandler) { h.onCrash = fn }
|
||||
}
|
||||
|
||||
// NewCrashHandler creates a crash handler with the given options.
|
||||
func NewCrashHandler(opts ...CrashOption) *CrashHandler {
|
||||
h := &CrashHandler{}
|
||||
for _, o := range opts {
|
||||
o(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Recover captures a panic and creates a crash report.
|
||||
// Use as: defer c.Crash().Recover()
|
||||
func (h *CrashHandler) Recover() {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
|
||||
report := CrashReport{
|
||||
Timestamp: time.Now(),
|
||||
Error: err.Error(),
|
||||
Stack: string(debug.Stack()),
|
||||
System: CrashSystem{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Version: runtime.Version(),
|
||||
},
|
||||
Meta: h.meta,
|
||||
}
|
||||
|
||||
if h.onCrash != nil {
|
||||
h.onCrash(report)
|
||||
}
|
||||
|
||||
if h.filePath != "" {
|
||||
h.appendReport(report)
|
||||
}
|
||||
}
|
||||
|
||||
// SafeGo runs a function in a goroutine with panic recovery.
|
||||
func (h *CrashHandler) SafeGo(fn func()) {
|
||||
go func() {
|
||||
defer h.Recover()
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
// Reports returns the last n crash reports from the file.
|
||||
func (h *CrashHandler) Reports(n int) ([]CrashReport, error) {
|
||||
if h.filePath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := os.ReadFile(h.filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reports []CrashReport
|
||||
if err := json.Unmarshal(data, &reports); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reports) <= n {
|
||||
return reports, nil
|
||||
}
|
||||
return reports[len(reports)-n:], nil
|
||||
}
|
||||
|
||||
func (h *CrashHandler) appendReport(report CrashReport) {
|
||||
var reports []CrashReport
|
||||
|
||||
if data, err := os.ReadFile(h.filePath); err == nil {
|
||||
json.Unmarshal(data, &reports)
|
||||
}
|
||||
|
||||
reports = append(reports, report)
|
||||
data, _ := json.MarshalIndent(reports, "", " ")
|
||||
os.WriteFile(h.filePath, data, 0644)
|
||||
}
|
||||
|
|
@ -80,9 +80,10 @@ type LocaleProvider interface {
|
|||
|
||||
// Core is the central application object that manages services, assets, and communication.
|
||||
type Core struct {
|
||||
App any // GUI runtime (e.g., Wails App) - set by WithApp option
|
||||
mnt *Sub // Mount point for embedded assets
|
||||
etc *Etc // Configuration, settings, and feature flags
|
||||
App any // GUI runtime (e.g., Wails App) - set by WithApp option
|
||||
mnt *Sub // Mount point for embedded assets
|
||||
etc *Etc // Configuration, settings, and feature flags
|
||||
crash *CrashHandler // Panic recovery and crash reporting
|
||||
svc *serviceManager
|
||||
bus *messageBus
|
||||
locales []fs.FS // collected from LocaleProvider services
|
||||
|
|
@ -107,6 +108,14 @@ func (c *Core) Etc() *Etc {
|
|||
return c.etc
|
||||
}
|
||||
|
||||
// Crash returns the crash handler for panic recovery.
|
||||
//
|
||||
// defer c.Crash().Recover()
|
||||
// c.Crash().SafeGo(func() { ... })
|
||||
func (c *Core) Crash() *CrashHandler {
|
||||
return c.crash
|
||||
}
|
||||
|
||||
// Locales returns all locale filesystems collected from registered services.
|
||||
// The i18n service uses this during startup to load translations.
|
||||
func (c *Core) Locales() []fs.FS {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue