Skip to content

Commit 0c8e5a7

Browse files
authored
fix(v3/linux): wire common events + dbus monitors on GTK4 default path (#5474) (#5477)
* fix(v3/linux): wire common events + dbus monitors on GTK4 default path (#5474) The GTK4 default build's `linuxApp.run()` (the post-#5463 default in alpha.93) never called `setupCommonEvents()`, `monitorThemeChanges()`, or `monitorPowerEvents()`. As a result, listeners registered for `events.Common.ApplicationStarted`, `Common.ThemeChanged`, `Common.SystemWillSleep` and `Common.SystemDidWake` never fired, since nothing forwarded the underlying `events.Linux.*` events to their common counterparts. Reverting to alpha.92 worked because the gtk3 default already wired these in `application_linux_gtk3.go` (renamed from the previous `application_linux.go`). Restore parity by calling all three from the gtk4 `run()`, and extract the two dbus monitor helpers to a build-tag-shared `application_linux_dbus.go` so both stacks use the same implementation. Verified with the existing `pkg/application/internal/tests/services/*` suite on lin-node1: the startup/shutdown tests hang under the pre-fix GTK4 default (`Common.ApplicationStarted` never fires, listener never calls `app.Quit()`) and pass after the fix on both default and `-tags gtk3` builds. * fix(v3): address review on #5477 - application_linux.go run(): drop a.monitorThemeChanges() — the GTK4 default already has go listenForSystemThemeChanges(a) running from init(), which uses the portal-standard org.freedesktop.appearance namespace, so calling both would double-fire Common.ThemeChanged. monitorThemeChanges() stays in the shared dbus file because GTK3 still uses it. Updates the changelog to reflect that only the power-monitor helper is newly shared. - application_linux.go isDarkMode(): type-check result.Value().(dbus.Variant) instead of an unchecked assertion that would panic on unexpected portal payload shapes. - application_linux_dbus.go monitorThemeChanges(): replace panic(err) on AddMatchSignal failure with the same warning-and-return pattern used by the surrounding dbus setup, so a session-bus policy error disables theme monitoring instead of killing the app.
1 parent b60578e commit 0c8e5a7

4 files changed

Lines changed: 162 additions & 143 deletions

File tree

v3/UNRELEASED_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file
2323

2424
## Fixed
2525
<!-- Bug fixes -->
26+
- Fix `events.Common.ApplicationStarted`, `Common.ThemeChanged`, `Common.SystemWillSleep` and `Common.SystemDidWake` not firing on Linux after the GTK4 + WebKitGTK 6.0 stack was promoted to the default in alpha.93. The new default `application_linux.go` `run()` wasn't calling `setupCommonEvents()` (which forwards `Linux.*` events to their `Common.*` counterparts) or `monitorPowerEvents()`. The DBus power-monitor helper is now shared between the GTK3 and GTK4 build paths via `application_linux_dbus.go`. (#5474)
2627

2728
## Deprecated
2829
<!-- Soon-to-be removed features -->

v3/pkg/application/application_linux.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ func (a *linuxApp) run() error {
9292
a.parent.handleError(err)
9393
}
9494
})
95+
a.setupCommonEvents()
96+
// Theme changes are already monitored by listenForSystemThemeChanges via init();
97+
// it uses the portal-standard org.freedesktop.appearance namespace.
98+
a.monitorPowerEvents()
9599
return appRun(a.application)
96100
}
97101

@@ -292,7 +296,10 @@ func (a *linuxApp) isDarkMode() bool {
292296
return false
293297
}
294298

