mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-24 01:11:29 +02:00
[client] Fix tray notification crash on Linux
The Wails notifications service connects to the D-Bus session bus in its ServiceStartup, which Wails runs synchronously inside app.Run. daemonFeed.Watch was started before app.Run, so the first daemon SubscribeEvents message (which replays the cached available-update state) fanned out to the tray's update-state listener and fired an OS notification before that startup ran. The notifier's *dbus.Conn was still nil, so SendNotification nil-dereferenced deep in godbus and the panic was fatal to the whole process (observed on Linux Mint). Move daemonFeed.Watch into the ApplicationStarted hook so it runs after the service-startup loop, and route every notification send through a new safeSendNotification helper that recovers from a panic and logs it, so a broken or unavailable notification bus degrades to a skipped toast instead of crashing.
This commit is contained in:
@@ -141,6 +141,8 @@ User-actionable operation failures (config save, profile switch, debug bundle, u
|
||||
|
||||
The tray uses Wails' built-in `notifications` service. One `notifications.NotificationService` is created in `main.go` and passed into `TrayServices.Notifier`. Notification IDs are prefixed for coalescing: `netbird-update-<version>`, `netbird-event-<id>`, `netbird-tray-error`, `netbird-session-expired`. Notifications are gated by the user's "Notifications" toggle (cached in `Tray.notificationsEnabled`, seeded from `Settings.GetConfig` at boot). `Severity == "critical"` events bypass the gate, mirroring the legacy Fyne `event.Manager`.
|
||||
|
||||
**All sends go through `safeSendNotification` (`tray_notify.go`)** — never call `Notifier.SendNotification`/`SendNotificationWithActions` directly. It swallows both errors *and panics*. The panic guard is load-bearing on Linux: Wails' notifier connects to the session bus in its `ServiceStartup`, and when that connect fails (headless box, no/unreachable `DBUS_SESSION_BUS_ADDRESS`, UI launched outside a desktop session) Wails logs the error but leaves the service registered with a **nil `*dbus.Conn`**. The next send then nil-derefs deep inside godbus (`Conn.getSerial`), and because sends run on a Wails event-dispatch goroutine the panic is fatal to the whole process — observed as a "catastrophic failure" crash on a Linux Mint VM when an update toast fired (`tray_update.go sendUpdateNotification`). `recover()` turns it into a logged no-op. The `tray_session.go` plain-notification fallback keys off the returned error, so a recovered panic (returns nil) correctly skips the fallback — the bus is dead, the plain send would panic too.
|
||||
|
||||
### Profile switching invariants
|
||||
|
||||
`ProfileSwitcher.SwitchActive` is the only switch path on the TS side — `ProfileContext.switchProfile` is the single TS wrapper, and `modules/profiles/ProfilesTab.tsx` + the header `ProfileDropdown` both go through it. The Go side captures `prevStatus`, drives the optimistic-Connecting paint via `Peers.BeginProfileSwitch`, mirrors into the user-side `profilemanager`, and conditionally fires Down/Up per the reconnect-policy table above.
|
||||
|
||||
@@ -190,7 +190,20 @@ func main() {
|
||||
})
|
||||
listenForShowSignal(context.Background(), tray)
|
||||
|
||||
daemonFeed.Watch(context.Background())
|
||||
// Start the daemon event feed only after Wails has run every service's
|
||||
// ServiceStartup. The very first daemon SubscribeEvents message replays
|
||||
// the cached state (status + available update) synchronously, which fans
|
||||
// out through app.Event into the tray's update-state listener and fires an
|
||||
// OS notification. If Watch ran before app.Run, that send could beat the
|
||||
// notifications service's ServiceStartup — on Linux the Wails notifier
|
||||
// connects to the session bus there, so its *dbus.Conn would still be nil
|
||||
// and SendNotification would nil-deref (fatal panic on the event-dispatch
|
||||
// goroutine; observed on Linux Mint). ApplicationStarted fires inside
|
||||
// app.Run after the synchronous service-startup loop, so the bus is up by
|
||||
// the time the first event lands.
|
||||
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
|
||||
daemonFeed.Watch(context.Background())
|
||||
})
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -637,13 +637,11 @@ func (t *Tray) notify(title, body, id string) {
|
||||
if t.svc.Notifier == nil {
|
||||
return
|
||||
}
|
||||
if err := t.svc.Notifier.SendNotification(notifications.NotificationOptions{
|
||||
_ = safeSendNotification(t.svc.Notifier.SendNotification, title, notifications.NotificationOptions{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Body: body,
|
||||
}); err != nil {
|
||||
log.Debugf("notify %q: %v", title, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// notifyError fires a generic "Error" notification for tray-driven action
|
||||
|
||||
39
client/ui/tray_notify.go
Normal file
39
client/ui/tray_notify.go
Normal file
@@ -0,0 +1,39 @@
|
||||
//go:build !android && !ios && !freebsd && !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
)
|
||||
|
||||
// sendFn is either NotificationService.SendNotification or
|
||||
// SendNotificationWithActions — both share the same signature.
|
||||
type sendFn func(notifications.NotificationOptions) error
|
||||
|
||||
// safeSendNotification dispatches an OS notification, swallowing both errors
|
||||
// and panics. OS toasts are best-effort: a missing or broken session bus must
|
||||
// never crash the app.
|
||||
//
|
||||
// The panic guard is load-bearing on Linux. Wails' notifier connects to the
|
||||
// session bus in its ServiceStartup; when that connect fails (headless box,
|
||||
// no/unreachable DBUS_SESSION_BUS_ADDRESS, a UI launched outside a desktop
|
||||
// session), Wails logs the error but leaves the service registered with a nil
|
||||
// *dbus.Conn. The next SendNotification then nil-derefs deep inside
|
||||
// godbus (Conn.getSerial) and, because the send runs on a Wails event-dispatch
|
||||
// goroutine, the panic is fatal to the whole process rather than to one event
|
||||
// listener. recover() turns that into a logged no-op. See
|
||||
// notifications_linux.go in wails v3.
|
||||
func safeSendNotification(send sendFn, what string, opts notifications.NotificationOptions) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("notify %s: recovered from panic (notification bus unavailable): %v", what, r)
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
if err := send(opts); err != nil {
|
||||
log.Errorf("notify %s: %v", what, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -224,16 +224,17 @@ func (t *Tray) notifySessionWarning(title, body string) {
|
||||
if t.svc.Notifier == nil {
|
||||
return
|
||||
}
|
||||
err := t.svc.Notifier.SendNotificationWithActions(notifications.NotificationOptions{
|
||||
err := safeSendNotification(t.svc.Notifier.SendNotificationWithActions, "session-warning with actions", notifications.NotificationOptions{
|
||||
ID: notifyIDSessionWarning,
|
||||
Title: title,
|
||||
Body: body,
|
||||
CategoryID: notifyCategorySessionWarning,
|
||||
})
|
||||
if err != nil {
|
||||
log.Debugf("notify session-warning with actions: %v", err)
|
||||
// Fall back to a plain notification so the user at least gets
|
||||
// the warning text, even without buttons.
|
||||
// the warning text, even without buttons. (A nil err here also
|
||||
// covers the panic-recovered case, where the bus is dead and the
|
||||
// plain fallback would fail too — so we correctly skip it.)
|
||||
t.notify(title, body, notifyIDSessionWarning)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,13 +202,11 @@ func (u *trayUpdater) sendUpdateNotification(st updater.State) {
|
||||
if st.Enforced {
|
||||
body += u.loc.T("notify.update.enforcedSuffix")
|
||||
}
|
||||
if err := u.notifier.SendNotification(notifications.NotificationOptions{
|
||||
_ = safeSendNotification(u.notifier.SendNotification, "update", notifications.NotificationOptions{
|
||||
ID: notifyIDUpdatePrefix + st.Version,
|
||||
Title: u.loc.T("notify.update.title"),
|
||||
Body: body,
|
||||
}); err != nil {
|
||||
log.Debugf("send update notification: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// openProgressWindow points the main window at the /update progress page
|
||||
|
||||
Reference in New Issue
Block a user