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.17.2] - 2026-05-29

### Fixed
- **Memory leaks** — AboutViewModel now properly disposes ManagementObject instances in all 5 WMI foreach loops (CPU, RAM, GPU, Display, OS detection).
- **Silent failures** — ThemeService Save/Load empty catch blocks now log errors via Serilog instead of swallowing silently.
- **Dashboard error handling** — replaced 4 bare `catch (Exception)` in alert scanners with logged exceptions for diagnostics.
- **UI flicker** — BulkInstallerViewModel.FilteredApps converted from ObservableCollection to BulkObservableCollection with ReplaceWith().
- **Visual consistency** — ShortcutCleanerView DataGrid now has `Background="Transparent"` and `BorderThickness="0"` matching all other views.

## [1.17.1] - 2026-05-29

### Fixed
Expand Down
5 changes: 3 additions & 2 deletions SysManager/SysManager/Services/ThemeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text.Json;
using System.Windows;
using System.Windows.Media;
using Serilog;

namespace SysManager.Services;

Expand Down Expand Up @@ -216,7 +217,7 @@
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettingsPath, json);
}
catch { }
catch (Exception ex) { Log.Debug("Theme save failed: {Error}", ex.Message); }

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
}

private void Load()
Expand Down Expand Up @@ -247,7 +248,7 @@
ApplyShade();
}
}
catch { }
catch (Exception ex) { Log.Debug("Theme load failed: {Error}", ex.Message); }

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
}