295-
innerVariant := result.Value().(dbus.Variant)
299+
innerVariant, ok := result.Value().(dbus.Variant)
300+
if !ok {
301+
return false
302+
}
296303
colorScheme, ok := innerVariant.Value().(uint32)
297304
if !ok {
298305
return false
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//go:build linux && cgo && !android && !server
2+
3+
package application
4+
5+
import (
6+
"github.com/godbus/dbus/v5"
7+
"github.com/wailsapp/wails/v3/pkg/events"
8+
)
9+
10+
func (a *linuxApp) monitorThemeChanges() {
11+
go func() {
12+
defer handlePanic()
13+
conn, err := dbus.ConnectSessionBus()
14+
if err != nil {
15+
a.parent.warning(
16+
"[WARNING] Failed to connect to session bus; monitoring for theme changes will not function: %v",
17+
err,
18+
)
19+
return
20+
}
21+
defer conn.Close()
22+
23+
if err = conn.AddMatchSignal(
24+
dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
25+
); err != nil {
26+
a.parent.warning(
27+
"[WARNING] Failed to subscribe to portal SettingChanged; theme changes will not fire: %v",
28+
err,
29+
)
30+
return
31+
}
32+
33+
c := make(chan *dbus.Signal, 10)
34+
conn.Signal(c)
35+
36+
getTheme := func(body []interface{}) (string, bool) {
37+
if len(body) < 3 {
38+
return "", false
39+
}
40+
if entry, ok := body[0].(string); !ok || entry != "org.gnome.desktop.interface" {
41+
return "", false
42+
}
43+
if entry, ok := body[1].(string); !ok || entry != "color-scheme" {
44+
return "", false
45+
}
46+
variant, ok := body[2].(dbus.Variant)
47+
if !ok {
48+
return "", false
49+
}
50+
value, ok := variant.Value().(string)
51+
if !ok {
52+
return "", false
53+
}
54+
return value, true
55+
}
56+
57+
for v := range c {
58+
theme, ok := getTheme(v.Body)
59+
if !ok {
60+
continue
61+
}
62+
63+
if theme != a.theme {
64+
a.theme = theme
65+
event := newApplicationEvent(events.Linux.SystemThemeChanged)
66+
event.Context().setIsDarkMode(a.isDarkMode())
67+
applicationEvents <- event
68+
}
69+
70+
}
71+
}()
72+
}
73+
74+
// monitorPowerEvents subscribes to systemd-logind's PrepareForSleep signal on
75+
// the system bus and translates it into Linux.SystemWillSleep (arg=true, just
76+
// before suspend) and Linux.SystemDidWake (arg=false, immediately on resume).
77+
// Mirrors NSWorkspace willSleep/didWake on macOS and WM_POWERBROADCAST on
78+
// Windows.
79+
//
80+
// On systems without systemd or logind/elogind reachable on the system bus
81+
// (Alpine, Void, some Devuan setups), we log a warning and exit cleanly so
82+
// the rest of the app keeps working.
83+
func (a *linuxApp) monitorPowerEvents() {
84+
go func() {
85+
defer handlePanic()
86+
conn, err := dbus.ConnectSystemBus()
87+
if err != nil {
88+
a.parent.warning(
89+
"[WARNING] Failed to connect to system bus; sleep/wake events will not fire: %v",
90+
err,
91+
)
92+
return
93+
}
94+
defer conn.Close()
95+
96+
// Probe for logind/elogind ownership of org.freedesktop.login1 on the
97+
// system bus. Without this check, AddMatchSignal would succeed on any
98+
// systemd-less distro and the goroutine would block forever on a
99+
// channel that never receives — silently masking the missing service.
100+
var hasOwner bool
101+
if err := conn.BusObject().Call(
102+
"org.freedesktop.DBus.NameHasOwner", 0, "org.freedesktop.login1",
103+
).Store(&hasOwner); err != nil {
104+
a.parent.warning(
105+
"[WARNING] Failed to probe org.freedesktop.login1; sleep/wake events will not fire: %v",
106+
err,
107+
)
108+
return
109+
}
110+
if !hasOwner {
111+
a.parent.warning(
112+
"[WARNING] systemd-logind/elogind not reachable on the system bus; sleep/wake events will not fire",
113+
)
114+
return
115+
}
116+
117+
// Constrain the sender to logind's well-known name so a hostile
118+
// connection on the system bus can't spoof PrepareForSleep signals.
119+
if err = conn.AddMatchSignal(
120+
dbus.WithMatchSender("org.freedesktop.login1"),
121+
dbus.WithMatchInterface("org.freedesktop.login1.Manager"),
122+
dbus.WithMatchMember("PrepareForSleep"),
123+
dbus.WithMatchObjectPath("/org/freedesktop/login1"),
124+
); err != nil {
125+
a.parent.warning(
126+
"[WARNING] Failed to subscribe to logind PrepareForSleep; sleep/wake events will not fire: %v",
127+
err,
128+
)
129+
return
130+
}
131+
132+
c := make(chan *dbus.Signal, 4)
133+
conn.Signal(c)
134+
135+
for v := range c {
136+
if v.Name != "org.freedesktop.login1.Manager.PrepareForSleep" {
137+
continue
138+
}
139+
if len(v.Body) < 1 {
140+
continue
141+
}
142+
willSleep, ok := v.Body[0].(bool)
143+
if !ok {
144+
continue
145+
}
146+
if willSleep {
147+
applicationEvents <- newApplicationEvent(events.Linux.SystemWillSleep)
148+
} else {
149+
applicationEvents <- newApplicationEvent(events.Linux.SystemDidWake)
150+
}
151+
}
152+
}()
153+
}

v3/pkg/application/application_linux_gtk3.go

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323

2424
"path/filepath"
2525

26-
"github.com/godbus/dbus/v5"
2726
"github.com/wailsapp/wails/v3/internal/operatingsystem"
2827
"github.com/wailsapp/wails/v3/pkg/events"
2928
)
@@ -227,147 +226,6 @@ func (a *linuxApp) getAccentColor() string {
227226
return "rgb(0,122,255)"
228227
}
229228

