From e668c81806a91a9655fd07ad60a23c5e13f8ad1d Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Thu, 16 Jan 2025 22:08:18 +1100 Subject: [PATCH] Improved panic handling. Added guide. --- docs/src/content/docs/changelog.mdx | 1 + .../content/docs/guides/panic-handling.mdx | 111 ++++++++++++++++++ v3/examples/panic-handling/README.md | 11 ++ v3/examples/panic-handling/assets/index.html | 27 +++++ v3/examples/panic-handling/main.go | 60 ++++++++++ v3/pkg/application/application.go | 47 +++++--- v3/pkg/application/application_linux.go | 1 + v3/pkg/application/application_options.go | 2 +- v3/pkg/application/bindings.go | 20 +--- v3/pkg/application/dialogs_linux.go | 5 +- v3/pkg/application/dialogs_windows.go | 2 + v3/pkg/application/events.go | 15 ++- v3/pkg/application/linux_cgo.go | 1 + v3/pkg/application/mainthread.go | 12 +- v3/pkg/application/menuitem.go | 5 +- v3/pkg/application/messageprocessor.go | 5 + v3/pkg/application/messageprocessor_call.go | 4 +- v3/pkg/application/messageprocessor_dialog.go | 2 + v3/pkg/application/panic_handler.go | 107 +++++++++++++++++ v3/pkg/application/single_instance.go | 1 + v3/pkg/application/systemtray.go | 1 + v3/pkg/application/systemtray_linux.go | 1 + v3/pkg/application/webview_window.go | 10 +- 23 files changed, 398 insertions(+), 53 deletions(-) create mode 100644 docs/src/content/docs/guides/panic-handling.mdx create mode 100644 v3/examples/panic-handling/README.md create mode 100644 v3/examples/panic-handling/assets/index.html create mode 100644 v3/examples/panic-handling/main.go create mode 100644 v3/pkg/application/panic_handler.go diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index b1e0ce13d8b..a0aeed68d75 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add diagnostics section to `wails doctor` by [@leaanthony](https://github.com/leaanthony) - Add window to context when calling a service method by [@leaanthony](https://github.com/leaanthony) - Add `window-call` example to demonstrate how to know which window is calling a service by [@leaanthony](https://github.com/leaanthony) +- Better panic handling by [@leaanthony](https://github.com/leaanthony) ### Fixed - Fixed Windows+Linux Edit Menu issues by [@leaanthony](https://github.com/leaanthony) in [#3f78a3a](https://github.com/wailsapp/wails/commit/3f78a3a8ce7837e8b32242c8edbbed431c68c062) diff --git a/docs/src/content/docs/guides/panic-handling.mdx b/docs/src/content/docs/guides/panic-handling.mdx new file mode 100644 index 00000000000..5a38c444e73 --- /dev/null +++ b/docs/src/content/docs/guides/panic-handling.mdx @@ -0,0 +1,111 @@ +--- +title: Handling Panics +description: How to handle panics in your Wails application +--- + +In Go applications, panics can occur during runtime when something unexpected happens. This guide explains how to handle panics both in general Go code and specifically in your Wails application. + +## Understanding Panics in Go + +Before diving into Wails-specific panic handling, it's essential to understand how panics work in Go: + +1. Panics are for unrecoverable errors that shouldn't happen during normal operation +2. When a panic occurs in a goroutine, only that goroutine is affected +3. Panics can be recovered using `defer` and `recover()` + +Here's a basic example of panic handling in Go: + +```go +func doSomething() { + // Deferred functions run even when a panic occurs + defer func() { + if r := recover(); r != nil { + fmt.Printf("Recovered from panic: %v\n", r) + } + }() + + // Your code that might panic + panic("something went wrong") +} +``` + +For more detailed information about panic and recover in Go, see the [Go Blog: Defer, Panic, and Recover](https://go.dev/blog/defer-panic-and-recover). + +## Panic Handling in Wails + +Wails automatically handles panics that occur in your Service methods when they are called from the frontend. This means you don't need to add panic recovery to these methods - Wails will catch the panic and process it through your configured panic handler. + +The panic handler is specifically designed to catch: +- Panics in bound service methods called from the frontend +- Internal panics from the Wails runtime + +For other scenarios, such as background goroutines or standalone Go code, you should handle panics yourself using Go's standard panic recovery mechanisms. + +## The PanicDetails Struct + +When a panic occurs, Wails captures important information about the panic in a `PanicDetails` struct: + +```go +type PanicDetails struct { + StackTrace string // The stack trace of where the panic occurred. Potentially trimmed to provide more context + Error error // The error that caused the panic + Time time.Time // The time when the panic occurred + FullStackTrace string // The complete stack trace including runtime frames +} +``` + +This structure provides comprehensive information about the panic: +- `StackTrace`: A formatted string showing the call stack that led to the panic +- `Error`: The actual error or panic message +- `Time`: The exact time when the panic occurred +- `FullStackTrace`: The complete stack trace including runtime frames + +:::note[Panics in Service Code] + +When panics are caught in your Service code after being called from the frontend, the stack trace is trimmed to focus on exactly where in your code the panic occurred. +If you want to see the full stack trace, you can use the `FullStackTrace` field. + +::: + +## Default Panic Handler + +If you don't specify a custom panic handler, Wails will use its default handler which outputs error information in a formatted log message. For example: + +``` +Jan 16 21:18:05.649 ERR panic error: oh no! something went wrong deep in my service! :( +main.(*WindowService).call2 + at E:/wails/v3/examples/panic-handling/main.go:24 +main.(*WindowService).call1 + at E:/wails/v3/examples/panic-handling/main.go:20 +main.(*WindowService).GeneratePanic + at E:/wails/v3/examples/panic-handling/main.go:16 +``` + +## Custom Panic Handler + +You can implement your own panic handler by setting the `PanicHandler` option when creating your application. Here's an example: + +```go +app := application.New(application.Options{ + Name: "My App", + PanicHandler: func(panicDetails *application.PanicDetails) { + fmt.Printf("*** Custom Panic Handler ***\n") + fmt.Printf("Time: %s\n", panicDetails.Time) + fmt.Printf("Error: %s\n", panicDetails.Error) + fmt.Printf("Stacktrace: %s\n", panicDetails.StackTrace) + fmt.Printf("Full Stacktrace: %s\n", panicDetails.FullStackTrace) + + // You could also: + // - Log to a file + // - Send to a crash reporting service + // - Show a user-friendly error dialog + // - Attempt to recover or restart the application + }, +}) +``` + +For a complete working example of panic handling in a Wails application, see the panic-handling example in `v3/examples/panic-handling`. + +## Final Notes + +Remember that the Wails panic handler is specifically for managing panics in bound methods and internal runtime errors. For other parts of your application, you should use Go's standard error handling patterns and panic recovery mechanisms where appropriate. As with all Go applications, it's better to prevent panics through proper error handling where possible. diff --git a/v3/examples/panic-handling/README.md b/v3/examples/panic-handling/README.md new file mode 100644 index 00000000000..99068495f8a --- /dev/null +++ b/v3/examples/panic-handling/README.md @@ -0,0 +1,11 @@ +# Panic Handling Example + +This example is a demonstration of how to handle panics in your application. + +## Running the example + +To run the example, simply run the following command: + +```bash +go run . +``` diff --git a/v3/examples/panic-handling/assets/index.html b/v3/examples/panic-handling/assets/index.html new file mode 100644 index 00000000000..f4b5fe88608 --- /dev/null +++ b/v3/examples/panic-handling/assets/index.html @@ -0,0 +1,27 @@ + + + + + + Window Call Demo + + + + + + + + + \ No newline at end of file diff --git a/v3/examples/panic-handling/main.go b/v3/examples/panic-handling/main.go new file mode 100644 index 00000000000..1efcb153ee0 --- /dev/null +++ b/v3/examples/panic-handling/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "embed" + "fmt" + "github.com/wailsapp/wails/v3/pkg/application" + "log" +) + +//go:embed assets/* +var assets embed.FS + +type WindowService struct{} + +func (s *WindowService) GeneratePanic() { + s.call1() +} + +func (s *WindowService) call1() { + s.call2() +} + +func (s *WindowService) call2() { + panic("oh no! something went wrong deep in my service! :(") +} + +// ============================================== + +func main() { + app := application.New(application.Options{ + Name: "Panic Handler Demo", + Description: "A demo of Handling Panics", + Assets: application.AssetOptions{ + Handler: application.BundledAssetFileServer(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: false, + }, + Services: []application.Service{ + application.NewService(&WindowService{}), + }, + PanicHandler: func(panicDetails *application.PanicDetails) { + fmt.Printf("*** There was a panic! ***\n") + fmt.Printf("Time: %s\n", panicDetails.Time) + fmt.Printf("Error: %s\n", panicDetails.Error) + fmt.Printf("Stacktrace: %s\n", panicDetails.StackTrace) + }, + }) + + app.NewWebviewWindow(). + SetTitle("WebviewWindow 1"). + Show() + + err := app.Run() + + if err != nil { + log.Fatal(err) + } + +} diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 97937b59751..edd50402456 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -232,17 +232,6 @@ type ( } ) -func processPanicHandlerRecover() { - h := globalApplication.options.PanicHandler - if h == nil { - return - } - - if err := recover(); err != nil { - h(err) - } -} - // Messages sent from javascript get routed here type windowMessage struct { windowId uint @@ -486,7 +475,10 @@ func (a *App) OnApplicationEvent(eventType events.ApplicationEventType, callback } a.applicationEventListeners[eventID] = append(a.applicationEventListeners[eventID], listener) if a.impl != nil { - go a.impl.on(eventID) + go func() { + defer handlePanic() + a.impl.on(eventID) + }() } return func() { @@ -537,13 +529,19 @@ func (a *App) GetPID() int { func (a *App) info(message string, args ...any) { if a.Logger != nil { - go a.Logger.Info(message, args...) + go func() { + defer handlePanic() + a.Logger.Info(message, args...) + }() } } func (a *App) debug(message string, args ...any) { if a.Logger != nil { - go a.Logger.Debug(message, args...) + go func() { + defer handlePanic() + a.Logger.Debug(message, args...) + }() } } @@ -593,9 +591,6 @@ func (a *App) NewSystemTray() *SystemTray { func (a *App) Run() error { - // Setup panic handler - defer processPanicHandlerRecover() - // Call post-create hooks err := a.preRun() if err != nil { @@ -651,7 +646,10 @@ func (a *App) Run() error { a.running = true for _, systray := range a.pendingRun { - go systray.Run() + go func() { + defer handlePanic() + systray.Run() + }() } a.pendingRun = nil @@ -687,6 +685,7 @@ func (a *App) Run() error { } func (a *App) handleApplicationEvent(event *ApplicationEvent) { + defer handlePanic() a.applicationEventListenersLock.RLock() listeners, ok := a.applicationEventListeners[event.Id] a.applicationEventListenersLock.RUnlock() @@ -708,11 +707,15 @@ func (a *App) handleApplicationEvent(event *ApplicationEvent) { } for _, listener := range listeners { - go listener.callback(event) + go func() { + defer handlePanic() + listener.callback(event) + }() } } func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) { + defer handlePanic() // Get window from window map a.windowsLock.Lock() window, ok := a.windows[event.windowId] @@ -726,6 +729,7 @@ func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) { } func (a *App) handleWindowMessage(event *windowMessage) { + defer handlePanic() // Get window from window map a.windowsLock.RLock() window, ok := a.windows[event.windowId] @@ -745,10 +749,12 @@ func (a *App) handleWindowMessage(event *windowMessage) { } func (a *App) handleWebViewRequest(request *webViewAssetRequest) { + defer handlePanic() a.assets.ServeWebViewRequest(request) } func (a *App) handleWindowEvent(event *windowEvent) { + defer handlePanic() // Get window from window map a.windowsLock.RLock() window, ok := a.windows[event.WindowID] @@ -761,6 +767,8 @@ func (a *App) handleWindowEvent(event *windowEvent) { } func (a *App) handleMenuItemClicked(menuItemID uint) { + defer handlePanic() + menuItem := getMenuItemByID(menuItemID) if menuItem == nil { log.Printf("MenuItem #%d not found", menuItemID) @@ -1022,6 +1030,7 @@ func (a *App) removeKeyBinding(acceleratorString string) { } func (a *App) handleWindowKeyEvent(event *windowKeyEvent) { + defer handlePanic() // Get window from window map a.windowsLock.RLock() window, ok := a.windows[event.windowId] diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index 19f789c05af..c56b668a16e 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -138,6 +138,7 @@ func (a *linuxApp) isDarkMode() bool { func (a *linuxApp) monitorThemeChanges() { go func() { + defer handlePanic() conn, err := dbus.ConnectSessionBus() if err != nil { a.parent.info("[WARNING] Failed to connect to session bus; monitoring for theme changes will not function:", err) diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go index 25328c9c6f5..57643bb0277 100644 --- a/v3/pkg/application/application_options.go +++ b/v3/pkg/application/application_options.go @@ -84,7 +84,7 @@ type Options struct { Flags map[string]any // PanicHandler is called when a panic occurs - PanicHandler func(any) + PanicHandler func(*PanicDetails) // DisableDefaultSignalHandler disables the default signal handler DisableDefaultSignalHandler bool diff --git a/v3/pkg/application/bindings.go b/v3/pkg/application/bindings.go index cc098fc1dd9..e690e757650 100644 --- a/v3/pkg/application/bindings.go +++ b/v3/pkg/application/bindings.go @@ -280,24 +280,7 @@ var errorType = reflect.TypeFor[error]() // Call will attempt to call this bound method with the given args func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnValue interface{}, err error) { // Use a defer statement to capture panics - defer func() { - if r := recover(); r != nil { - if str, ok := r.(string); ok { - if strings.HasPrefix(str, "reflect: Call using") { - // Remove prefix - str = strings.Replace(str, "reflect: Call using ", "", 1) - // Split on "as" - parts := strings.Split(str, " as type ") - if len(parts) == 2 { - err = fmt.Errorf("invalid argument type: got '%s', expected '%s'", parts[0], parts[1]) - return - } - } - } - err = fmt.Errorf("%v", r) - } - }() - + defer handlePanic(handlePanicOptions{skipEnd: 5}) argCount := len(args) if b.needsContext { argCount++ @@ -309,7 +292,6 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV } // Convert inputs to values of appropriate type - callArgs := make([]reflect.Value, argCount) base := 0 diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go index 4d0e5374342..239ae9d76a8 100644 --- a/v3/pkg/application/dialogs_linux.go +++ b/v3/pkg/application/dialogs_linux.go @@ -35,7 +35,10 @@ func (m *linuxDialog) show() { if response >= 0 && response < len(m.dialog.Buttons) { button := m.dialog.Buttons[response] if button.Callback != nil { - go button.Callback() + go func() { + defer handlePanic() + button.Callback() + }() } } }) diff --git a/v3/pkg/application/dialogs_windows.go b/v3/pkg/application/dialogs_windows.go index f96484e0c7f..d944cea5a2b 100644 --- a/v3/pkg/application/dialogs_windows.go +++ b/v3/pkg/application/dialogs_windows.go @@ -152,6 +152,7 @@ func (m *windowOpenFileDialog) show() (chan string, error) { files := make(chan string) go func() { + defer handlePanic() for _, file := range result { files <- file } @@ -196,6 +197,7 @@ func (m *windowSaveFileDialog) show() (chan string, error) { return cfd.NewSaveFileDialog(config) }, false) go func() { + defer handlePanic() files <- result.(string) close(files) }() diff --git a/v3/pkg/application/events.go b/v3/pkg/application/events.go index 3accabd89de..2b5e64713bf 100644 --- a/v3/pkg/application/events.go +++ b/v3/pkg/application/events.go @@ -131,8 +131,14 @@ func (e *EventProcessor) Emit(thisEvent *CustomEvent) { } } - go e.dispatchEventToListeners(thisEvent) - go e.dispatchEventToWindows(thisEvent) + go func() { + defer handlePanic() + e.dispatchEventToListeners(thisEvent) + }() + go func() { + defer handlePanic() + e.dispatchEventToWindows(thisEvent) + }() } func (e *EventProcessor) Off(eventName string) { @@ -219,7 +225,10 @@ func (e *EventProcessor) dispatchEventToListeners(event *CustomEvent) { if listener.counter > 0 { listener.counter-- } - go listener.callback(event) + go func() { + defer handlePanic() + listener.callback(event) + }() if listener.counter == 0 { listener.delete = true diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index acc4ac19244..86d930f0360 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -1712,6 +1712,7 @@ func runChooserDialog(window pointer, allowMultiple, createFolders, showHidden b InvokeAsync(func() { response := C.gtk_dialog_run((*C.GtkDialog)(fc)) go func() { + defer handlePanic() if response == C.GTK_RESPONSE_ACCEPT { filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc)) iter := filenames diff --git a/v3/pkg/application/mainthread.go b/v3/pkg/application/mainthread.go index b76663aa2e0..6eb40ba9d14 100644 --- a/v3/pkg/application/mainthread.go +++ b/v3/pkg/application/mainthread.go @@ -24,7 +24,7 @@ func InvokeSync(fn func()) { var wg sync.WaitGroup wg.Add(1) globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() + defer handlePanic() fn() wg.Done() }) @@ -35,7 +35,7 @@ func InvokeSyncWithResult[T any](fn func() T) (res T) { var wg sync.WaitGroup wg.Add(1) globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() + defer handlePanic() res = fn() wg.Done() }) @@ -47,7 +47,7 @@ func InvokeSyncWithError(fn func() error) (err error) { var wg sync.WaitGroup wg.Add(1) globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() + defer handlePanic() err = fn() wg.Done() }) @@ -59,7 +59,7 @@ func InvokeSyncWithResultAndError[T any](fn func() (T, error)) (res T, err error var wg sync.WaitGroup wg.Add(1) globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() + defer handlePanic() res, err = fn() wg.Done() }) @@ -71,7 +71,7 @@ func InvokeSyncWithResultAndOther[T any, U any](fn func() (T, U)) (res T, other var wg sync.WaitGroup wg.Add(1) globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() + defer handlePanic() res, other = fn() wg.Done() }) @@ -81,7 +81,7 @@ func InvokeSyncWithResultAndOther[T any, U any](fn func() (T, U)) (res T, other func InvokeAsync(fn func()) { globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() + defer handlePanic() fn() }) } diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index 515851a086b..7bce23ee79e 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -262,7 +262,10 @@ func (m *MenuItem) handleClick() { } } if m.callback != nil { - go m.callback(ctx) + go func() { + defer handlePanic() + m.callback(ctx) + }() } } diff --git a/v3/pkg/application/messageprocessor.go b/v3/pkg/application/messageprocessor.go index dd47ddf17c4..1ba2368bfdc 100644 --- a/v3/pkg/application/messageprocessor.go +++ b/v3/pkg/application/messageprocessor.go @@ -83,6 +83,11 @@ func (m *MessageProcessor) ServeHTTP(rw http.ResponseWriter, r *http.Request) { } func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *http.Request) { + defer func() { + if handlePanic() { + rw.WriteHeader(http.StatusInternalServerError) + } + }() object, err := strconv.Atoi(r.URL.Query().Get("object")) if err != nil { m.httpError(rw, "Error decoding object value: "+err.Error()) diff --git a/v3/pkg/application/messageprocessor_call.go b/v3/pkg/application/messageprocessor_call.go index 4848d64180c..26ea5188e8a 100644 --- a/v3/pkg/application/messageprocessor_call.go +++ b/v3/pkg/application/messageprocessor_call.go @@ -104,6 +104,7 @@ func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter, } go func() { + defer handlePanic() defer func() { cancel() @@ -114,7 +115,8 @@ func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter, result, err := boundMethod.Call(ctx, options.Args) if err != nil { - m.callErrorCallback(window, "Error calling method: %s", callID, err) + msg := fmt.Sprintf("Error calling method '%v'", boundMethod.Name) + m.callErrorCallback(window, msg+": %s", callID, err) return } var jsonResult = []byte("{}") diff --git a/v3/pkg/application/messageprocessor_dialog.go b/v3/pkg/application/messageprocessor_dialog.go index bbddd4e6b43..8e0089ddbba 100644 --- a/v3/pkg/application/messageprocessor_dialog.go +++ b/v3/pkg/application/messageprocessor_dialog.go @@ -107,6 +107,7 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite dialog := OpenFileDialogWithOptions(&options) go func() { + defer handlePanic() if options.AllowsMultipleSelection { files, err := dialog.PromptForMultipleSelection() if err != nil { @@ -148,6 +149,7 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite dialog := SaveFileDialogWithOptions(&options) go func() { + defer handlePanic() file, err := dialog.PromptForSingleSelection() if err != nil { m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err) diff --git a/v3/pkg/application/panic_handler.go b/v3/pkg/application/panic_handler.go new file mode 100644 index 00000000000..de2d9ab3df9 --- /dev/null +++ b/v3/pkg/application/panic_handler.go @@ -0,0 +1,107 @@ +package application + +import ( + "fmt" + "runtime" + "runtime/debug" + "strings" + "time" +) + +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 +} + +type PanicDetails struct { + StackTrace string + Error error + Time time.Time + FullStackTrace string +} + +func newPanicDetails(err error, trace string) *PanicDetails { + return &PanicDetails{ + Error: err, + Time: time.Now(), + StackTrace: trace, + FullStackTrace: string(debug.Stack()), + } +} + +// handlePanic handles any panics +// Returns the error if there was one +func handlePanic(options ...handlePanicOptions) bool { + // Try to recover + e := recover() + if e == nil { + return false + } + + // Get the error + var err error + if errPanic, ok := e.(error); ok { + err = errPanic + } else { + err = fmt.Errorf("%v", e) + } + + // Get the stack trace + var stackTrace string + skipEnd := 0 + if len(options) > 0 { + skipEnd = options[0].skipEnd + } + stackTrace = getStackTrace(3, skipEnd) + + processPanic(newPanicDetails(err, stackTrace)) + return false +} + +func processPanic(panicDetails *PanicDetails) { + h := globalApplication.options.PanicHandler + if h != nil { + h(panicDetails) + return + } + defaultPanicHandler(panicDetails) +} + +func defaultPanicHandler(panicDetails *PanicDetails) { + errorMessage := fmt.Sprintf("panic error: %s\n%s", panicDetails.Error.Error(), panicDetails.StackTrace) + globalApplication.error(errorMessage) +} diff --git a/v3/pkg/application/single_instance.go b/v3/pkg/application/single_instance.go index 43270ede16e..24bbf5c3191 100644 --- a/v3/pkg/application/single_instance.go +++ b/v3/pkg/application/single_instance.go @@ -74,6 +74,7 @@ func newSingleInstanceManager(app *App, options *SingleInstanceOptions) (*single // Launch second instance data listener once.Do(func() { go func() { + defer handlePanic() for encryptedData := range secondInstanceBuffer { var secondInstanceData SecondInstanceData var jsonData []byte diff --git a/v3/pkg/application/systemtray.go b/v3/pkg/application/systemtray.go index fa117e51cea..2df93b64a32 100644 --- a/v3/pkg/application/systemtray.go +++ b/v3/pkg/application/systemtray.go @@ -109,6 +109,7 @@ func (s *SystemTray) Run() { } s.attachedWindow.justClosed = true go func() { + defer handlePanic() time.Sleep(s.attachedWindow.Debounce) s.attachedWindow.justClosed = false }() diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index ad6fa358d8c..10908e7e022 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -331,6 +331,7 @@ func (s *linuxSystemTray) run() { } s.setLabel(s.label) go func() { + defer handlePanic() s.register() if err := conn.AddMatchSignal( diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index a49511d12e6..866b8fa7054 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -793,7 +793,10 @@ func (w *WebviewWindow) HandleWindowEvent(id uint) { } for _, listener := range w.eventListeners[id] { - go listener.callback(thisEvent) + go func() { + defer handlePanic() + listener.callback(thisEvent) + }() } w.dispatchWindowEvent(id) } @@ -1261,7 +1264,10 @@ func (w *WebviewWindow) processKeyBinding(acceleratorString string) bool { defer w.keyBindingsLock.RUnlock() if callback := w.keyBindings[acceleratorString]; callback != nil { // Execute callback - go callback(w) + go func() { + defer handlePanic() + callback(w) + }() return true } }