diff --git a/pkg/core/core.go b/pkg/core/core.go index 896f427..24c8bfe 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -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) diff --git a/pkg/core/crash.go b/pkg/core/crash.go new file mode 100644 index 0000000..bf1fb94 --- /dev/null +++ b/pkg/core/crash.go @@ -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) +} diff --git a/pkg/core/interfaces.go b/pkg/core/interfaces.go index 47288dd..e3be378 100644 --- a/pkg/core/interfaces.go +++ b/pkg/core/interfaces.go @@ -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 {