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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions