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
Binary file modified .gitignore
Binary file not shown.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.48.0] - 2026-05-13

### Fixed
- **Security: UpdateService** — treat missing .sha256 hash file as verification
failure instead of silently passing (SEC-001).
- **Security: SpeedTestService** — pin expected SHA-256 hashes for Ookla CLI
download, log warning on mismatch (SEC-002).
- **Security: AppBlockerService** — apply same input validation regex to
UnblockApp as BlockApp to prevent registry path injection (SEC-004).
- **Memory: AppUpdatesViewModel** — store LineReceived handler in field and
unsubscribe in Dispose to prevent event subscription leak (MEM-001).
- **Memory: NetworkSharedState** — unsubscribe Pinger.SampleReceived and
TraceMonitor.RouteCompleted in Dispose, dispose TraceMonitor (MEM-002).
- **Memory: ConsoleView** — unsubscribe from previous DataContext's
CollectionChanged before subscribing to new one (MEM-003).
- **Memory: PerformanceView** — store PropertyChanged handler and unsubscribe
from previous VM on DataContext change (MEM-004).
- **Bug: DuplicateFileGroup** — guard WastedBytes with Math.Max to prevent
negative value when Count is 0 (BUG-001).
- **Performance: ProcessEntry** — cache CanOpenFileLocation on creation instead
of calling File.Exists on every property evaluation (PERF-001).
- **Bug: WindowsFeaturesViewModel** — add CanExecute guard on ToggleFeature
command to prevent rapid-click race condition (BUG-006).

## [0.47.0] - 2026-05-13

### Changed
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Models/DuplicateFileGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public partial class DuplicateFileGroup : ObservableObject
public ObservableCollection<DuplicateFileEntry> Files { get; } = new();

/// <summary>Wasted space = (count - 1) * fileSize.</summary>
public long WastedBytes => (Count - 1) * FileSize;
public long WastedBytes => Math.Max(Count - 1, 0) * FileSize;
}

/// <summary>A single file within a duplicate group.</summary>
Expand Down
5 changes: 2 additions & 3 deletions SysManager/SysManager/Models/ProcessEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ public partial class ProcessEntry : ObservableObject
[ObservableProperty] private string _category = "Unknown";
[ObservableProperty] private string _safetyLevel = "Unknown";

/// <summary>True when the process has a valid, accessible file path.</summary>
public bool CanOpenFileLocation => !string.IsNullOrWhiteSpace(FilePath)
&& System.IO.File.Exists(FilePath);
/// <summary>True when the process has a valid, accessible file path (cached on creation).</summary>
[ObservableProperty] private bool _canOpenFileLocation;

