Skip to content

Commit bb72bf5

Browse files
committed
Fix duplicated and crashing DBus tray icon
Two problems in DBusTrayIconImpl, both visible on GNOME with the appindicator/KStatusNotifierItem extension. Duplicated icon: the StatusNotifierItem object was exported on the connection before the well-known name was requested, and the name was released and re-requested (with a new instance id) every time the StatusNotifierWatcher came back. A host that scans the bus for items - gnome-shell-extension-appindicator does this whenever it starts - finds the object on a connection which does not own an org.kde.StatusNotifierItem-* name, so it registers the item under the unique connection name, and the RegisterStatusNotifierItem call that follows adds a second entry for the same process. The object is now exported only while the name is owned, the name keeps its instance id for the lifetime of the tray icon, and it is no longer dropped and retaken when the watcher restarts. Hiding still releases the name: that is the only part of it a host can observe, since the item stays Active and unexporting the object is silent on the bus. It now happens after the object has been unexported, so the "object exported while the name is not owned" window which produces the duplicate never opens. Crash on disposal: WatchAsync is an async void method awaiting a cancellable delay. Dispose cancels the token and only then sets _isDisposed, but the continuation is posted to the dispatcher and runs after Dispose has returned, so neither exception filter matches and the OperationCanceledException is rethrown on the dispatcher, terminating the process. The cancellation is now handled explicitly. CreateTrayIcon is async void as well and takes the process down the same way when a DBus call fails (for example when the watcher disappears while the item is being registered), so its body is guarded too.
1 parent 845b81d commit bb72bf5

1 file changed

Lines changed: 59 additions & 15 deletions

File tree

src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal class DBusTrayIconImpl : ITrayIconImpl
2323
private (int, int, byte[]) _icon;
2424

2525
private string? _sysTrayServiceName;
26+
private bool _sysTrayServiceNameOwned;
2627
private string? _tooltipText;
2728
private bool _isDisposed;
2829
private bool _serviceConnected;
@@ -52,8 +53,8 @@ public DBusTrayIconImpl()
5253

5354
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusMenuPath, _connection);
5455

56+
// The object is only exported once the well-known name is owned, see CreateTrayIcon.
5557
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath);
56-
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
5758
_statusNotifierItemDbusObj.ActivationDelegate += () => OnClicked?.Invoke();
5859

5960
WatchAsync();
@@ -93,6 +94,11 @@ private async void WatchAsync()
9394
OnOwnerChanged(owner);
9495
}
9596
}
97+
catch (OperationCanceledException)
98+
{
99+
// The watch was cancelled by Dispose. Note that this can't be filtered on _isDisposed:
100+
// the continuation is posted to the dispatcher and runs after Dispose has completed.
101+
}
96102
catch (Exception e) when (!_isDisposed)
97103
{
98104
Logger.TryGet(LogEventLevel.Error, "DBUS")
@@ -110,8 +116,6 @@ private void OnOwnerChanged(string? newOwner)
110116
_serviceConnected = true;
111117
_statusNotifierWatcher = new StatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
112118

113-
DestroyTrayIcon();
114-
115119
if (_isVisible)
116120
CreateTrayIcon();
117121
}
@@ -124,40 +128,79 @@ private void OnOwnerChanged(string? newOwner)
124128

125129
private async void CreateTrayIcon()
126130
{
127-
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierWatcher is null)
131+
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _statusNotifierWatcher is null)
128132
return;
129133

134+
try
135+
{
136+
// The name is generated once and reused: taking a new instance id on every registration
137+
// makes the host see a brand new item instead of the one it already knows about.
138+
if (_sysTrayServiceName is null)
139+
{
130140
#if NET5_0_OR_GREATER
131-
var pid = Environment.ProcessId;
141+
var pid = Environment.ProcessId;
132142
#else
133-
var pid = Process.GetCurrentProcess().Id;
143+
var pid = Process.GetCurrentProcess().Id;
134144
#endif
135-
var tid = s_trayIconInstanceId++;
145+
var tid = s_trayIconInstanceId++;
146+
_sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}");
147+
}
136148

137-
_connection.RemoveMethodHandler(_statusNotifierItemDbusObj!.Path);
138-
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
149+
if (!_sysTrayServiceNameOwned)
150+
{
151+
// The flag is set before the call so that a hide racing with this one queues its
152+
// ReleaseName after the request below instead of skipping it altogether.
153+
_sysTrayServiceNameOwned = true;
154+
await _connection.RequestNameAsync(_sysTrayServiceName);
155+
}
139156

140-
_sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}");
141-
await _connection.RequestNameAsync(_sysTrayServiceName);
142-
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
157+
// Only export the object once the name is owned: a host which scans the bus for items would
158+
// otherwise find it on the unique connection name and register a second, duplicate item.
159+
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
143160

144-
_statusNotifierItemDbusObj!.SetTitleAndTooltip(_tooltipText);
145-
_statusNotifierItemDbusObj.SetIcon(_icon);
161+
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
162+
163+
_statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
164+
_statusNotifierItemDbusObj.SetIcon(_icon);
165+
}
166+
catch (Exception e)
167+
{
168+
if (!_isDisposed)
169+
Logger.TryGet(LogEventLevel.Error, "DBUS")
170+
?.Log(this, "Unable to register the system tray icon.\n{Exception}", e);
171+
}
146172
}
147173

148174
private void DestroyTrayIcon()
149175
{
150176
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null)
151177
return;
152178

153-
_connection!.ReleaseNameAsync(_sysTrayServiceName);
154179
_connection.RemoveMethodHandler(_statusNotifierItemDbusObj.Path);
155180
}
156181

182+
/// <summary>
183+
/// Gives up the well-known name, which is what makes a host drop the item: unexporting the
184+
/// object is not observable on the bus at all. Must be called after <see cref="DestroyTrayIcon"/>
185+
/// so that the object is never exported while the name is unowned - that is the state in which a
186+
/// host scanning the bus registers the item under the unique connection name and ends up with a
187+
/// duplicate. The name is not released when the watcher goes away: keeping it means a restarting
188+
/// host always sees the object and the name together.
189+
/// </summary>
190+
private void ReleaseTrayServiceName()
191+
{
192+
if (_connection is null || _sysTrayServiceName is null || !_sysTrayServiceNameOwned)
193+
return;
194+
195+
_sysTrayServiceNameOwned = false;
196+
_connection.ReleaseNameAsync(_sysTrayServiceName);
197+
}
198+
157199
public void Dispose()
158200
{
159201
IsActive = false;
160202
DestroyTrayIcon();
203+
ReleaseTrayServiceName();
161204
(MenuExporter as IDisposable)?.Dispose();
162205
_watchCts?.Cancel();
163206
_isDisposed = true;
@@ -215,6 +258,7 @@ public void SetIsVisible(bool visible)
215258
break;
216259
case false when _isVisible:
217260
DestroyTrayIcon();
261+
ReleaseTrayServiceName();
218262
break;
219263
}
220264

0 commit comments

Comments
 (0)