Skip to content

Fix duplicated and crashing DBus tray icon - #21980

Open
bolikcraft wants to merge 4 commits into
AvaloniaUI:mainfrom
bolikcraft:fix/dbus-tray-icon-duplicate-and-crash
Open

Fix duplicated and crashing DBus tray icon#21980
bolikcraft wants to merge 4 commits into
AvaloniaUI:mainfrom
bolikcraft:fix/dbus-tray-icon-duplicate-and-crash

Conversation

@bolikcraft

@bolikcraft bolikcraft commented Aug 14, 2026

Copy link
Copy Markdown

What does the pull request do?

Fixes two independent defects in DBusTrayIconImpl.

Duplicate icon (#21978). The /StatusNotifierItem object was exported before any well-known name was requested, and the name was released and re-taken with a new instance id whenever the watcher reappeared. A host that scans the bus for items (gnome-shell-extension-appindicator does this on every start) finds the object on a connection owning no org.kde.StatusNotifierItem-* name and keys the item by the unique connection name, while the RegisterStatusNotifierItem call that follows is keyed by the well-known name — two entries, two icons. The object is now exported only while the name is owned, and the name keeps its instance id for the lifetime of the icon. It is deliberately not released when the watcher goes away, so a restarting host always sees object and name together; hiding still releases it, which is the only part of hiding a host can observe.

Crash on disposal (#21979). WatchAsync is async void and awaits a cancellable delay. Dispose cancels the token and only then sets _isDisposed, but the continuation is posted to the dispatcher and runs after Dispose returned, so neither existing exception filter matches and the OperationCanceledException is rethrown on the dispatcher, aborting the process (exit code 134). It is now caught explicitly. CreateTrayIcon is async void too and had no handler at all, so a DBus call failing mid-registration ended the same way; its body is guarded now.

No public API changes.

What is the updated/expected behavior with this PR?

One tray icon per application, surviving any number of host restarts; disposal and application shutdown complete silently. SetIsVisible(false) still removes the icon, and SetIsVisible(true) brings back the same item instead of a new one.

Testing

Manually verified on GNOME Shell 48.5 with appindicatorsupport@rgcjonas.gmail.com, against both an unpatched (12.1.0) and a patched build of the same test application, polling org.kde.StatusNotifierWatcher.RegisteredStatusNotifierItems: disabling and re-enabling the extension repeatedly, replacing the icon through TrayIcon.SetIcons, shutting the application down, and hide/show cycles (including one racing ReleaseName against RequestName). Unpatched runs show the duplicate entry and the 134 exit; patched runs keep exactly one entry under a stable name and exit 0. Menu, tooltip, icon and Activate still work after a hide/show cycle. Not covered by unit tests: the behaviour only shows against a live session bus and a StatusNotifier host.

Checklist

Breaking changes

None.

Fixed issues

Fixes #21978
Fixes #21979

Related to #13130 (same registration path, but that report is about the item being lost when the host restarts without the watcher name going away — not addressed here).

@MrJul MrJul added bug os-linux backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch labels Aug 14, 2026
@MrJul

MrJul commented Aug 14, 2026

Copy link
Copy Markdown
Member

Thank you for your contribution. Please follow our AI Guidelines and trim down the PR description and code comments.

@cla-avalonia

cla-avalonia commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator
  • All contributors have signed the CLA.

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068316-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@bolikcraft

Copy link
Copy Markdown
Author

@cla-avalonia agree

The StatusNotifierItem object was exported before the well-known name
was requested, and the name was released and re-taken with a new
instance id whenever the watcher came back. A host that scans the bus
then finds the object on a connection owning no such name and registers
it under the unique connection name, so RegisterStatusNotifierItem adds
a second entry for the same process. The object is now exported only
while the name is owned, and the name is kept for the lifetime of the
icon.

WatchAsync and CreateTrayIcon are async void: the OperationCanceledException
from disposal escaped onto the dispatcher and terminated the process,
as would any failing DBus call during registration. Both are handled now.
@bolikcraft
bolikcraft force-pushed the fix/dbus-tray-icon-duplicate-and-crash branch from bb72bf5 to aec2e66 Compare August 14, 2026 09:58
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068318-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

if (!_sysTrayServiceNameOwned)
{
// Set before awaiting, so that a concurrent hide queues its ReleaseName instead of skipping it.
_sysTrayServiceNameOwned = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_sysTrayServiceNameOwned is set before ownership is acquired. if invoke a failing RequestNameAsync: the exception is logged by the outer catch, but this flag remains true. Every subsequent CreateTrayIcon() therefore skips name acquisition and attempts to register using a name the connection does not own.
Could the in-flight request be tracked separately, with _sysTrayServiceNameOwned set only after successful acquisition?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in fdf755a. The flag is gone; the request task itself is now the state, which distinguishes in-flight, acquired and failed:

  • _sysTrayServiceNameRequest ??= _connection.RequestNameAsync(...) then await it — the happy path is unchanged.
  • the catch clears the field unless the task ran to completion, so a refused name is asked for again on the next attempt, while a failure in RegisterStatusNotifierItem afterwards leaves ownership alone.
  • ReleaseTrayServiceName takes the task, clears the field and awaits it before calling ReleaseName — that is what the early flag was there for (a hide racing the request), and it no longer releases a name that was never acquired.

@jsuarezruiz jsuarezruiz self-assigned this Aug 24, 2026
The flag was set before awaiting RequestName, so a refused acquisition left it
true: every later CreateTrayIcon skipped the request and registered the item
under a name the connection does not own. Keeping the request task tells the
three states apart - in flight, acquired, failed - so a failure is retried,
and a hide racing the request now waits for it before releasing the name
instead of releasing one that is not owned yet.
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068818-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Its entry conditions were only checked before the first await, so a hide or a
dispose while the bus answered RequestName still ran into the export: the
release queued on that same task runs after this continuation, leaving the
object exported under a name no longer owned - the state the duplicate item
grows from. The conditions are re-read after each await, and the request is
compared by reference so a stale continuation gives way to a newer one.

Releasing the name is now awaited before requesting it again: the bus keeps a
name registered until the release is acknowledged, and since the name is reused
for the lifetime of the icon, a show right after a hide would otherwise fail
outright and leave no icon until the next toggle.
@bolikcraft

Copy link
Copy Markdown
Author

Pushed db0484e, closing two more races I found while re-reading the registration path:

  • CreateTrayIcon only checked its entry conditions before the first await. A hide or a dispose while the bus answered RequestName queues its release on that same task, so the release runs after this continuation and the object ends up exported under a name that is no longer owned — the state the duplicate item grows from. The conditions are re-read after every await, and the request is compared by reference so a stale continuation gives way to a newer one.
  • Because the name is now reused for the lifetime of the icon, a show right after a hide could ask for it while the release was still in flight. The bus keeps the name registered until the release is acknowledged, so that request fails and no icon appears until the next toggle. The release is kept as a task and awaited before requesting the name again.

Also narrowed the catch that drops a refused request: it now only clears the request this call made, and only if it never completed.

@jmacato

jmacato commented Aug 26, 2026

Copy link
Copy Markdown
Member

@bolikcraft please remind your ai agent to reduce the unnecessary comments. direct it to write docs in ASD-STE100 Simplified Technical English.

The comments explained the same rule more than one time and used long
sentences. Each comment is now one or two short sentences in ASD-STE100
style. The code does not change.
@bolikcraft

bolikcraft commented Aug 26, 2026

Copy link
Copy Markdown
Author

Ok, fixed in 5cd6efe: fewer comments, ASD-STE100 style.

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068842-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@bolikcraft

Copy link
Copy Markdown
Author

@jsuarezruiz the flag issue is fixed in fdf755a — the name request task is now the state. Two more races are closed in db0484e, and the comments are shortened in 5cd6efe. All checks pass. Please look at it again when you have time.

@MrJul MrJul mentioned this pull request Sep 4, 2026
1 task

// CreateTrayIcon exports this object when the connection owns the name.
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath);
_connection.AddMethodHandler(_statusNotifierItemDbusObj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed this is the root cause of the duplicate. On main, the object can be inspected through the connection immediately after construction, even before the name is owned. On this branch, it isn’t exposed until the name is acquired. Good catch.

_statusNotifierItemDbusObj.SetIcon(_icon);
// Export the object only while the connection owns the name. If not, a host that scans
// the bus adds a second item.
_connection.AddMethodHandler(_statusNotifierItemDbusObj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If SetIsVisible toggles quickly (for exampke, false/true/false/true in one turn), two CreateTrayIcon calls end up running at once, both wait on the same release/request, and both reach AddMethodHandler. The second one throws "A method handler is already registered for '/StatusNotifierItem'" and logs "Unable to register the system tray icon."

It is just an error in the log, but it points to CreateTrayIcon not being safe to re enter. On main this never happened because it removed the handler before re adding it.

We could improve with a small guard so re entry skips the add instead of throwing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch bug os-linux

Projects

None yet

6 participants