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).
This commit is contained in:
Zoltán Papp
2026-06-01 21:54:37 +02:00
parent 8d05fe07bf
commit 072d789463

View File

@@ -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)