Fix resource leaks and disposal issues in fleet alert tray

- Dispose GraphicsPath after creating Region to prevent GDI handle leak
- Track and dispose all Font objects to prevent GDI+ resource leak
- Join worker thread before disposal to ensure client not used after disposal
- Fix Screen.PrimaryScreen null dereference with fallback rectangle
- Propagate cancellation token to FetchAlertsAsync to stop orphaned requests
- Bump tray to 1.6.5, agent to 1.13.4
This commit is contained in:
Proudlock
2026-09-08 18:22:24 -04:00
parent 57f7225195
commit deb2bbe9df
4 changed files with 47 additions and 11 deletions

View File

@@ -16,8 +16,10 @@ internal sealed class FleetAlertForm : Form
private readonly string? hostname; private readonly string? hostname;
private System.Windows.Forms.Timer? countdownTimer; private System.Windows.Forms.Timer? countdownTimer;
private System.Windows.Forms.Timer? statusCheckTimer; private System.Windows.Forms.Timer? statusCheckTimer;
private readonly CancellationTokenSource statusCheckCancellation = new();
private int remainingSeconds; private int remainingSeconds;
private Label? countdownLabel; private Label? countdownLabel;
private readonly List<Font> fonts = new();
public event EventHandler? OnAcknowledgeClicked; public event EventHandler? OnAcknowledgeClicked;
@@ -35,7 +37,7 @@ internal sealed class FleetAlertForm : Form
ClientSize = new Size(400, 280); ClientSize = new Size(400, 280);
BackColor = Color.White; BackColor = Color.White;
var screen = Screen.PrimaryScreen?.WorkingArea ?? Screen.PrimaryScreen!.Bounds; var screen = Screen.PrimaryScreen?.WorkingArea ?? Screen.PrimaryScreen?.Bounds ?? new Rectangle(0, 0, 1920, 1080);
Location = new Point(screen.Right - Width - 20, screen.Top + 20); Location = new Point(screen.Right - Width - 20, screen.Top + 20);
BuildLayout(); BuildLayout();
@@ -49,6 +51,7 @@ internal sealed class FleetAlertForm : Form
{ {
countdownTimer?.Stop(); countdownTimer?.Stop();
statusCheckTimer?.Stop(); statusCheckTimer?.Stop();
statusCheckCancellation.Cancel();
} }
private void FleetAlertForm_Load(object? sender, EventArgs e) private void FleetAlertForm_Load(object? sender, EventArgs e)
@@ -77,7 +80,7 @@ internal sealed class FleetAlertForm : Form
{ {
try try
{ {
var alerts = await client.FetchAlertsAsync(hostname, CancellationToken.None) var alerts = await client.FetchAlertsAsync(hostname, statusCheckCancellation.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
var stillActive = alerts?.Any(a => a.NotificationId == alert.NotificationId) ?? false; var stillActive = alerts?.Any(a => a.NotificationId == alert.NotificationId) ?? false;
@@ -90,6 +93,10 @@ internal sealed class FleetAlertForm : Form
})); }));
} }
} }
catch (OperationCanceledException)
{
// Form is closing, ignore
}
catch catch
{ {
// Ignore errors - keep the form open if we can't check status // Ignore errors - keep the form open if we can't check status
@@ -99,7 +106,7 @@ internal sealed class FleetAlertForm : Form
private void ApplyRoundedCorners() private void ApplyRoundedCorners()
{ {
var path = new GraphicsPath(); using var path = new GraphicsPath();
int radius = 8; int radius = 8;
var rect = new Rectangle(0, 0, Width, Height); var rect = new Rectangle(0, 0, Width, Height);
@@ -146,6 +153,13 @@ internal sealed class FleetAlertForm : Form
} }
} }
private Font CreateFont(string family, float size, FontStyle style = FontStyle.Regular)
{
var font = new Font(family, size, style);
fonts.Add(font);
return font;
}
private void BuildLayout() private void BuildLayout()
{ {
var borderPanel = new Panel var borderPanel = new Panel
@@ -182,7 +196,7 @@ internal sealed class FleetAlertForm : Form
Left = 16, Left = 16,
Top = 14, Top = 14,
Width = 300, Width = 300,
Font = new Font("Segoe UI", 10, FontStyle.Bold), Font = CreateFont("Segoe UI", 10, FontStyle.Bold),
ForeColor = Color.FromArgb(51, 51, 51), ForeColor = Color.FromArgb(51, 51, 51),
}; };
headerPanel.Controls.Add(typeLabel); headerPanel.Controls.Add(typeLabel);
@@ -195,7 +209,7 @@ internal sealed class FleetAlertForm : Form
Width = 24, Width = 24,
Height = 24, Height = 24,
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
Font = new Font("Segoe UI", 14), Font = CreateFont("Segoe UI", 14),
ForeColor = Color.FromArgb(102, 102, 102), ForeColor = Color.FromArgb(102, 102, 102),
BackColor = Color.Transparent, BackColor = Color.Transparent,
Cursor = Cursors.Hand, Cursor = Cursors.Hand,
@@ -212,7 +226,7 @@ internal sealed class FleetAlertForm : Form
Top = 58, Top = 58,
Width = 360, Width = 360,
Height = 50, Height = 50,
Font = new Font("Segoe UI", 11, FontStyle.Bold), Font = CreateFont("Segoe UI", 11, FontStyle.Bold),
ForeColor = Color.FromArgb(51, 51, 51), ForeColor = Color.FromArgb(51, 51, 51),
AutoSize = false, AutoSize = false,
}; };
@@ -225,7 +239,7 @@ internal sealed class FleetAlertForm : Form
Top = 115, Top = 115,
Width = 360, Width = 360,
Height = 80, Height = 80,
Font = new Font("Segoe UI", 9.75f), Font = CreateFont("Segoe UI", 9.75f),
ForeColor = Color.FromArgb(85, 85, 85), ForeColor = Color.FromArgb(85, 85, 85),
AutoSize = false, AutoSize = false,
}; };
@@ -241,7 +255,7 @@ internal sealed class FleetAlertForm : Form
BackColor = Color.FromArgb(76, 175, 80), BackColor = Color.FromArgb(76, 175, 80),
ForeColor = Color.White, ForeColor = Color.White,
FlatStyle = FlatStyle.Flat, FlatStyle = FlatStyle.Flat,
Font = new Font("Segoe UI", 9, FontStyle.Bold), Font = CreateFont("Segoe UI", 9, FontStyle.Bold),
Cursor = Cursors.Hand, Cursor = Cursors.Hand,
}; };
acknowledgeButton.FlatAppearance.BorderSize = 0; acknowledgeButton.FlatAppearance.BorderSize = 0;
@@ -277,7 +291,7 @@ internal sealed class FleetAlertForm : Form
Width = 395, Width = 395,
Height = 20, Height = 20,
TextAlign = ContentAlignment.MiddleCenter, TextAlign = ContentAlignment.MiddleCenter,
Font = new Font("Segoe UI", 8.25f), Font = CreateFont("Segoe UI", 8.25f),
ForeColor = Color.FromArgb(153, 153, 153), ForeColor = Color.FromArgb(153, 153, 153),
}; };
footerPanel.Controls.Add(countdownLabel); footerPanel.Controls.Add(countdownLabel);
@@ -329,6 +343,13 @@ internal sealed class FleetAlertForm : Form
countdownTimer?.Dispose(); countdownTimer?.Dispose();
statusCheckTimer?.Stop(); statusCheckTimer?.Stop();
statusCheckTimer?.Dispose(); statusCheckTimer?.Dispose();
statusCheckCancellation.Cancel();
statusCheckCancellation.Dispose();
foreach (var font in fonts)
{
font.Dispose();
}
fonts.Clear();
} }
base.Dispose(disposing); base.Dispose(disposing);
} }

