fix: prevent window menu crash on Wayland (#4769) (#4770)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lea Anthony 2025-12-12 17:59:47 +11:00 committed by GitHub
commit e80cf28578
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 205 additions and 17 deletions

View file

@ -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
<!-- New features, capabilities, or enhancements -->
## Changed
<!-- Changes in existing functionality -->
## 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
<!-- Bug fixes -->
## Deprecated

View file

@ -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 {

View file

@ -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),

View file

@ -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())
}

View file

@ -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(&gtkWidgetGetWindow, gtk, "gtk_widget_get_window")
purego.RegisterLibFunc(&gtkWidgetHide, gtk, "gtk_widget_hide")
purego.RegisterLibFunc(&gtkWidgetIsVisible, gtk, "gtk_widget_is_visible")
purego.RegisterLibFunc(&gtkWidgetRealize, gtk, "gtk_widget_realize")
purego.RegisterLibFunc(&gtkWidgetSetAppPaintable, gtk, "gtk_widget_set_app_paintable")
purego.RegisterLibFunc(&gtkWidgetSetName, gtk, "gtk_widget_set_name")
purego.RegisterLibFunc(&gtkWidgetSetSensitive, 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))
}

View file

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

View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Menu Wayland Test (#4769)</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
text-align: center;
padding: 2rem;
background: rgba(255,255,255,0.1);
border-radius: 1rem;
backdrop-filter: blur(10px);
}
h1 {
margin: 0 0 1rem 0;
font-size: 2rem;
}
p {
margin: 0.5rem 0;
opacity: 0.9;
}
.success {
color: #90EE90;
font-weight: bold;
font-size: 1.2rem;
margin-top: 1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-family: monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>Menu Wayland Test</h1>
<p>GitHub Issue: <code>#4769</code></p>
<p>This tests the window menu on Wayland.</p>
<p class="success">If you can see this window with the menu bar above, the fix works!</p>
<p style="margin-top: 1.5rem; font-size: 0.9rem; opacity: 0.7;">
Try clicking the menu items (File, Edit, Help) to verify they work.
</p>
</div>
</body>
</html>

70
v3/test/4769-menu/main.go Normal file
View file

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