private sealed record ThemeSettings(
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.17.1</Version>
<FileVersion>1.17.1.0</FileVersion>
<AssemblyVersion>1.17.1.0</AssemblyVersion>
<Version>1.17.2</Version>
<FileVersion>1.17.2.0</FileVersion>
<AssemblyVersion>1.17.2.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
91 changes: 48 additions & 43 deletions SysManager/SysManager/ViewModels/AboutViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,17 +278,18 @@ private string CollectEnvironmentInfo()
using var cpuSearch = new System.Management.ManagementObjectSearcher(
"SELECT Name,NumberOfCores,NumberOfLogicalProcessors,MaxClockSpeed FROM Win32_Processor");
foreach (System.Management.ManagementObject mo in cpuSearch.Get())
{
var name = mo["Name"]?.ToString()?.Trim() ?? "unknown";
var cores = mo["NumberOfCores"];
var threads = mo["NumberOfLogicalProcessors"];
var mhz = mo["MaxClockSpeed"];
sb.Append("CPU: ").Append(name);
if (cores is not null) sb.Append($" ({cores}c/{threads}t)");
if (mhz is uint speed) sb.Append($" @ {speed / 1000.0:F1} GHz");
sb.AppendLine();
break;
}
using (mo)
{
var name = mo["Name"]?.ToString()?.Trim() ?? "unknown";
var cores = mo["NumberOfCores"];
var threads = mo["NumberOfLogicalProcessors"];
var mhz = mo["MaxClockSpeed"];
sb.Append("CPU: ").Append(name);
if (cores is not null) sb.Append($" ({cores}c/{threads}t)");
if (mhz is uint speed) sb.Append($" @ {speed / 1000.0:F1} GHz");
sb.AppendLine();
break;
}
Comment on lines 278 to +292
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Complete the resource cleanup by disposing ManagementObjectCollection.

The ManagementObjectSearcher.Get() method returns a ManagementObjectCollection which also implements IDisposable and holds unmanaged WMI resources. While disposing each ManagementObject addresses the immediate leak, not disposing the collection itself leaves the fix incomplete.

♻️ Recommended pattern for complete disposal

Apply this pattern to all five WMI queries (CPU, RAM, GPU, Display, Windows):

 using var cpuSearch = new System.Management.ManagementObjectSearcher(
     "SELECT Name,NumberOfCores,NumberOfLogicalProcessors,MaxClockSpeed FROM Win32_Processor");
-foreach (System.Management.ManagementObject mo in cpuSearch.Get())
+using var cpuResults = cpuSearch.Get();
+foreach (System.Management.ManagementObject mo in cpuResults)
     using (mo)
     {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/ViewModels/AboutViewModel.cs` around lines 278 - 292,
The ManagementObjectCollection returned by ManagementObjectSearcher.Get() is not
disposed; change the pattern in the CPU block (and the other WMI query blocks:
RAM, GPU, Display, Windows) to capture the collection into an IDisposable local
(e.g., var coll = cpuSearch.Get()) and wrap it in a using (or using var) so the
collection is disposed after enumeration, while still disposing each
ManagementObject inside the loop; update the code that currently calls
cpuSearch.Get() directly in the foreach to use this using-scoped collection for
full cleanup of unmanaged WMI resources.

}
catch (System.Management.ManagementException ex) { Log.Debug("CPU info unavailable: {Error}", ex.Message); }

Expand All @@ -298,13 +299,14 @@ private string CollectEnvironmentInfo()
using var memSearch = new System.Management.ManagementObjectSearcher(
"SELECT TotalVisibleMemorySize,FreePhysicalMemory FROM Win32_OperatingSystem");
foreach (System.Management.ManagementObject mo in memSearch.Get())
{
var totalKb = mo["TotalVisibleMemorySize"] as ulong? ?? 0;
var freeKb = mo["FreePhysicalMemory"] as ulong? ?? 0;
if (totalKb > 0)
sb.AppendLine($"RAM: {totalKb / 1024.0 / 1024.0:F1} GB total, {freeKb / 1024.0 / 1024.0:F1} GB free");
break;
}
using (mo)
{
var totalKb = mo["TotalVisibleMemorySize"] as ulong? ?? 0;
var freeKb = mo["FreePhysicalMemory"] as ulong? ?? 0;
if (totalKb > 0)
sb.AppendLine($"RAM: {totalKb / 1024.0 / 1024.0:F1} GB total, {freeKb / 1024.0 / 1024.0:F1} GB free");
break;
}
}
catch (System.Management.ManagementException ex) { Log.Debug("RAM info unavailable: {Error}", ex.Message); }

Expand All @@ -314,15 +316,16 @@ private string CollectEnvironmentInfo()
using var gpuSearch = new System.Management.ManagementObjectSearcher(
"SELECT Name,DriverVersion,AdapterRAM FROM Win32_VideoController");
foreach (System.Management.ManagementObject mo in gpuSearch.Get())
{
var name = mo["Name"]?.ToString()?.Trim() ?? "unknown";
var driver = mo["DriverVersion"]?.ToString() ?? "";
var vram = mo["AdapterRAM"] as uint? ?? 0;
sb.Append("GPU: ").Append(name);
if (vram > 0) sb.Append($" ({vram / 1024.0 / 1024.0 / 1024.0:F1} GB VRAM)");
if (!string.IsNullOrEmpty(driver)) sb.Append($" driver {driver}");
sb.AppendLine();
}
using (mo)
{
var name = mo["Name"]?.ToString()?.Trim() ?? "unknown";
var driver = mo["DriverVersion"]?.ToString() ?? "";
var vram = mo["AdapterRAM"] as uint? ?? 0;
sb.Append("GPU: ").Append(name);
if (vram > 0) sb.Append($" ({vram / 1024.0 / 1024.0 / 1024.0:F1} GB VRAM)");
if (!string.IsNullOrEmpty(driver)) sb.Append($" driver {driver}");
sb.AppendLine();
}
}
catch (System.Management.ManagementException ex) { Log.Debug("GPU info unavailable: {Error}", ex.Message); }

Expand All @@ -341,18 +344,19 @@ private string CollectEnvironmentInfo()
using var dispSearch = new System.Management.ManagementObjectSearcher(
"SELECT CurrentHorizontalResolution,CurrentVerticalResolution,CurrentRefreshRate FROM Win32_VideoController");
foreach (System.Management.ManagementObject mo in dispSearch.Get())
{
var w = mo["CurrentHorizontalResolution"];
var h = mo["CurrentVerticalResolution"];
var hz = mo["CurrentRefreshRate"];
if (w is not null && h is not null)
using (mo)
{
sb.Append($"Display: {w}×{h}");
if (hz is not null) sb.Append($" @ {hz} Hz");
sb.AppendLine();
break;
var w = mo["CurrentHorizontalResolution"];
var h = mo["CurrentVerticalResolution"];
var hz = mo["CurrentRefreshRate"];
if (w is not null && h is not null)
{
sb.Append($"Display: {w}×{h}");
if (hz is not null) sb.Append($" @ {hz} Hz");
sb.AppendLine();
break;
}
}
}
}
catch (System.Management.ManagementException ex) { Log.Debug("Display info unavailable: {Error}", ex.Message); }

Expand All @@ -377,12 +381,13 @@ private static string DescribeWindows()
using var searcher = new System.Management.ManagementObjectSearcher(
"SELECT Caption,BuildNumber FROM Win32_OperatingSystem");
foreach (System.Management.ManagementObject mo in searcher.Get())
{
var caption = mo["Caption"]?.ToString()?.Trim() ?? "";
var build = mo["BuildNumber"]?.ToString() ?? "";
if (!string.IsNullOrEmpty(caption))
return $"{caption} (build {build})";
}
using (mo)
{
var caption = mo["Caption"]?.ToString()?.Trim() ?? "";
var build = mo["BuildNumber"]?.ToString() ?? "";
if (!string.IsNullOrEmpty(caption))
return $"{caption} (build {build})";
}
}
catch (System.Management.ManagementException ex) { Log.Debug("WMI OS info unavailable: {Error}", ex.Message); }

Expand Down
6 changes: 2 additions & 4 deletions SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed partial class BulkInstallerViewModel : ViewModelBase
private CancellationTokenSource? _cts;

public BulkObservableCollection<InstallableApp> Apps { get; } = new();
public ObservableCollection<InstallableApp> FilteredApps { get; } = new();
public BulkObservableCollection<InstallableApp> FilteredApps { get; } = new();
public ICollectionView GroupedView { get; }

[ObservableProperty] private string _filterText = "";
Expand Down Expand Up @@ -344,9 +344,7 @@ private void ApplyFilter()
filtered = filtered.Where(a =>
a.Name.Contains(FilterText, StringComparison.OrdinalIgnoreCase));

FilteredApps.Clear();
foreach (var app in filtered)
FilteredApps.Add(app);
FilteredApps.ReplaceWith(filtered);
}

private static string GlyphForCategory(string category) => category switch
Expand Down
10 changes: 5 additions & 5 deletions SysManager/SysManager/ViewModels/DashboardViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,10 @@
});
}
}
catch (Exception)
catch (Exception ex)
{
// No NVIDIA or driver issue — GPU stays at default values
Log.Debug("GPU polling unavailable: {Error}", ex.Message);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
}

// ══════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -370,7 +370,7 @@
}
});
}
catch (Exception)
catch (Exception ex) { Log.Debug("Alert scan failed: {Error}", ex.Message); }//

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
{
System.Windows.Application.Current?.Dispatcher.BeginInvoke(() =>
{
Expand Down Expand Up @@ -425,7 +425,7 @@
}
});
}
catch (Exception)
catch (Exception ex) { Log.Debug("Alert scan failed: {Error}", ex.Message); }//

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
{
System.Windows.Application.Current?.Dispatcher.BeginInvoke(() =>
{
Expand Down Expand Up @@ -458,7 +458,7 @@
}
});
}
catch (Exception)
catch (Exception ex) { Log.Debug("Alert scan failed: {Error}", ex.Message); }//

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
{
System.Windows.Application.Current?.Dispatcher.BeginInvoke(() =>
{
Expand Down
4 changes: 2 additions & 2 deletions SysManager/SysManager/Views/ShortcutCleanerView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@
<DataGrid Grid.Row="2" ItemsSource="{Binding BrokenShortcuts}"
AutoGenerateColumns="False" IsReadOnly="False"
CanUserAddRows="False" CanUserDeleteRows="False"
CanUserSortColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="True" CanUserResizeColumns="True"
HeadersVisibility="Column" GridLinesVisibility="None"
Background="Transparent" BorderThickness="0"
Margin="28,12,28,0"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
Expand Down
Loading