wails/v3/pkg/application/application_linux.go
Lea Anthony e80cf28578
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>
2025-12-12 17:59:47 +11:00

347 lines
9.1 KiB
Go

//go:build linux && !android
package application
/*
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
static guint get_compiled_gtk_major_version() { return GTK_MAJOR_VERSION; }
static guint get_compiled_gtk_minor_version() { return GTK_MINOR_VERSION; }
static guint get_compiled_gtk_micro_version() { return GTK_MICRO_VERSION; }
static guint get_compiled_webkit_major_version() { return WEBKIT_MAJOR_VERSION; }
static guint get_compiled_webkit_minor_version() { return WEBKIT_MINOR_VERSION; }
static guint get_compiled_webkit_micro_version() { return WEBKIT_MICRO_VERSION; }
*/
import "C"
import (
"fmt"
"os"
"regexp"
"slices"
"strings"
"sync"
"path/filepath"
"github.com/godbus/dbus/v5"
"github.com/wailsapp/wails/v3/internal/operatingsystem"
"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
if os.Getenv("GDK_BACKEND") == "" &&
(os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") {
_ = os.Setenv("GDK_BACKEND", "x11")
}
// Disable DMA-BUF renderer on Wayland with NVIDIA to prevent "Error 71 (Protocol error)" crashes.
// This is a known WebKitGTK issue with NVIDIA proprietary drivers on Wayland.
// See: https://bugs.webkit.org/show_bug.cgi?id=262607
if os.Getenv("WEBKIT_DISABLE_DMABUF_RENDERER") == "" &&
os.Getenv("XDG_SESSION_TYPE") == "wayland" &&
isNVIDIAGPU() {
_ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
}
}
// isNVIDIAGPU checks if an NVIDIA GPU is present by looking for the nvidia kernel module.
func isNVIDIAGPU() bool {
// Check if nvidia module is loaded (most reliable for proprietary driver)
if _, err := os.Stat("/sys/module/nvidia"); err == nil {
return true
}
return false
}
type linuxApp struct {
application pointer
parent *App
startupActions []func()
// Native -> uint
windowMap map[windowPointer]uint
windowMapLock sync.Mutex
theme string
icon pointer
}
func (a *linuxApp) GetFlags(options Options) map[string]any {
if options.Flags == nil {
options.Flags = make(map[string]any)
}
return options.Flags
}
func getNativeApplication() *linuxApp {
return globalApplication.impl.(*linuxApp)
}
func (a *linuxApp) hide() {
a.hideAllWindows()
}
func (a *linuxApp) show() {
a.showAllWindows()
}
func (a *linuxApp) on(eventID uint) {
// TODO: Test register/unregister events
//C.registerApplicationEvent(l.application, C.uint(eventID))
}
func (a *linuxApp) name() string {
return appName()
}
type rnr struct {
f func()
}
func (r rnr) run() {
r.f()
}
func (a *linuxApp) setApplicationMenu(menu *Menu) {
// FIXME: How do we avoid putting a menu?
if menu == nil {
// Create a default menu
menu = DefaultApplicationMenu()
globalApplication.applicationMenu = menu
}
}
func (a *linuxApp) run() error {
if len(os.Args) == 2 { // Case: program + 1 argument
arg1 := os.Args[1]
// Check if the argument is likely a URL from a custom protocol invocation
if strings.Contains(arg1, "://") {
a.parent.info("Application launched with argument, potentially a URL from custom protocol", "url", arg1)
eventContext := newApplicationEventContext()
eventContext.setURL(arg1)
applicationEvents <- &ApplicationEvent{
Id: uint(events.Common.ApplicationLaunchedWithUrl),
ctx: eventContext,
}
} else {
// Check if the argument matches any file associations
if a.parent.options.FileAssociations != nil {
ext := filepath.Ext(arg1)
if slices.Contains(a.parent.options.FileAssociations, ext) {
a.parent.info("File opened via file association", "file", arg1, "extension", ext)
eventContext := newApplicationEventContext()
eventContext.setOpenedWithFile(arg1)
applicationEvents <- &ApplicationEvent{
Id: uint(events.Common.ApplicationOpenedWithFile),
ctx: eventContext,
}
return nil
}
}
a.parent.info("Application launched with single argument (not a URL), potential file open?", "arg", arg1)
}
} else if len(os.Args) > 2 {
// Log if multiple arguments are passed
a.parent.info("Application launched with multiple arguments", "args", os.Args[1:])
}
a.parent.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(evt *ApplicationEvent) {
// TODO: What should happen here?
})
a.setupCommonEvents()
a.monitorThemeChanges()
return appRun(a.application)
}
func (a *linuxApp) unregisterWindow(w windowPointer) {
a.windowMapLock.Lock()
delete(a.windowMap, w)
a.windowMapLock.Unlock()
// If this was the last window...
if len(a.windowMap) == 0 && !a.parent.options.Linux.DisableQuitOnLastWindowClosed {
a.destroy()
}
}
func (a *linuxApp) destroy() {
if !globalApplication.shouldQuit() {
return
}
globalApplication.cleanup()
appDestroy(a.application)
}
func (a *linuxApp) isOnMainThread() bool {
return isOnMainThread()
}
// register our window to our parent mapping
func (a *linuxApp) registerWindow(window pointer, id uint) {
a.windowMapLock.Lock()
a.windowMap[windowPointer(window)] = id
a.windowMapLock.Unlock()
}
func (a *linuxApp) isDarkMode() bool {
return strings.Contains(a.theme, "dark")
}
func (a *linuxApp) getAccentColor() string {
// Linux doesn't have a unified system accent color API
// Return a default blue color
return "rgb(0,122,255)"
}
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,
)
return
}
defer conn.Close()
if err = conn.AddMatchSignal(
dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
); err != nil {
panic(err)
}
c := make(chan *dbus.Signal, 10)
conn.Signal(c)
getTheme := func(body []interface{}) (string, bool) {
if len(body) < 2 {
return "", false
}
if entry, ok := body[0].(string); !ok || entry != "org.gnome.desktop.interface" {
return "", false
}
if entry, ok := body[1].(string); ok && entry == "color-scheme" {
return body[2].(dbus.Variant).Value().(string), true
}
return "", false
}
for v := range c {
theme, ok := getTheme(v.Body)
if !ok {
continue
}
if theme != a.theme {
a.theme = theme
event := newApplicationEvent(events.Linux.SystemThemeChanged)
event.Context().setIsDarkMode(a.isDarkMode())
applicationEvents <- event
}
}
}()
}
func newPlatformApp(parent *App) *linuxApp {
name := sanitizeAppName(parent.options.Name)
app := &linuxApp{
parent: parent,
application: appNew(name),
windowMap: map[windowPointer]uint{},
}
if parent.options.Linux.ProgramName != "" {
setProgramName(parent.options.Linux.ProgramName)
}
return app
}
// logPlatformInfo logs the platform information to the console
func (a *App) logPlatformInfo() {
info, err := operatingsystem.Info()
if err != nil {
a.error("error getting OS info: %w", err)
return
}
wkVersion := operatingsystem.GetWebkitVersion()
platformInfo := info.AsLogSlice()
platformInfo = append(platformInfo, "Webkit2Gtk", wkVersion)
a.info("Platform Info:", platformInfo...)
}
//export processWindowEvent
func processWindowEvent(windowID C.uint, eventID C.uint) {
windowEvents <- &windowEvent{
WindowID: uint(windowID),
EventID: uint(eventID),
}
}
func buildVersionString(major, minor, micro C.uint) string {
return fmt.Sprintf("%d.%d.%d", uint(major), uint(minor), uint(micro))
}
func (a *App) platformEnvironment() map[string]any {
result := map[string]any{}
result["gtk3-compiled"] = buildVersionString(
C.get_compiled_gtk_major_version(),
C.get_compiled_gtk_minor_version(),
C.get_compiled_gtk_micro_version(),
)
result["gtk3-runtime"] = buildVersionString(
C.gtk_get_major_version(),
C.gtk_get_minor_version(),
C.gtk_get_micro_version(),
)
result["webkit2gtk-compiled"] = buildVersionString(
C.get_compiled_webkit_major_version(),
C.get_compiled_webkit_minor_version(),
C.get_compiled_webkit_micro_version(),
)
result["webkit2gtk-runtime"] = buildVersionString(
C.webkit_get_major_version(),
C.webkit_get_minor_version(),
C.webkit_get_micro_version(),
)
return result
}
func fatalHandler(errFunc func(error)) {
// Stub for windows function
return
}