Skip to content

libayatana-appindicator activates XEmbed fallback when xapp-sn-watcher hasn't claimed its bus name at boot — duplicate icon in systray + xapp-status applets #199

@martin152

Description

@martin152

TITLE:
libayatana-appindicator activates XEmbed fallback when xapp-sn-watcher hasn't claimed its bus name at boot — duplicate icon in systray + xapp-status applets

────────────────────────────────────────────────────────────────────────────────

BODY:

Summary

AppIndicator apps show two tray icons at boot — one in xapp-status@cinnamon.org
(small, tooltip works) and one in systray@cinnamon.org (larger, no tooltip).
Single process, registered once on the StatusNotifierWatcher bus, but visible
in two applets simultaneously.

Observed on two independent apps: Diodon 1.13.0 and a local
AyatanaAppIndicator3 Python app. Neither app registers more than once.

This is the same symptom reported and never code-fixed in
cinnamon#8426, cjs#74, and xapp#157. The 2022 hypothesis from @mtwebster in
#157"They're probably not showing up in the xapp applet twice, but once
there and once in the old system tray applet"
— is correct. I have controlled
reproduction and a confirmed fix.

Environment

  • OS: Linux Mint 22.3 Cinnamon 6.6.7
  • Kernel: 6.17.0-22-generic
  • xapps-common: 3.2.2+zena
  • cinnamon-common: 6.6.7+zena
  • libayatana-appindicator3-1: 0.5.93-1build3

Visual identification — two applets, not one

The duplicate icons render differently, which identifies which applet each
belongs to:

Property Small icon Large icon
Size Panel-native (xapp-status sizing) Larger (systray sizing)
Hover tooltip ✓ appears (AppIndicator title prop) ✗ absent (XEmbed has no equivalent)
Menu on click App's own menu App's own menu (same underlying object)
Applet xapp-status@cinnamon.org systray@cinnamon.org

Confirmed by checking gdbus — the indicator appears once on the
StatusNotifierWatcher bus:

$ gdbus call --session --dest org.kde.StatusNotifierWatcher \
    --object-path /StatusNotifierWatcher \
    --method org.freedesktop.DBus.Properties.Get \
    org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems
(<[':1.72/org/ayatana/NotificationItem/Diodon'
   ':1.76/org/ayatana/NotificationItem/battery_limit'
   ...]>,)

The duplication is downstream of the watcher — between the watcher and the
panel applets.

Root cause: boot-time race between xapp-sn-watcher and AppIndicator clients

Both apps and xapp-sn-watcher autostart in the same second at session login:

14:52:48 — xapp-sn-watcher    PID 1997
14:52:48 — battery-limit-tray PID 2025
14:52:48 — diodon             PID 2038

libayatana-appindicator checks at set_status(ACTIVE) whether
org.kde.StatusNotifierWatcher is owned on the session bus. If the watcher
process is alive but has not yet claimed that name (a race window of a few
hundred milliseconds at cold boot), the library activates a legacy
Gtk.StatusIcon XEmbed fallback. When the watcher comes up moments later,
the library also completes the StatusNotifier registration. Both paths remain
active: one icon in xapp-status, one in systray.

Controlled reproduction confirming the mechanism

I patched my local AppIndicator app to wait until org.kde.StatusNotifierWatcher
is actually owned before calling set_status(ACTIVE):

def wait_for_status_notifier_watcher(timeout_s=30):
    bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        try:
            res = bus.call_sync(
                "org.freedesktop.DBus", "/org/freedesktop/DBus",
                "org.freedesktop.DBus", "NameHasOwner",
                GLib.Variant("(s)", ("org.kde.StatusNotifierWatcher",)),
                GLib.VariantType("(b)"), Gio.DBusCallFlags.NONE, 1000, None,
            )
            if res.unpack()[0]:
                return True
        except GLib.Error:
            pass
        time.sleep(0.2)
    return False

# called immediately before set_status(ACTIVE)
wait_for_status_notifier_watcher()
self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE)

I then ran 5 cold reboots, alternating between the patched and unpatched
versions:

With watcher-wait patch (tested separately before revert):
battery_limit: 1 icon across multiple boots — no duplicate.

After reverting the patch (5 consecutive cold reboots):

Reboot Result
1 battery_limit: 1 icon (race did not trigger)
2 battery_limit: 2 icons (large systray + small xapp-status)
3 Diodon: 2 icons (large systray + small xapp-status); battery_limit: 1
4 Diodon: 2 icons
5 1 icon each (race did not trigger)

Two things are clear from this data:

  1. The watcher-wait fix directly prevents the bug — with it in place, no
    duplicate appeared across any boot. After reverting, the duplicate appeared
    on 3 of 5 reboots across two different apps.
  2. The bug is non-deterministic — whether the race triggers depends on
    scheduler timing at each boot. Reboots 1 and 5 show no duplicate even
    without the fix. This explains why all three previous issues were closed
    without a code fix: if a maintainer tested on a fast machine where the
    watcher claims its name in under 100ms, they would never see the bug.

Diodon exhibits the identical symptom but I cannot patch it; it demonstrates
that the bug affects any AppIndicator app that starts in the same boot-time
window as xapp-sn-watcher.

Why the fix belongs in xapp-sn-watcher, not per-app

The app-side fix works but is the wrong place for it. Users cannot patch
apps they don't maintain (Diodon, Transmission, etc.). The clean fix is to
ensure xapp-sn-watcher claims org.kde.StatusNotifierWatcher on the session
bus before any user-session AppIndicator client could race it.

The simplest approach: add an autostart phase hint to
xapp-sn-watcher.desktop so it starts in the initialization phase rather
than the applications phase. Alternatively, libayatana-appindicator could
be patched to de-activate its XEmbed fallback if the watcher claims its name
shortly after activation — but that crosses package boundaries.

This was all compiled with Claude Code.
Happy to test patches or provide D-Bus traces.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions