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) + } +}