diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 6f628a6814f..cab3ba0d3f4 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -23,8 +23,13 @@ internal class DBusTrayIconImpl : ITrayIconImpl private (int, int, byte[]) _icon; private string? _sysTrayServiceName; + // This task shows if the name request is in progress, complete, or failed. + private Task? _sysTrayServiceNameRequest; + // The bus keeps the name until the release is complete. A request before that fails. + private Task? _sysTrayServiceNameRelease; private string? _tooltipText; private bool _isDisposed; + private bool _itemExported; private bool _serviceConnected; private bool _isVisible = true; @@ -52,8 +57,8 @@ public DBusTrayIconImpl() MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusMenuPath, _connection); + // CreateTrayIcon exports this object when the connection owns the name. _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath); - _connection.AddMethodHandler(_statusNotifierItemDbusObj); _statusNotifierItemDbusObj.ActivationDelegate += () => OnClicked?.Invoke(); WatchAsync(); @@ -93,6 +98,10 @@ private async void WatchAsync() OnOwnerChanged(owner); } } + catch (OperationCanceledException) + { + // Dispose cancels this task. The continuation runs after Dispose sets _isDisposed. + } catch (Exception e) when (!_isDisposed) { Logger.TryGet(LogEventLevel.Error, "DBUS") @@ -110,8 +119,6 @@ private void OnOwnerChanged(string? newOwner) _serviceConnected = true; _statusNotifierWatcher = new StatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); - DestroyTrayIcon(); - if (_isVisible) CreateTrayIcon(); } @@ -124,40 +131,118 @@ private void OnOwnerChanged(string? newOwner) private async void CreateTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierWatcher is null) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _statusNotifierWatcher is null) return; + Task? request = null; + + try + { + // Keep the name for the life of the icon. A new id shows a new item to the host. + if (_sysTrayServiceName is null) + { #if NET5_0_OR_GREATER - var pid = Environment.ProcessId; + var pid = Environment.ProcessId; #else - var pid = Process.GetCurrentProcess().Id; + var pid = Process.GetCurrentProcess().Id; #endif - var tid = s_trayIconInstanceId++; + var tid = s_trayIconInstanceId++; + _sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}"); + } + + if (_sysTrayServiceNameRelease is { } release) + { + await release; + if (ReferenceEquals(_sysTrayServiceNameRelease, release)) + _sysTrayServiceNameRelease = null; + if (!ShouldShowTrayIcon) + return; + } + + request = _sysTrayServiceNameRequest ??= _connection.RequestNameAsync(_sysTrayServiceName); + await request; + + // The icon can become hidden or disposed while the bus answers. The release then runs + // after this line. + if (!ShouldShowTrayIcon || !ReferenceEquals(_sysTrayServiceNameRequest, request)) + return; + + // Export the object only while the connection owns the name. If not, a host that scans + // the bus adds a second item. Two calls can get here together, and a second export + // throws. + if (!_itemExported) + { + _connection.AddMethodHandler(_statusNotifierItemDbusObj); + _itemExported = true; + } - _connection.RemoveMethodHandler(_statusNotifierItemDbusObj!.Path); - _connection.AddMethodHandler(_statusNotifierItemDbusObj); + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - _sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}"); - await _connection.RequestNameAsync(_sysTrayServiceName); - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + if (!ShouldShowTrayIcon) + return; - _statusNotifierItemDbusObj!.SetTitleAndTooltip(_tooltipText); - _statusNotifierItemDbusObj.SetIcon(_icon); + _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); + _statusNotifierItemDbusObj.SetIcon(_icon); + } + catch (Exception e) + { + // Clear only this request, and only if it did not complete. The next call then asks for + // the name again. + if (request is { Status: not TaskStatus.RanToCompletion } && ReferenceEquals(_sysTrayServiceNameRequest, request)) + _sysTrayServiceNameRequest = null; + + if (!_isDisposed) + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, "Unable to register the system tray icon.\n{Exception}", e); + } } + // CreateTrayIcon reads these conditions again after each await. + private bool ShouldShowTrayIcon => !_isDisposed && _isVisible && _serviceConnected; + private void DestroyTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _connection!.ReleaseNameAsync(_sysTrayServiceName); _connection.RemoveMethodHandler(_statusNotifierItemDbusObj.Path); + _itemExported = false; + } + + /// + /// Releases the bus name. A host removes the item when the name goes away. + /// Call this after , but not when the watcher stops. + /// + private void ReleaseTrayServiceName() + { + if (_connection is null || _sysTrayServiceName is null || _sysTrayServiceNameRequest is not { } request) + return; + + _sysTrayServiceNameRequest = null; + _sysTrayServiceNameRelease = ReleaseTrayServiceName(request, _connection, _sysTrayServiceName); + } + + private async Task ReleaseTrayServiceName(Task request, DBusConnection connection, string name) + { + try + { + // Wait for the request. A release before the connection owns the name has no effect. + await request; + await connection.ReleaseNameAsync(name); + } + catch (Exception e) + { + if (!_isDisposed) + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, "Unable to release the system tray icon name.\n{Exception}", e); + } } public void Dispose() { IsActive = false; DestroyTrayIcon(); + ReleaseTrayServiceName(); (MenuExporter as IDisposable)?.Dispose(); _watchCts?.Cancel(); _isDisposed = true; @@ -215,6 +300,7 @@ public void SetIsVisible(bool visible) break; case false when _isVisible: DestroyTrayIcon(); + ReleaseTrayServiceName(); break; }