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 @@ + + +
+ + +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. +
+