From 072d78946324ad020e4a2558f1a36b551e9a1b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Mon, 1 Jun 2026 21:54:37 +0200 Subject: [PATCH] client/ui: retry XEmbed-tray probe before claiming SNI watcher The XEmbed tray (panel) can come up after the autostarted UI on minimal WMs, so the single startup probe added in #6320 could miss a tray that appears a second or two later, leaving the icon silently absent. Re-probe for a ~10s grace period in a goroutine, claiming the watcher as soon as a tray shows up; back off cleanly if none ever appears (headless/Wayland). --- client/ui/tray_watcher_linux.go | 41 +++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/client/ui/tray_watcher_linux.go b/client/ui/tray_watcher_linux.go index 63213985e..4b2c06b29 100644 --- a/client/ui/tray_watcher_linux.go +++ b/client/ui/tray_watcher_linux.go @@ -18,6 +18,7 @@ package main import ( "sync" + "time" "github.com/godbus/dbus/v5" log "github.com/sirupsen/logrus" @@ -27,6 +28,16 @@ const ( watcherName = "org.kde.StatusNotifierWatcher" watcherPath = "/StatusNotifierWatcher" watcherIface = "org.kde.StatusNotifierWatcher" + + // watcherProbeInterval / watcherProbeTimeout bound how long we keep + // re-probing for an XEmbed tray before giving up. The UI is commonly + // autostarted *before* the panel/tray on minimal WMs, so a single probe + // at startup would miss a tray that comes up a second or two later and + // the icon would silently never appear. ~10s of polling covers a slow + // panel launch while staying short enough that a headless / pure-Wayland + // session (no XEmbed tray ever) winds down quickly. + watcherProbeInterval = 500 * time.Millisecond + watcherProbeTimeout = 10 * time.Second ) type statusNotifierWatcher struct { @@ -113,12 +124,34 @@ func (w *statusNotifierWatcher) tryStartXembedHost(busName string, objPath dbus. // If not (pure Wayland, or any environment already running a real watcher), // we have nothing to bridge and must stay off the bus entirely so the real // watcher owns the name. Safe to call unconditionally. +// +// The XEmbed tray may come up *after* the UI (the panel and the autostarted +// app race at login), so we re-probe for a short grace period instead of +// deciding once at startup. The probing runs in a goroutine so it never +// blocks the caller's startup path. func startStatusNotifierWatcher() { - if !xembedTrayAvailable() { - log.Debugf("StatusNotifierWatcher: no XEmbed tray present, leaving the watcher to the desktop") - return - } + go func() { + deadline := time.Now().Add(watcherProbeTimeout) + for { + if xembedTrayAvailable() { + claimStatusNotifierWatcher() + return + } + if time.Now().After(deadline) { + log.Debugf("StatusNotifierWatcher: no XEmbed tray appeared within %s, leaving the watcher to the desktop", watcherProbeTimeout) + return + } + time.Sleep(watcherProbeInterval) + } + }() +} +// claimStatusNotifierWatcher opens a private session-bus connection and takes +// ownership of org.kde.StatusNotifierWatcher, exporting the in-process stub +// watcher. The caller has already confirmed an XEmbed tray is present, so we +// genuinely have an item to bridge. The GetNameOwner / DoNotQueue guards still +// back off if a real watcher already holds the name. +func claimStatusNotifierWatcher() { conn, err := dbus.SessionBusPrivate() if err != nil { log.Debugf("StatusNotifierWatcher: cannot open private session bus: %v", err)