Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.20.4] - 2026-06-08

### Fixed
- **Ping monitor no longer leaks its CancellationTokenSource.** When `Stop()` was called while the ping loop was still winding down (the 1.5s wait timed out), the CTS reference was dropped and never disposed. It is now disposed once the loop actually finishes — immediately if already complete, otherwise via a continuation.
- **System Logs no longer block the UI thread while reading the Event Log.** `EventLogService.ReadAsync` ran the blocking `EventLogReader.ReadEvent()` COM call on the caller's (UI) thread, freezing the app while large logs were enumerated. Each read now runs on the thread pool via `Task.Run`.

### Changed
- **Dashboard GPU name now works for AMD/Intel, not just NVIDIA.** When no NVIDIA GPU is present, the Dashboard falls back to `Win32_VideoController` (WMI) to show the adapter name. Live usage % remains NVIDIA-only (it requires vendor-specific APIs).

## [1.20.3] - 2026-06-08

### Fixed
Expand Down
59 changes: 59 additions & 0 deletions SysManager/SysManager.Tests/PingMonitorServiceLifecycleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SysManager · PingMonitorServiceLifecycleTests
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using SysManager.Services;

namespace SysManager.Tests;

/// <summary>
/// Lifecycle/regression tests for <see cref="PingMonitorService"/>. The Stop()
/// path was rewritten so the CancellationTokenSource is always disposed (even
/// when the loop is still winding down) instead of being dropped and leaked.
/// These verify the start/stop/dispose cycle is safe and idempotent.
/// </summary>
public class PingMonitorServiceLifecycleTests
{
[Fact]
public void StartStop_DoesNotThrow()
{
using var svc = new PingMonitorService();
svc.Start();
svc.Stop();
}

[Fact]
public void Stop_WithoutStart_IsNoOp()
{
using var svc = new PingMonitorService();
svc.Stop(); // never started — must not throw
}

[Fact]
public void RepeatedStartStop_DoesNotThrow()
{
using var svc = new PingMonitorService();
for (var i = 0; i < 5; i++)
{
svc.Start();
svc.Stop();
}
}

[Fact]
public void Dispose_AfterStart_DoesNotThrow()
{
var svc = new PingMonitorService();
svc.Start();
svc.Dispose(); // Dispose() routes through Stop()
}

[Fact]
public void DoubleStart_IsIdempotent()
{
using var svc = new PingMonitorService();
svc.Start();
svc.Start(); // second Start() must be ignored while running, not leak a CTS
svc.Stop();
}
}
13 changes: 8 additions & 5 deletions SysManager/SysManager/Services/EventLogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,17 @@
int emitted = 0;
using (reader)
{
var localReader = reader;
while (!ct.IsCancellationRequested && emitted < opt.MaxResults)
{
EventRecord? rec = null;
try { rec = reader.ReadEvent(); }
// ReadEvent() is a blocking COM/IO call. Run it on a thread-pool
// thread so enumerating large logs never blocks the UI thread the
// caller awaits on. (await Task.Yield() alone did not move the work
// off the UI thread — it only released it momentarily per 200 rows.)
EventRecord? rec;

Check notice

Code scanning / CodeQL

Missed 'using' opportunity Note

This variable is manually
disposed
in a
finally block
- consider a C# using statement as a preferable resource management technique.
try { rec = await Task.Run(() => localReader.ReadEvent(), ct).ConfigureAwait(false); }
catch (EventLogException) { continue; }
catch (OperationCanceledException) { yield break; }
if (rec is null) yield break;

FriendlyEventEntry? entry = null;
Expand All @@ -61,9 +67,6 @@

emitted++;
yield return entry;

// Yield occasionally to keep the UI responsive on huge logs.
if (emitted % 200 == 0) await Task.Yield();
}
}
}
Expand Down
26 changes: 17 additions & 9 deletions SysManager/SysManager/Services/PingMonitorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,25 @@ public void Stop()
{
lock (_stateLock)
{
_cts?.Cancel();
try { _loop?.Wait(1500); }
catch (AggregateException) { /* task cancellation or faulted — expected during stop */ }
catch (ObjectDisposedException) { /* task already cleaned up */ }
// Only dispose CTS if the loop actually completed; otherwise the
// background task still holds a reference to the token and would
// throw ObjectDisposedException on next cancellation check.
if (_loop is { IsCompleted: true })
_cts?.Dispose();
var cts = _cts;
var loop = _loop;
_cts = null;
_loop = null;
if (cts is null) return;

cts.Cancel();
try { loop?.Wait(1500); }
catch (AggregateException) { /* task cancellation or faulted — expected during stop */ }
catch (ObjectDisposedException) { /* task already cleaned up */ }

// Dispose the CTS once the loop has actually finished. If Wait timed out
// (the loop is still winding down), defer disposal to a continuation so the
// CTS is never leaked — the previous code dropped the reference and never
// disposed it in that case.
if (loop is null || loop.IsCompleted)
cts.Dispose();
else
loop.ContinueWith(_ => cts.Dispose(), TaskScheduler.Default);
}
}

Expand Down
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<RootNamespace>SysManager</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>NU1603;NU1701</NoWarn>
<Version>1.20.3</Version>
<FileVersion>1.20.3.0</FileVersion>
<AssemblyVersion>1.20.3.0</AssemblyVersion>
<Version>1.20.4</Version>
<FileVersion>1.20.4.0</FileVersion>
<AssemblyVersion>1.20.4.0</AssemblyVersion>
<Product>SysManager</Product>
<Description>SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.</Description>
<PackageProjectUrl>https://github.com/laurentiu021/SystemManager</PackageProjectUrl>
Expand Down
40 changes: 39 additions & 1 deletion SysManager/SysManager/ViewModels/DashboardViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,49 @@ private void UpdateGpuUsage()
GpuName = gpu.FullName;
GpuVram = $"{memUsed:F1} / {memTotal:F1} GB VRAM";
});
return;
}
}
catch (Exception ex)
{
Log.Debug("GPU polling unavailable: {Error}", ex.Message);
Log.Debug("NVIDIA GPU polling unavailable: {Error}", ex.Message);
}

// No NVIDIA GPU (NvAPI only covers NVIDIA). Fall back to WMI so AMD/Intel
// GPUs at least show the adapter name. Live usage % is NVIDIA-only because
// it requires vendor-specific APIs.
UpdateGpuNameFromWmi();
}

private void UpdateGpuNameFromWmi()
{
try
{
using var searcher = new System.Management.ManagementObjectSearcher(
"SELECT Name FROM Win32_VideoController");
using var collection = searcher.Get();
foreach (System.Management.ManagementObject mo in collection)
{
using (mo)
{
var name = mo["Name"]?.ToString()?.Trim();
if (string.IsNullOrEmpty(name)) continue;
System.Windows.Application.Current?.Dispatcher.BeginInvoke(() =>
{
GpuName = name;
GpuVram = "";
});
return; // first adapter is enough
}
}
}
catch (System.Management.ManagementException ex)
{
Log.Debug("WMI GPU name lookup unavailable: {Error}", ex.Message);
}
catch (System.Runtime.InteropServices.COMException ex)
{
Log.Debug("WMI GPU name lookup failed: {Error}", ex.Message);
}
}

Expand Down
Loading