From b6deed39fbd3065a55f50f2d9b3c4495cd694712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Tue, 28 Jul 2026 16:43:48 +0200 Subject: [PATCH] [client] Feed tray status via direct callback instead of the event bus --- client/ui/services/daemon_feed.go | 32 ++++++++++++++++++++++++++++--- client/ui/tray.go | 2 +- client/ui/tray_status.go | 10 ---------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/client/ui/services/daemon_feed.go b/client/ui/services/daemon_feed.go index 632581fe9..52117270c 100644 --- a/client/ui/services/daemon_feed.go +++ b/client/ui/services/daemon_feed.go @@ -5,6 +5,7 @@ package services import ( "context" "fmt" + "slices" "strings" "sync" "time" @@ -167,6 +168,12 @@ type DaemonFeed struct { cancel context.CancelFunc streamWg sync.WaitGroup + // statusSubs are Go-side snapshot consumers (the tray), fed directly so + // they don't ride the window event bus. Callbacks run synchronously on + // the stream goroutine, so pushes arrive in order. + statusSubsMu sync.Mutex + statusSubs []func(Status) + switchMu sync.Mutex switchInProgress bool switchInProgressUntil time.Time @@ -188,6 +195,25 @@ func NewDaemonFeed(conn DaemonConn, emitter Emitter, updaterHolder *updater.Hold return &DaemonFeed{conn: conn, emitter: emitter, updater: updaterHolder, logCtl: logCtl} } +// OnStatus registers a Go-side status subscriber. Not for the frontend — +// React consumers subscribe to EventStatusSnapshot on the event bus. +func (s *DaemonFeed) OnStatus(cb func(Status)) { + s.statusSubsMu.Lock() + s.statusSubs = append(s.statusSubs, cb) + s.statusSubsMu.Unlock() +} + +// pushStatus delivers a snapshot to the Go-side subscribers and the frontend. +func (s *DaemonFeed) pushStatus(st Status) { + s.statusSubsMu.Lock() + subs := slices.Clone(s.statusSubs) + s.statusSubsMu.Unlock() + for _, cb := range subs { + cb(st) + } + s.emitter.Emit(EventStatusSnapshot, st) +} + // BeginProfileSwitch arms suppression for a switch from Connected/Connecting, // where the daemon emits stale Connected updates during Down's teardown then an // Idle before the new Up; statusStreamLoop drops those, and a synthetic @@ -201,7 +227,7 @@ func (s *DaemonFeed) BeginProfileSwitch() { s.switchLoginWatch = true s.switchLoginWatchUntil = now.Add(30 * time.Second) s.switchMu.Unlock() - s.emitter.Emit(EventStatusSnapshot, Status{Status: StatusConnecting}) + s.pushStatus(Status{Status: StatusConnecting}) } // CancelProfileSwitch aborts a switch midway (tray Disconnect while Connecting): @@ -343,7 +369,7 @@ func (s *DaemonFeed) statusStreamLoop(ctx context.Context) { return } unavailable = true - s.emitter.Emit(EventStatusSnapshot, Status{Status: StatusDaemonUnavailable}) + s.pushStatus(Status{Status: StatusDaemonUnavailable}) } op := func() error { @@ -403,7 +429,7 @@ func (s *DaemonFeed) emitStatus(st Status) { log.Debugf("suppressing status=%q during profile switch", st.Status) return } - s.emitter.Emit(EventStatusSnapshot, st) + s.pushStatus(st) if triggerLogin { s.emitter.Emit(EventTriggerLogin) } diff --git a/client/ui/tray.go b/client/ui/tray.go index 3050d159a..55970ce0f 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -196,7 +196,7 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe // menu (e.g. GNOME Shell AppIndicator). bindTrayClick(t) - app.Event.On(services.EventStatusSnapshot, t.onStatusEvent) + svc.DaemonFeed.OnStatus(t.applyStatus) app.Event.On(services.EventDaemonNotification, t.onSystemEvent) // Refresh the Profiles submenu on ProfileSwitcher's change event. A // switch on an idle daemon drives no status transition, so without this diff --git a/client/ui/tray_status.go b/client/ui/tray_status.go index 793f1785a..1de595a12 100644 --- a/client/ui/tray_status.go +++ b/client/ui/tray_status.go @@ -5,19 +5,9 @@ package main import ( "strings" - "github.com/wailsapp/wails/v3/pkg/application" - "github.com/netbirdio/netbird/client/ui/services" ) -func (t *Tray) onStatusEvent(ev *application.CustomEvent) { - st, ok := ev.Data.(services.Status) - if !ok { - return - } - t.applyStatus(st) -} - // applyStatus repaints the tray from a daemon snapshot. Icon refresh is skipped // when no icon-relevant input changed: the daemon emits rapid SubscribeStatus // bursts during health probes that would otherwise spam Shell_NotifyIcon.