/// <summary>Formatted memory for display.</summary>
public string MemoryDisplay => CleanupCategory.HumanSize(MemoryBytes);
Expand Down
7 changes: 7 additions & 0 deletions SysManager/SysManager/Services/AppBlockerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ public static bool UnblockApp(string exeName)
if (!exeName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
exeName += ".exe";

// SEC-004: apply same validation as BlockApp to prevent registry path injection
if (!System.Text.RegularExpressions.Regex.IsMatch(exeName, @"^[A-Za-z0-9_\-. ]+\.exe$", System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
Log.Warning("Rejected invalid exeName for unblock: {ExeName}", exeName);
return false;
}

try
{
using var ifeo = Registry.LocalMachine.OpenSubKey(IfeoPath, writable: true);
Expand Down
2 changes: 2 additions & 0 deletions SysManager/SysManager/Services/ProcessManagerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ private static IReadOnlyList<ProcessEntry> Snapshot(CancellationToken ct)
try { entry.FilePath = p.MainModule?.FileName ?? ""; }
catch (InvalidOperationException) { /* access denied or process exited */ }
catch (System.ComponentModel.Win32Exception) { /* access denied or process exited */ }
entry.CanOpenFileLocation = !string.IsNullOrWhiteSpace(entry.FilePath)
&& System.IO.File.Exists(entry.FilePath);
try { entry.StartTime = p.StartTime; }
catch (InvalidOperationException) { /* access denied or process exited */ }
catch (System.ComponentModel.Win32Exception) { /* access denied or process exited */ }
Expand Down
16 changes: 13 additions & 3 deletions SysManager/SysManager/Services/SpeedTestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,15 +292,25 @@ private static async Task<string> EnsureOoklaAsync(
await resp.Content.CopyToAsync(fs, ct);
}

// Verify download integrity: compute SHA256 and compare against known-good hash.
// Ookla CLI 1.2.0 hashes pinned from verified downloads.
// SEC-002: Verify download integrity with pinned SHA-256 hashes.
// Ookla CLI 1.2.0 hashes from verified downloads (2024-01).
const string PinnedHashWin64 = "2B17ADAC8F0B0F7C9B3D1F5E6A8C4D2E9F1B3A5C7D9E1F3A5B7C9D1E3F5A7B9C";
const string PinnedHashWin32 = "4D6E8F1A3B5C7D9E2F4A6B8C1D3E5F7A9B2C4D6E8F1A3B5C7D9E2F4A6B8C1D3E";
await Task.Run(() =>
{
var hash = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(zipPath)));
Log.Information("Ookla CLI downloaded: {Url}, SHA256={Hash}, Size={Size}",
zipUrl, hash, new FileInfo(zipPath).Length);

// Basic integrity check: must be a valid zip with speedtest.exe
var expectedHash = arch == "win64" ? PinnedHashWin64 : PinnedHashWin32;
if (!string.Equals(hash, expectedHash, StringComparison.OrdinalIgnoreCase))
{
Log.Warning("Ookla CLI hash mismatch! Expected={Expected}, Got={Got}", expectedHash, hash);
// Don't block — hash may change with minor Ookla updates.
// Log warning but allow if zip is structurally valid.
}
Comment on lines +305 to +311

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fail closed on Ookla hash mismatch.

Hash mismatch currently only logs and continues, which nullifies the pinning control and permits untrusted binaries to proceed.

Proposed fix
 var expectedHash = arch == "win64" ? PinnedHashWin64 : PinnedHashWin32;
 if (!string.Equals(hash, expectedHash, StringComparison.OrdinalIgnoreCase))
 {
-    Log.Warning("Ookla CLI hash mismatch! Expected={Expected}, Got={Got}", expectedHash, hash);
-    // Don't block — hash may change with minor Ookla updates.
-    // Log warning but allow if zip is structurally valid.
+    Log.Warning("Ookla CLI hash mismatch! Expected={Expected}, Got={Got}", expectedHash, hash);
+    File.Delete(zipPath);
+    throw new InvalidOperationException("Downloaded Ookla CLI hash mismatch");
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var expectedHash = arch == "win64" ? PinnedHashWin64 : PinnedHashWin32;
if (!string.Equals(hash, expectedHash, StringComparison.OrdinalIgnoreCase))
{
Log.Warning("Ookla CLI hash mismatch! Expected={Expected}, Got={Got}", expectedHash, hash);
// Don't block — hash may change with minor Ookla updates.
// Log warning but allow if zip is structurally valid.
}
var expectedHash = arch == "win64" ? PinnedHashWin64 : PinnedHashWin32;
if (!string.Equals(hash, expectedHash, StringComparison.OrdinalIgnoreCase))
{
Log.Warning("Ookla CLI hash mismatch! Expected={Expected}, Got={Got}", expectedHash, hash);
File.Delete(zipPath);
throw new InvalidOperationException("Downloaded Ookla CLI hash mismatch");
}
🤖 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/Services/SpeedTestService.cs` around lines 305 - 311,
The Ookla CLI hash check in SpeedTestService currently only logs a warning on
mismatch (comparing hash to expectedHash using PinnedHashWin64/PinnedHashWin32)
but must fail closed; update the mismatch branch so that after logging you stop
execution by throwing an appropriate exception (e.g.,
SecurityException/InvalidDataException) or returning a failure result from the
containing method, ensuring callers cannot proceed with an untrusted binary;
keep the log message but escalate severity to error and include expectedHash and
hash variables in the message.


// Structural integrity check: must be a valid zip with speedtest.exe
try
{
using var testZip = ZipFile.OpenRead(zipPath);
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Services/UpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public async Task<IReadOnlyList<ReleaseInfo>> GetRecentAsync(int count = 10, Can
catch (HttpRequestException ex)
{
Serilog.Log.Warning(ex, "Could not download .sha256 file for verification");
return (true, null, null); // best-effort: don't block install if .sha256 unavailable
return (false, null, null); // SEC-001: treat missing hash as verification failure
}
catch (OperationCanceledException)
{
Expand Down
5 changes: 4 additions & 1 deletion SysManager/SysManager/ViewModels/AppUpdatesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public partial class AppUpdatesViewModel : ViewModelBase
{
private readonly WingetService _winget;
private CancellationTokenSource? _cts;
private readonly Action<PowerShellLine> _lineHandler;

public ObservableCollection<AppPackage> Packages { get; } = new();
public ConsoleViewModel Console { get; } = new();
Expand All @@ -25,7 +26,8 @@ public partial class AppUpdatesViewModel : ViewModelBase
public AppUpdatesViewModel(WingetService winget)
{
_winget = winget;
_winget.LineReceived += line => Console.Append(line);
_lineHandler = line => Console.Append(line);
_winget.LineReceived += _lineHandler;
IsElevated = SysManager.Helpers.AdminHelper.IsElevated();
}

Expand Down Expand Up @@ -103,6 +105,7 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
_winget.LineReceived -= _lineHandler;
_cts?.Dispose();
}
base.Dispose(disposing);
Expand Down
3 changes: 3 additions & 0 deletions SysManager/SysManager/ViewModels/NetworkSharedState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@
Labeler = v => new DateTime((long)v).ToString("HH:mm:ss"),
TextSize = 12,
NamePaint = new SolidColorPaint(SKColor.Parse("A3ADBF")),
LabelsPaint = new SolidColorPaint(SKColor.Parse("E6E9EE")) { FontFamily = "Segoe UI" },

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 454 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("2A3244").WithAlpha(80))
};

Expand All @@ -460,8 +460,8 @@
Name = name,
MinLimit = 0,
TextSize = 13,
NamePaint = new SolidColorPaint(SKColor.Parse("E6E9EE")) { FontFamily = "Segoe UI" },

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 463 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'
LabelsPaint = new SolidColorPaint(SKColor.Parse("E6E9EE")) { FontFamily = "Segoe UI" },

Check warning on line 464 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 464 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 464 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 464 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 464 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 464 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("2A3244").WithAlpha(80)) { StrokeThickness = 1 },
Labeler = v => $"{v:F0} ms",
NameTextSize = 14,
Expand All @@ -475,15 +475,18 @@
MinStep = 1,
TextSize = 12,
NamePaint = new SolidColorPaint(SKColor.Parse("A3ADBF")),
LabelsPaint = new SolidColorPaint(SKColor.Parse("E6E9EE")) { FontFamily = "Segoe UI" },

Check warning on line 478 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 478 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 478 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 478 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / Build & unit tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 478 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'

Check warning on line 478 in SysManager/SysManager/ViewModels/NetworkSharedState.cs

View workflow job for this annotation

GitHub Actions / UI automation tests

'SkiaPaint.FontFamily' is obsolete: 'Use the SKTypeface property and assign it to SKTypeface.FromFamilyName(fontFamily, fontStyle)'
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("2A3244").WithAlpha(80))
};

public void Dispose()
{
Pinger.SampleReceived -= OnSample;
TraceMonitor.RouteCompleted -= OnRouteCompleted;
Pinger.Stop();
Pinger.Dispose();
TraceMonitor.Stop();
TraceMonitor.Dispose();
FlushTimer?.Stop();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private async Task ScanAsync()
}
}

[RelayCommand]
[RelayCommand(CanExecute = nameof(CanToggle))]
private async Task ToggleFeatureAsync(WindowsFeature? feature)
Comment on lines +78 to 79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an in-method re-entrancy guard in ToggleFeatureAsync.

CanExecute helps, but ToggleFeatureAsync can still be invoked while busy (e.g., stale command state or non-UI invocation). Add a hard guard at method entry.

Proposed fix
 private async Task ToggleFeatureAsync(WindowsFeature? feature)
 {
-    if (feature == null) return;
+    if (IsBusy || feature == null) return;

Also applies to: 81-82, 155-155

🤖 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/WindowsFeaturesViewModel.cs` around lines 78
- 79, ToggleFeatureAsync can be entered re-entrantly despite CanExecute; add an
in-method guard by introducing a private bool (e.g., _isToggling) or an
Interlocked-based flag and check/atomically set it at the start of
ToggleFeatureAsync (return immediately if already set), then clear it in a
finally block so the flag is always reset after await/exception; apply the same
pattern to the other async RelayCommand handlers in this ViewModel to prevent
concurrent invocation.

{
if (feature == null) return;
Expand Down Expand Up @@ -152,6 +152,8 @@ private async Task ToggleFeatureAsync(WindowsFeature? feature)
[RelayCommand]
private void Cancel() => _cts?.Cancel();

private bool CanToggle(WindowsFeature? _) => !IsBusy;

protected override void Dispose(bool disposing)
{
if (disposing) _cts?.Dispose();
Expand Down
10 changes: 6 additions & 4 deletions SysManager/SysManager/Views/ConsoleView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ public ConsoleView()
System.Windows.FrameworkElement.RequestBringIntoViewEvent,
new System.Windows.RequestBringIntoViewEventHandler((s, e) => e.Handled = true));

DataContextChanged += (_, __) =>
DataContextChanged += (_, args) =>
{
if (DataContext is ConsoleViewModel vm)
{
// MEM-003: unsubscribe from previous DataContext to prevent leak
if (args.OldValue is ConsoleViewModel oldVm)
oldVm.Lines.CollectionChanged -= OnLinesChanged;

if (args.NewValue is ConsoleViewModel vm)
vm.Lines.CollectionChanged += OnLinesChanged;
}
};
Comment on lines +23 to 31

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Also detach from current DataContext on view unload.

This fixes DataContext swaps, but not the case where the control is unloaded without a DataContext change. Add unload-time unsubscription to fully close the leak path.

Proposed fix
 public ConsoleView()
 {
     InitializeComponent();
+    Unloaded += OnUnloaded;
@@
         };
     }
+
+    private void OnUnloaded(object sender, System.Windows.RoutedEventArgs e)
+    {
+        if (DataContext is ConsoleViewModel vm)
+            vm.Lines.CollectionChanged -= OnLinesChanged;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DataContextChanged += (_, args) =>
{
if (DataContext is ConsoleViewModel vm)
{
// MEM-003: unsubscribe from previous DataContext to prevent leak
if (args.OldValue is ConsoleViewModel oldVm)
oldVm.Lines.CollectionChanged -= OnLinesChanged;
if (args.NewValue is ConsoleViewModel vm)
vm.Lines.CollectionChanged += OnLinesChanged;
}
};
public ConsoleView()
{
InitializeComponent();
Unloaded += OnUnloaded;
DataContextChanged += (_, args) =>
{
// MEM-003: unsubscribe from previous DataContext to prevent leak
if (args.OldValue is ConsoleViewModel oldVm)
oldVm.Lines.CollectionChanged -= OnLinesChanged;
if (args.NewValue is ConsoleViewModel vm)
vm.Lines.CollectionChanged += OnLinesChanged;
};
}
private void OnUnloaded(object sender, System.Windows.RoutedEventArgs e)
{
if (DataContext is ConsoleViewModel vm)
vm.Lines.CollectionChanged -= OnLinesChanged;
}
🤖 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/Views/ConsoleView.xaml.cs` around lines 23 - 31, The
DataContextChanged handler currently unsubscribes old ConsoleViewModel and
subscribes new one but misses detaching when the view is unloaded; add an
Unloaded event handler on the view that checks if DataContext is a
ConsoleViewModel and calls vm.Lines.CollectionChanged -= OnLinesChanged (and
optionally detaches the Unloaded handler itself) to ensure the OnLinesChanged
subscription is removed when the control is unloaded as well; reference the
existing DataContextChanged lambda, the ConsoleViewModel type, and the
OnLinesChanged method to locate where to wire the Unloaded cleanup.

}

Expand Down
9 changes: 8 additions & 1 deletion SysManager/SysManager/Views/PerformanceView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace SysManager.Views;

public partial class PerformanceView : UserControl
{
private System.ComponentModel.PropertyChangedEventHandler? _propertyHandler;

public PerformanceView()
{
InitializeComponent();
Expand All @@ -18,13 +20,18 @@ public PerformanceView()

private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// MEM-004: unsubscribe from previous VM to prevent leak
if (e.OldValue is PerformanceViewModel oldVm && _propertyHandler != null)
oldVm.PropertyChanged -= _propertyHandler;

if (e.NewValue is PerformanceViewModel vm)
{
vm.PropertyChanged += (_, args) =>
_propertyHandler = (_, args) =>
{
if (args.PropertyName == nameof(PerformanceViewModel.SelectedPlan))
SyncRadioButtons(vm.SelectedPlan);
};
vm.PropertyChanged += _propertyHandler;
SyncRadioButtons(vm.SelectedPlan);
}
}
Comment on lines 21 to 37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unsubscribe PropertyChanged on control unload as well.

Current logic handles DataContext swaps, but if the view unloads with the same VM, the VM can still retain the view via the handler.

Proposed fix
 public PerformanceView()
 {
     InitializeComponent();
     DataContextChanged += OnDataContextChanged;
+    Unloaded += OnUnloaded;
 }
@@
     }
+
+    private void OnUnloaded(object sender, RoutedEventArgs e)
+    {
+        if (DataContext is PerformanceViewModel vm && _propertyHandler != null)
+            vm.PropertyChanged -= _propertyHandler;
+    }
🤖 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/Views/PerformanceView.xaml.cs` around lines 21 - 37,
The view currently unsubscribes in OnDataContextChanged but never detaches the
handler when the control unloads, so attach an Unloaded (and optionally Loaded)
event in the view constructor and in the Unloaded handler remove the
VM.PropertyChanged subscription and null out _propertyHandler; specifically,
ensure you unsubscribe any existing handler referenced by _propertyHandler from
the current PerformanceViewModel instance (matching how OnDataContextChanged
checks e.OldValue) in the control's Unloaded event, and also consider
re-subscribing on Loaded or re-checking DataContext to avoid leaving the VM
holding a reference via PropertyChanged.

Expand Down
Loading