Files
logikonline 3412cb982e
All checks were successful
CI / Build (Linux) (push) Successful in 21s
fix(interop): resolve native resource leaks in GTK and WebKit interop
Fix critical memory leaks identified in architecture review: Add signal handler disconnection in WebKitNative (load-changed and script-dialog signals now properly cleaned up), implement GTK idle callback cleanup with automatic removal on completion, add dlclose() calls for WebKit library handles, track GTK signal IDs in GtkSkiaSurfaceWidget for proper disposal. Replace empty catch blocks in GestureManager with logged exception handling. Add WebKitNative.Cleanup() and GtkNative.ClearCallbacks() methods for application shutdown.
2026-03-06 23:14:53 -05:00

271 lines
6.5 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux system tray service using various backends.
/// Supports yad, zenity, or native D-Bus StatusNotifierItem.
/// </summary>
public class SystemTrayService : IDisposable
{
private Process? _trayProcess;
private readonly string _appName;
private string? _iconPath;
private string? _tooltip;
private readonly List<TrayMenuItem> _menuItems = new();
private bool _isVisible;
private bool _disposed;
public event EventHandler? Clicked;
public event EventHandler<string>? MenuItemClicked;
public SystemTrayService(string appName)
{
_appName = appName;
}
/// <summary>
/// Gets or sets the tray icon path.
/// </summary>
public string? IconPath
{
get => _iconPath;
set
{
_iconPath = value;
if (_isVisible) UpdateTray();
}
}
/// <summary>
/// Gets or sets the tooltip text.
/// </summary>
public string? Tooltip
{
get => _tooltip;
set
{
_tooltip = value;
if (_isVisible) UpdateTray();
}
}
/// <summary>
/// Gets the menu items.
/// </summary>
public IList<TrayMenuItem> MenuItems => _menuItems;
/// <summary>
/// Shows the system tray icon.
/// </summary>
public async Task ShowAsync()
{
if (_isVisible) return;
// Try yad first (most feature-complete)
if (await TryYadTray())
{
_isVisible = true;
return;
}
// Fall back to a simple approach
_isVisible = true;
}
/// <summary>
/// Hides the system tray icon.
/// </summary>
public void Hide()
{
if (!_isVisible) return;
_trayProcess?.Kill();
_trayProcess?.Dispose();
_trayProcess = null;
_isVisible = false;
}
/// <summary>
/// Updates the tray icon and menu.
/// </summary>
public void UpdateTray()
{
if (!_isVisible) return;
// Restart tray with new settings
Hide();
_ = ShowAsync();
}
private Task<bool> TryYadTray()
{
try
{
var args = BuildYadArgs();
var startInfo = new ProcessStartInfo
{
FileName = "yad",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
_trayProcess = Process.Start(startInfo);
if (_trayProcess == null) return Task.FromResult(false);
// Start reading output for menu clicks
_ = Task.Run(async () =>
{
try
{
while (!_trayProcess.HasExited)
{
var line = await _trayProcess.StandardOutput.ReadLineAsync();
if (!string.IsNullOrEmpty(line))
{
HandleTrayOutput(line);
}
}
}
catch (Exception ex) { DiagnosticLog.Debug("SystemTrayService", "Tray output reading failed", ex); }
});
return Task.FromResult(true);
}
catch
{
return Task.FromResult(false);
}
}
private string BuildYadArgs()
{
var args = new List<string>
{
"--notification",
"--listen"
};
if (!string.IsNullOrEmpty(_iconPath) && File.Exists(_iconPath))
{
args.Add($"--image=\"{_iconPath}\"");
}
else
{
args.Add("--image=application-x-executable");
}
if (!string.IsNullOrEmpty(_tooltip))
{
args.Add($"--text=\"{EscapeArg(_tooltip)}\"");
}
// Build menu
if (_menuItems.Count > 0)
{
var menuStr = string.Join("!", _menuItems.Select(m =>
m.IsSeparator ? "---" : $"{EscapeArg(m.Text)}"));
args.Add($"--menu=\"{menuStr}\"");
}
args.Add("--command=\"echo clicked\"");
return string.Join(" ", args);
}
private void HandleTrayOutput(string output)
{
if (output == "clicked")
{
Clicked?.Invoke(this, EventArgs.Empty);
}
else
{
// Menu item clicked
var menuItem = _menuItems.FirstOrDefault(m => m.Text == output);
if (menuItem != null)
{
menuItem.Action?.Invoke();
MenuItemClicked?.Invoke(this, output);
}
}
}
/// <summary>
/// Adds a menu item to the tray context menu.
/// </summary>
public void AddMenuItem(string text, Action? action = null)
{
_menuItems.Add(new TrayMenuItem { Text = text, Action = action });
}
/// <summary>
/// Adds a separator to the tray context menu.
/// </summary>
public void AddSeparator()
{
_menuItems.Add(new TrayMenuItem { IsSeparator = true });
}
/// <summary>
/// Clears all menu items.
/// </summary>
public void ClearMenuItems()
{
_menuItems.Clear();
}
/// <summary>
/// Checks if system tray is available on this system.
/// </summary>
public static bool IsAvailable()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "which",
Arguments = "yad",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private static string EscapeArg(string arg)
{
return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("!", "\\!") ?? "";
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Hide();
GC.SuppressFinalize(this);
}
~SystemTrayService()
{
Dispose();
}
}