From e80cf2857875295eb781150a61b4be019965b79c Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Fri, 12 Dec 2025 17:59:47 +1100 Subject: [PATCH] fix: prevent window menu crash on Wayland (#4769) (#4770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent window menu crash on Wayland by realizing window before showing On Wayland with GTK3, the appmenu-gtk-module tries to set DBus properties for global menu integration before the window is fully realized, causing a crash with the error "GDK_IS_WAYLAND_WINDOW (window) assertion failed". The fix calls gtk_widget_realize() before gtk_widget_show_all() to ensure the window has a valid GdkWindow when the menu system accesses it. This fix is applied to both the CGO and purego implementations. Fixes wailsapp/wails#4769 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * feat: add Wayland detection to wails3 doctor and test for #4769 - Add Wayland session detection row to `wails3 doctor` output on Linux - Create test project v3/test/4769-menu to manually verify the menu fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix: sanitize GTK application name to prevent crashes with invalid characters Application names containing spaces, parentheses, hash symbols, or other invalid characters would cause GTK to fail with an assertion error. Added sanitizeAppName() function that: - Replaces invalid characters with underscores - Handles leading digits - Removes consecutive underscores - Defaults to "wailsapp" if empty 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- v3/UNRELEASED_CHANGELOG.md | 3 ++ v3/internal/doctor/doctor_linux.go | 11 ++++ v3/pkg/application/application_linux.go | 30 +++++++++-- v3/pkg/application/linux_cgo.go | 13 +++-- v3/pkg/application/linux_purego.go | 14 +++-- v3/test/4769-menu/Taskfile.yml | 22 ++++++++ v3/test/4769-menu/assets/index.html | 59 +++++++++++++++++++++ v3/test/4769-menu/main.go | 70 +++++++++++++++++++++++++ 8 files changed, 205 insertions(+), 17 deletions(-) create mode 100644 v3/test/4769-menu/Taskfile.yml create mode 100644 v3/test/4769-menu/assets/index.html create mode 100644 v3/test/4769-menu/main.go diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 8e4648038..51fe477f8 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -16,12 +16,15 @@ After processing, the content will be moved to the main changelog and this file --> ## Added +- Add `XDG_SESSION_TYPE` to `wails3 doctor` output on Linux by @leaanthony ## Changed ## Fixed +- Fix window menu crash on Wayland caused by appmenu-gtk-module accessing unrealized window (#4769) by @leaanthony +- Fix GTK application crash when app name contains invalid characters (spaces, parentheses, etc.) by @leaanthony ## Deprecated diff --git a/v3/internal/doctor/doctor_linux.go b/v3/internal/doctor/doctor_linux.go index e72528c4b..65f8fe27a 100644 --- a/v3/internal/doctor/doctor_linux.go +++ b/v3/internal/doctor/doctor_linux.go @@ -13,12 +13,23 @@ import ( func getInfo() (map[string]string, bool) { result := make(map[string]string) + // Check session type (X11/Wayland) + result["XDG_SESSION_TYPE"] = getSessionType() + // Check for NVIDIA driver result["NVIDIA Driver"] = getNvidiaDriverInfo() return result, true } +func getSessionType() string { + sessionType := os.Getenv("XDG_SESSION_TYPE") + if sessionType == "" { + return "unset" + } + return sessionType +} + func getNvidiaDriverInfo() string { version, err := os.ReadFile("/sys/module/nvidia/version") if err != nil { diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index d0a32d1c5..f0e14f47d 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -16,6 +16,7 @@ import "C" import ( "fmt" "os" + "regexp" "slices" "strings" "sync" @@ -27,6 +28,29 @@ import ( "github.com/wailsapp/wails/v3/pkg/events" ) +// sanitizeAppName sanitizes the application name to be a valid GTK/D-Bus application ID. +// Valid IDs contain only alphanumeric characters, hyphens, and underscores. +// They must not start with a digit. +var invalidAppNameChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`) +var leadingDigits = regexp.MustCompile(`^[0-9]+`) + +func sanitizeAppName(name string) string { + // Replace invalid characters with underscores + name = invalidAppNameChars.ReplaceAllString(name, "_") + // Prefix with underscore if starts with digit + name = leadingDigits.ReplaceAllString(name, "_$0") + // Remove consecutive underscores + for strings.Contains(name, "__") { + name = strings.ReplaceAll(name, "__", "_") + } + // Trim leading/trailing underscores + name = strings.Trim(name, "_") + if name == "" { + name = "wailsapp" + } + return strings.ToLower(name) +} + func init() { // FIXME: This should be handled appropriately in the individual files most likely. // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings @@ -250,11 +274,7 @@ func (a *linuxApp) monitorThemeChanges() { } func newPlatformApp(parent *App) *linuxApp { - - name := strings.ToLower(strings.Replace(parent.options.Name, " ", "", -1)) - if name == "" { - name = "undefined" - } + name := sanitizeAppName(parent.options.Name) app := &linuxApp{ parent: parent, application: appNew(name), diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index d0c31a4cf..109ee6214 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -4,7 +4,6 @@ package application import ( "fmt" - "regexp" "strings" "sync" "time" @@ -385,12 +384,7 @@ func appName() string { func appNew(name string) pointer { C.install_signal_handlers() - // prevent leading number - if matched, _ := regexp.MatchString(`^\d+`, name); matched { - name = fmt.Sprintf("_%s", name) - } - name = strings.Replace(name, "(", "_", -1) - name = strings.Replace(name, ")", "_", -1) + // Name is already sanitized by sanitizeAppName() in application_linux.go appId := fmt.Sprintf("org.wails.%s", name) nameC := C.CString(appId) defer C.free(unsafe.Pointer(nameC)) @@ -1169,6 +1163,11 @@ func (w *linuxWebviewWindow) windowShow() { if w.gtkWidget() == nil { return } + // Realize the window first to ensure it has a valid GdkWindow. + // This prevents crashes on Wayland when appmenu-gtk-module tries to + // set DBus properties for global menu integration before the window + // is fully realized. See: https://github.com/wailsapp/wails/issues/4769 + C.gtk_widget_realize(w.gtkWidget()) C.gtk_widget_show_all(w.gtkWidget()) } diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index f5edf2e59..5d6c1cd95 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -178,6 +178,7 @@ var ( gtkWidgetGetWindow func(pointer) pointer gtkWidgetHide func(pointer) gtkWidgetIsVisible func(pointer) bool + gtkWidgetRealize func(pointer) gtkWidgetShow func(pointer) gtkWidgetShowAll func(pointer) gtkWidgetSetAppPaintable func(pointer, int) @@ -330,6 +331,7 @@ func init() { purego.RegisterLibFunc(>kWidgetGetWindow, gtk, "gtk_widget_get_window") purego.RegisterLibFunc(>kWidgetHide, gtk, "gtk_widget_hide") purego.RegisterLibFunc(>kWidgetIsVisible, gtk, "gtk_widget_is_visible") + purego.RegisterLibFunc(>kWidgetRealize, gtk, "gtk_widget_realize") purego.RegisterLibFunc(>kWidgetSetAppPaintable, gtk, "gtk_widget_set_app_paintable") purego.RegisterLibFunc(>kWidgetSetName, gtk, "gtk_widget_set_name") purego.RegisterLibFunc(>kWidgetSetSensitive, gtk, "gtk_widget_set_sensitive") @@ -393,11 +395,8 @@ func appName() string { func appNew(name string) pointer { GApplicationDefaultFlags := uint(0) - name = strings.ToLower(name) - if name == "" { - name = "undefined" - } - identifier := fmt.Sprintf("org.wails.%s", strings.Replace(name, " ", "-", -1)) + // Name is already sanitized by sanitizeAppName() in application_linux.go + identifier := fmt.Sprintf("org.wails.%s", name) return pointer(gtkApplicationNew(identifier, GApplicationDefaultFlags)) } @@ -863,6 +862,11 @@ func windowResize(window pointer, width, height int) { } func windowShow(window pointer) { + // Realize the window first to ensure it has a valid GdkWindow. + // This prevents crashes on Wayland when appmenu-gtk-module tries to + // set DBus properties for global menu integration before the window + // is fully realized. See: https://github.com/wailsapp/wails/issues/4769 + gtkWidgetRealize(pointer(window)) gtkWidgetShowAll(pointer(window)) } diff --git a/v3/test/4769-menu/Taskfile.yml b/v3/test/4769-menu/Taskfile.yml new file mode 100644 index 000000000..e32f90e51 --- /dev/null +++ b/v3/test/4769-menu/Taskfile.yml @@ -0,0 +1,22 @@ +version: '3' + +vars: + APP_NAME: "4769-menu{{exeExt}}" + +tasks: + build: + summary: Builds the test application + cmds: + - go build -o bin/{{.APP_NAME}} . + + run: + summary: Runs the test application + deps: + - build + cmds: + - ./bin/{{.APP_NAME}} + + dev: + summary: Builds and runs the test application + cmds: + - go run . diff --git a/v3/test/4769-menu/assets/index.html b/v3/test/4769-menu/assets/index.html new file mode 100644 index 000000000..d09369614 --- /dev/null +++ b/v3/test/4769-menu/assets/index.html @@ -0,0 +1,59 @@ + + + + + + Menu Wayland Test (#4769) + + + +
+

Menu Wayland Test

+

GitHub Issue: #4769

+

This tests the window menu on Wayland.

+

If you can see this window with the menu bar above, the fix works!

+

+ Try clicking the menu items (File, Edit, Help) to verify they work. +

+
+ + diff --git a/v3/test/4769-menu/main.go b/v3/test/4769-menu/main.go new file mode 100644 index 000000000..59a196ebe --- /dev/null +++ b/v3/test/4769-menu/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "embed" + _ "embed" + "log" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed assets/* +var assets embed.FS + +func main() { + app := application.New(application.Options{ + Name: "Menu Wayland Test (#4769)", + Description: "Test for window menu crash on Wayland", + Assets: application.AssetOptions{ + Handler: application.BundledAssetFileServer(assets), + }, + }) + + // Create a menu - this would crash on Wayland before the fix + menu := app.NewMenu() + + fileMenu := menu.AddSubmenu("File") + fileMenu.Add("New").OnClick(func(ctx *application.Context) { + log.Println("New clicked") + }) + fileMenu.Add("Open").OnClick(func(ctx *application.Context) { + log.Println("Open clicked") + }) + fileMenu.AddSeparator() + fileMenu.Add("Exit").OnClick(func(ctx *application.Context) { + app.Quit() + }) + + editMenu := menu.AddSubmenu("Edit") + editMenu.Add("Cut").OnClick(func(ctx *application.Context) { + log.Println("Cut clicked") + }) + editMenu.Add("Copy").OnClick(func(ctx *application.Context) { + log.Println("Copy clicked") + }) + editMenu.Add("Paste").OnClick(func(ctx *application.Context) { + log.Println("Paste clicked") + }) + + helpMenu := menu.AddSubmenu("Help") + helpMenu.Add("About").OnClick(func(ctx *application.Context) { + log.Println("About clicked") + }) + + // Create window with menu attached via Linux options + // This tests the fix for issue #4769 + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Menu Wayland Test (#4769)", + Width: 800, + Height: 600, + Linux: application.LinuxWindow{ + Menu: menu, + }, + }) + + log.Println("Starting application - if you see this on Wayland, the fix works!") + err := app.Run() + if err != nil { + log.Fatal(err) + } +}