View File

@@ -183,6 +183,15 @@ internal sealed class FleetAlertWorker : IDisposable
public void Dispose() public void Dispose()
{ {
cancellation.Cancel(); cancellation.Cancel();
if (workerThread.IsAlive)
{
if (!workerThread.Join(TimeSpan.FromSeconds(5)))
{
logger.LogWarning("Fleet alert worker thread did not exit within timeout.");
}
}
cancellation.Dispose(); cancellation.Dispose();
} }
} }

View File

@@ -82,7 +82,11 @@
<!-- 1.6.4: fix UI thread deadlock. Acknowledgment and status checks now run async <!-- 1.6.4: fix UI thread deadlock. Acknowledgment and status checks now run async
properly without blocking the UI thread. Acknowledgment is marked locally first, properly without blocking the UI thread. Acknowledgment is marked locally first,
then posted to server in background to prevent lockups. --> then posted to server in background to prevent lockups. -->
<Version>1.6.4</Version> <!-- 1.6.5: fix resource leaks and disposal issues. GraphicsPath properly disposed,
Font objects tracked and disposed, worker thread joined before client disposal,
Screen.PrimaryScreen null dereference prevented, status check cancellation token
propagated to prevent orphaned HTTP requests. -->
<Version>1.6.5</Version>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>

View File

@@ -177,7 +177,9 @@
No agent code changes, but deployed together for version consistency. --> No agent code changes, but deployed together for version consistency. -->
<!-- 1.13.3: coordinated release with tray 1.6.4 (deadlock fix). <!-- 1.13.3: coordinated release with tray 1.6.4 (deadlock fix).
No agent code changes, but deployed together for version consistency. --> No agent code changes, but deployed together for version consistency. -->
<Version>1.13.3</Version> <!-- 1.13.4: coordinated release with tray 1.6.5 (resource leak fixes).
No agent code changes, but deployed together for version consistency. -->
<Version>1.13.4</Version>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>