230-
func (a *linuxApp) monitorThemeChanges() {
231-
go func() {
232-
defer handlePanic()
233-
conn, err := dbus.ConnectSessionBus()
234-
if err != nil {
235-
a.parent.warning(
236-
"[WARNING] Failed to connect to session bus; monitoring for theme changes will not function: %v",
237-
err,
238-
)
239-
return
240-
}
241-
defer conn.Close()
242-
243-
if err = conn.AddMatchSignal(
244-
dbus.WithMatchObjectPath("/org/freedesktop/portal/desktop"),
245-
); err != nil {
246-
panic(err)
247-
}
248-
249-
c := make(chan *dbus.Signal, 10)
250-
conn.Signal(c)
251-
252-
getTheme := func(body []interface{}) (string, bool) {
253-
if len(body) < 3 {
254-
return "", false
255-
}
256-
if entry, ok := body[0].(string); !ok || entry != "org.gnome.desktop.interface" {
257-
return "", false
258-
}
259-
if entry, ok := body[1].(string); !ok || entry != "color-scheme" {
260-
return "", false
261-
}
262-
variant, ok := body[2].(dbus.Variant)
263-
if !ok {
264-
return "", false
265-
}
266-
value, ok := variant.Value().(string)
267-
if !ok {
268-
return "", false
269-
}
270-
return value, true
271-
}
272-
273-
for v := range c {
274-
theme, ok := getTheme(v.Body)
275-
if !ok {
276-
continue
277-
}
278-
279-
if theme != a.theme {
280-
a.theme = theme
281-
event := newApplicationEvent(events.Linux.SystemThemeChanged)
282-
event.Context().setIsDarkMode(a.isDarkMode())
283-
applicationEvents <- event
284-
}
285-
286-
}
287-
}()
288-
}
289-
290-
// monitorPowerEvents subscribes to systemd-logind's PrepareForSleep signal on
291-
// the system bus and translates it into Linux.SystemWillSleep (arg=true, just
292-
// before suspend) and Linux.SystemDidWake (arg=false, immediately on resume).
293-
// Mirrors NSWorkspace willSleep/didWake on macOS and WM_POWERBROADCAST on
294-
// Windows.
295-
//
296-
// On systems without systemd or logind/elogind reachable on the system bus
297-
// (Alpine, Void, some Devuan setups), we log a warning and exit cleanly so
298-
// the rest of the app keeps working.
299-
func (a *linuxApp) monitorPowerEvents() {
300-
go func() {
301-
defer handlePanic()
302-
conn, err := dbus.ConnectSystemBus()
303-
if err != nil {
304-
a.parent.warning(
305-
"[WARNING] Failed to connect to system bus; sleep/wake events will not fire: %v",
306-
err,
307-
)
308-
return
309-
}
310-
defer conn.Close()
311-
312-
// Probe for logind/elogind ownership of org.freedesktop.login1 on the
313-
// system bus. Without this check, AddMatchSignal would succeed on any
314-
// systemd-less distro and the goroutine would block forever on a
315-
// channel that never receives — silently masking the missing service.
316-
var hasOwner bool
317-
if err := conn.BusObject().Call(
318-
"org.freedesktop.DBus.NameHasOwner", 0, "org.freedesktop.login1",
319-
).Store(&hasOwner); err != nil {
320-
a.parent.warning(
321-
"[WARNING] Failed to probe org.freedesktop.login1; sleep/wake events will not fire: %v",
322-
err,
323-
)
324-
return
325-
}
326-
if !hasOwner {
327-
a.parent.warning(
328-
"[WARNING] systemd-logind/elogind not reachable on the system bus; sleep/wake events will not fire",
329-
)
330-
return
331-
}
332-
333-
// Constrain the sender to logind's well-known name so a hostile
334-
// connection on the system bus can't spoof PrepareForSleep signals.
335-
if err = conn.AddMatchSignal(
336-
dbus.WithMatchSender("org.freedesktop.login1"),
337-
dbus.WithMatchInterface("org.freedesktop.login1.Manager"),
338-
dbus.WithMatchMember("PrepareForSleep"),
339-
dbus.WithMatchObjectPath("/org/freedesktop/login1"),
340-
); err != nil {
341-
a.parent.warning(
342-
"[WARNING] Failed to subscribe to logind PrepareForSleep; sleep/wake events will not fire: %v",
343-
err,
344-
)
345-
return
346-
}
347-
348-
c := make(chan *dbus.Signal, 4)
349-
conn.Signal(c)
350-
351-
for v := range c {
352-
if v.Name != "org.freedesktop.login1.Manager.PrepareForSleep" {
353-
continue
354-
}
355-
if len(v.Body) < 1 {
356-
continue
357-
}
358-
willSleep, ok := v.Body[0].(bool)
359-
if !ok {
360-
continue
361-
}
362-
if willSleep {
363-
applicationEvents <- newApplicationEvent(events.Linux.SystemWillSleep)
364-
} else {
365-
applicationEvents <- newApplicationEvent(events.Linux.SystemDidWake)
366-
}
367-
}
368-
}()
369-
}
370-
371229
func newPlatformApp(parent *App) *linuxApp {
372230
name := sanitizeAppName(parent.options.Name)
373231
app := &linuxApp{

0 commit comments

Comments
 (0)