From ca206452cd8bc33db6fb91d8de9d370ba252c719 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 28 Jan 2026 07:00:33 +1100 Subject: [PATCH] The panic handling has been ported to v2. Here's a summary of the changes: ## Summary of Changes **1. Created `v2/internal/frontend/desktop/linux/panic_handler.go`** - Ported the panic recovery logic from v3 - Includes `getStackTrace()` function for generating readable stack traces - `handlePanic()` function that recovers from panics and either calls the custom handler or logs the error **2. Added to `v2/pkg/options/options.go`** - Added `PanicDetails` struct with `StackTrace`, `Error`, `Time`, and `FullStackTrace` fields - Added `PanicHandler` type: `func(*PanicDetails)` - Added `PanicHandler` field to the `App` struct with documentation **3. Modified `v2/internal/frontend/desktop/linux/frontend.go`** - Added `defer handlePanic(f.frontendOptions.PanicHandler, f.logger)` to the goroutine in `processMessage()` (line 468) ## Usage Example Users can now configure a custom panic handler in their v2 Wails application: ```go app := wails.Run(&options.App{ Title: "My App", // ... other options PanicHandler: func(details *options.PanicDetails) { // Custom panic handling logic log.Printf("Panic occurred at %v: %v\n%s", details.Time, details.Error, details.StackTrace) // Could show error dialog, send to error tracking service, etc. }, }) ``` If no `PanicHandler` is set, panics will be logged via the application logger and the application will continue running instead of crashing with a signal handler error. --- .../frontend/desktop/linux/frontend.go | 2 + .../frontend/desktop/linux/panic_handler.go | 100 ++++++++++++++++++ v2/pkg/options/options.go | 19 ++++ 3 files changed, 121 insertions(+) create mode 100644 v2/internal/frontend/desktop/linux/panic_handler.go diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 2942a112e..d3c886018 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -501,6 +501,8 @@ func (f *Frontend) processMessage(message string) { } go func() { + defer handlePanic(f.frontendOptions.PanicHandler, f.logger) + result, err := f.dispatcher.ProcessMessage(message, f) if err != nil { f.logger.Error(err.Error()) diff --git a/v2/internal/frontend/desktop/linux/panic_handler.go b/v2/internal/frontend/desktop/linux/panic_handler.go new file mode 100644 index 000000000..a4c301268 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/panic_handler.go @@ -0,0 +1,100 @@ +//go:build linux +// +build linux + +package linux + +import ( + "fmt" + "runtime" + "runtime/debug" + "strings" + "time" + + "github.com/wailsapp/wails/v2/pkg/options" +) + +func getStackTrace(skipStart int, skipEnd int) string { + // Get all program counters first + pc := make([]uintptr, 32) + n := runtime.Callers(skipStart+1, pc) + if n == 0 { + return "" + } + + pc = pc[:n] + frames := runtime.CallersFrames(pc) + + // Collect all frames first + var allFrames []runtime.Frame + for { + frame, more := frames.Next() + allFrames = append(allFrames, frame) + if !more { + break + } + } + + // Remove frames from the end + if len(allFrames) > skipEnd { + allFrames = allFrames[:len(allFrames)-skipEnd] + } + + // Build the output string + var builder strings.Builder + for _, frame := range allFrames { + fmt.Fprintf(&builder, "%s\n\tat %s:%d\n", + frame.Function, frame.File, frame.Line) + } + return builder.String() +} + +type handlePanicOptions struct { + skipEnd int +} + +func newPanicDetails(err error, trace string) *options.PanicDetails { + return &options.PanicDetails{ + Error: err, + Time: time.Now(), + StackTrace: trace, + FullStackTrace: string(debug.Stack()), + } +} + +// handlePanic recovers from panics and processes them through the configured handler. +// Returns true if a panic was recovered. +func handlePanic(handler options.PanicHandler, logger interface{ Error(string, ...interface{}) }, opts ...handlePanicOptions) bool { + // Try to recover + e := recover() + if e == nil { + return false + } + + // Get the error + err, ok := e.(error) + if !ok { + err = fmt.Errorf("%v", e) + } + + // Get the stack trace + var stackTrace string + skipEnd := 0 + if len(opts) > 0 { + skipEnd = opts[0].skipEnd + } + stackTrace = getStackTrace(3, skipEnd) + + panicDetails := newPanicDetails(err, stackTrace) + + // Use custom handler if provided + if handler != nil { + handler(panicDetails) + return true + } + + // Default behavior: log the panic + if logger != nil { + logger.Error("panic error: %v\n%s", panicDetails.Error, panicDetails.StackTrace) + } + return true +} diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index 0f62d5e4b..6e6f3e8c4 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "runtime" + "time" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/linux" @@ -69,6 +70,12 @@ type App struct { // ErrorFormatter overrides the formatting of errors returned by backend methods ErrorFormatter ErrorFormatter + // PanicHandler is called when a panic occurs in a bound method. + // If not set, the panic will be logged and the application will continue. + // This is particularly useful on Linux where panics in cgo callbacks + // can cause signal handler issues. + PanicHandler PanicHandler + // CSS property to test for draggable elements. Default "--wails-draggable" CSSDragProperty string @@ -108,6 +115,18 @@ type App struct { type ErrorFormatter func(error) any +// PanicDetails contains information about a panic that occurred in a bound method +type PanicDetails struct { + StackTrace string + Error error + Time time.Time + FullStackTrace string +} + +// PanicHandler is a function that handles panics in bound methods. +// If not set, panics will be logged to the application logger. +type PanicHandler func(*PanicDetails) + type RGBA struct { R uint8 `json:"r"` G uint8 `json:"g"`