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
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture

SysManager is a tabbed WPF desktop app on .NET 9, written in C# 13. It follows
SysManager is a tabbed WPF desktop app on .NET 10, written in C# 14. It follows
a standard MVVM layout with a thin service layer that wraps Windows APIs,
PowerShell, and external CLIs (winget, Ookla `speedtest`). It's gamer-focused:
network presets for CS2 / PUBG / Streaming and safe cleanup for Steam / Epic
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ follows, and what to expect when you open a pull request.
**Prerequisites**

- Windows 10 or later (WPF + Windows APIs won't build elsewhere).
- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0).
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0).
- Git 2.40+.
- A Windows account with admin rights (needed to test elevated features;
the app itself runs fine unelevated for everything else).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Event Log viewer — all in one WPF desktop app.
[![Downloads](https://img.shields.io/github/downloads/laurentiu021/SystemManager/total)](https://github.com/laurentiu021/SystemManager/releases)
[![Issues](https://img.shields.io/github/issues/laurentiu021/SystemManager)](https://github.com/laurentiu021/SystemManager/issues)
![Platform](https://img.shields.io/badge/platform-Windows%2010%2B-blue)
![.NET](https://img.shields.io/badge/.NET-9.0-512BD4)
![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)

---
Expand Down
5 changes: 2 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ older build, the first step is usually to update.

| Version | Supported |
| ------- | ------------------ |
| 0.49.x | :white_check_mark: |
| 0.48.x | :x: |
| < 0.48 | :x: |
| 1.0.x | :white_check_mark: |
| < 1.0 | :x: |

## Reporting a vulnerability

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

using System.Collections.Specialized;
using SysManager.Helpers;

namespace SysManager.Tests;

public class BulkObservableCollectionTests
{
[Fact]
public void ReplaceWith_EmptyCollection_ClearsAll()
{
var collection = new BulkObservableCollection<int>();
collection.Add(1);
collection.Add(2);
collection.Add(3);

collection.ReplaceWith(Array.Empty<int>());

Assert.Empty(collection);
}

[Fact]
public void ReplaceWith_PopulatesWithNewItems()
{
var collection = new BulkObservableCollection<string>();
collection.Add("old");

collection.ReplaceWith(new[] { "alpha", "beta", "gamma" });

Assert.Equal(3, collection.Count);
Assert.Equal("alpha", collection[0]);
Assert.Equal("beta", collection[1]);
Assert.Equal("gamma", collection[2]);
}

[StaFact]
public void ReplaceWith_FiresSingleResetNotification()
{
var collection = new BulkObservableCollection<int>();
collection.Add(1);
collection.Add(2);

var resetEvents = new List<NotifyCollectionChangedEventArgs>();
collection.CollectionChanged += (_, e) => resetEvents.Add(e);

collection.ReplaceWith(new[] { 10, 20, 30 });

var resets = resetEvents.Where(e => e.Action == NotifyCollectionChangedAction.Reset).ToList();
Assert.Single(resets);
}

[StaFact]
public void ReplaceWith_SuppressesIndividualNotifications()
{
var collection = new BulkObservableCollection<int>();
collection.Add(1);
collection.Add(2);

var events = new List<NotifyCollectionChangedEventArgs>();
collection.CollectionChanged += (_, e) => events.Add(e);

collection.ReplaceWith(new[] { 10, 20, 30 });

// Should not have any Add or Remove events — only the final Reset
Assert.DoesNotContain(events, e => e.Action == NotifyCollectionChangedAction.Add);
Assert.DoesNotContain(events, e => e.Action == NotifyCollectionChangedAction.Remove);
}

[Fact]
public void ReplaceWith_NullItems_ThrowsArgumentNullException()
{
var collection = new BulkObservableCollection<int>();

Assert.Throws<ArgumentNullException>(() => collection.ReplaceWith(null!));
}

[Fact]
public void ReplaceWith_EmptyEnumerable_ResultsInEmptyCollection()
{
var collection = new BulkObservableCollection<string>();
collection.Add("existing");

collection.ReplaceWith(Enumerable.Empty<string>());

Assert.Empty(collection);
}
}
1 change: 1 addition & 0 deletions SysManager/SysManager.Tests/OperationLockServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace SysManager.Tests;

[Collection("OperationLock")]
public class OperationLockServiceTests
{
private OperationLockService Sut => OperationLockService.Instance;
Expand Down
7 changes: 7 additions & 0 deletions SysManager/SysManager.Tests/TestCollections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ namespace SysManager.Tests;
/// </summary>
[CollectionDefinition("Network", DisableParallelization = true)]
public class NetworkCollection { }

/// <summary>
/// Groups tests that use the shared OperationLockService singleton so they
/// run sequentially and avoid cross-test lock contention.
/// </summary>
[CollectionDefinition("OperationLock", DisableParallelization = true)]
public class OperationLockCollection { }
113 changes: 113 additions & 0 deletions SysManager/SysManager.Tests/WingetTableParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SysManager · WingetTableParserTests
// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
// License: MIT

using System.Text.RegularExpressions;
using SysManager.Helpers;

namespace SysManager.Tests;

public class WingetTableParserTests
{
private static readonly Regex HeaderPattern = new(@"^\s*Name\s+Id\s+Version", RegexOptions.IgnoreCase);
private static readonly Regex SummaryPattern = new(@"^\d+\s+packages?\s+", RegexOptions.IgnoreCase);

[Fact]
public void Parse_EmptyLines_ReturnsEmpty()
{
var lines = new List<string>();
var result = WingetTableParser.Parse(lines, HeaderPattern, SummaryPattern);
Assert.Empty(result);
}

[Fact]
public void Parse_NoHeader_ReturnsEmpty()
{
var lines = new List<string>
{
"Some random output",
"No applicable upgrades found.",
"Another line without headers"
};
var result = WingetTableParser.Parse(lines, HeaderPattern, SummaryPattern);
Assert.Empty(result);
}

[Fact]
public void Parse_HeaderOnly_ReturnsEmpty()
{
var lines = new List<string>
{
"Name Id Version Available Source",
"-----------------------------------------------------------------------------------------"
};
var result = WingetTableParser.Parse(lines, HeaderPattern, SummaryPattern);
Assert.Empty(result);
}

[Fact]
public void Parse_ValidTable_ParsesCorrectly()
{
var lines = new List<string>
{
"Name Id Version Available Source",
"-----------------------------------------------------------------------------------------",
"Git Git.Git 2.47.0 2.48.0 winget",
"PowerShell Microsoft.PowerShell 7.4.3.0 7.4.4.0 winget",
"Node.js OpenJS.NodeJS 20.11.0 22.1.0 winget"
};

var result = WingetTableParser.Parse(lines, HeaderPattern, SummaryPattern);

Assert.Equal(3, result.Count);

Assert.Equal("Git", result[0].Name);
Assert.Equal("Git.Git", result[0].Id);
Assert.Equal("2.47.0", result[0].Version);
Assert.Equal("2.48.0", result[0].Available);
Assert.Equal("winget", result[0].Source);

Assert.Equal("PowerShell", result[1].Name);
Assert.Equal("Microsoft.PowerShell", result[1].Id);

Assert.Equal("Node.js", result[2].Name);
Assert.Equal("OpenJS.NodeJS", result[2].Id);
}

[Fact]
public void Parse_ShortLines_SkipsGracefully()
{
var lines = new List<string>
{
"Name Id Version Available Source",
"-----------------------------------------------------------------------------------------",
"ab",
"x",
"Git Git.Git 2.47.0 2.48.0 winget"
};

var result = WingetTableParser.Parse(lines, HeaderPattern, SummaryPattern);

Assert.Single(result);
Assert.Equal("Git.Git", result[0].Id);
}

[Fact]
public void Parse_StopsAtSummaryLine()
{
var lines = new List<string>
{
"Name Id Version Available Source",
"-----------------------------------------------------------------------------------------",
"Git Git.Git 2.47.0 2.48.0 winget",
"3 packages have version numbers that cannot be determined.",
"PowerShell Microsoft.PowerShell 7.4.3.0 7.4.4.0 winget"
};

var result = WingetTableParser.Parse(lines, HeaderPattern, SummaryPattern);

// Should stop at the summary line and not include PowerShell
Assert.Single(result);
Assert.Equal("Git.Git", result[0].Id);
}
}
8 changes: 4 additions & 4 deletions SysManager/SysManager/Services/AppAlertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,14 @@ public static IReadOnlyList<AppInstallEntry> GetRegistryApps()
try
{
using var key = Registry.LocalMachine.OpenSubKey(path);
if (key == null) continue;
if (key is null) continue;

foreach (var subKeyName in key.GetSubKeyNames())
{
try
{
using var sub = key.OpenSubKey(subKeyName);
if (sub == null) continue;
if (sub is null) continue;

var name = sub.GetValue("DisplayName") as string;
if (string.IsNullOrWhiteSpace(name)) continue;
Expand Down Expand Up @@ -201,15 +201,15 @@ private void CheckRegistry(object? state)
/// </summary>
private void RaiseNewAppDetected(AppInstallEntry entry)
{
if (_syncContext != null)
if (_syncContext is not null)
_syncContext.Post(_ => NewAppDetected?.Invoke(entry), null);
else
NewAppDetected?.Invoke(entry);
}

private static List<string> GetMonitoredDirectories()
{
var dirs = new List<string>();
List<string> dirs = [];

var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var pfx86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
Expand Down
20 changes: 10 additions & 10 deletions SysManager/SysManager/Services/AppBlockerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public static bool BlockApp(string exeName)
try
{
using var ifeo = Registry.LocalMachine.OpenSubKey(IfeoPath, writable: true);
if (ifeo == null) return false;
if (ifeo is null) return false;

using var appKey = ifeo.CreateSubKey(exeName, writable: true);
appKey.SetValue("Debugger", BlockerDebugger, RegistryValueKind.String);
Expand Down Expand Up @@ -87,13 +87,13 @@ public static bool UnblockApp(string exeName)
try
{
using var ifeo = Registry.LocalMachine.OpenSubKey(IfeoPath, writable: true);
if (ifeo == null) return false;
if (ifeo is null) return false;

using var appKey = ifeo.OpenSubKey(exeName, writable: true);
if (appKey == null) return true;
if (appKey is null) return true;

var debugger = appKey.GetValue("Debugger") as string;
if (debugger != null && debugger.Equals(BlockerDebugger, StringComparison.OrdinalIgnoreCase))
if (debugger is not null && debugger.Equals(BlockerDebugger, StringComparison.OrdinalIgnoreCase))
{
appKey.DeleteValue("Debugger", throwOnMissingValue: false);

Expand Down Expand Up @@ -137,13 +137,13 @@ public static bool IsBlocked(string exeName)
try
{
using var ifeo = Registry.LocalMachine.OpenSubKey(IfeoPath);
if (ifeo == null) return false;
if (ifeo is null) return false;

using var appKey = ifeo.OpenSubKey(exeName);
if (appKey == null) return false;
if (appKey is null) return false;

var debugger = appKey.GetValue("Debugger") as string;
return debugger != null && debugger.Equals(BlockerDebugger, StringComparison.OrdinalIgnoreCase);
return debugger is not null && debugger.Equals(BlockerDebugger, StringComparison.OrdinalIgnoreCase);
}
catch (IOException) { return false; }
catch (UnauthorizedAccessException) { return false; }
Expand All @@ -160,17 +160,17 @@ public static IReadOnlyList<BlockedApp> GetBlockedApps()
try
{
using var ifeo = Registry.LocalMachine.OpenSubKey(IfeoPath);
if (ifeo == null) return blocked;
if (ifeo is null) return blocked;

foreach (var subKeyName in ifeo.GetSubKeyNames())
{
try
{
using var appKey = ifeo.OpenSubKey(subKeyName);
if (appKey == null) continue;
if (appKey is null) continue;

var debugger = appKey.GetValue("Debugger") as string;
if (debugger != null && debugger.Equals(BlockerDebugger, StringComparison.OrdinalIgnoreCase))
if (debugger is not null && debugger.Equals(BlockerDebugger, StringComparison.OrdinalIgnoreCase))
{
blocked.Add(new BlockedApp
{
Expand Down
8 changes: 4 additions & 4 deletions SysManager/SysManager/Services/DeepCleanupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ private static string[] SteamRoots(string pfx86, string pf)

private static string[] SteamCacheDirs(string pfx86, string pf, string localAppData)
{
var result = new List<string>();
List<string> result = [];
foreach (var root in SteamRoots(pfx86, pf))
{
result.Add(Path.Combine(root, "appcache"));
Expand All @@ -280,7 +280,7 @@ private static string[] SteamCacheDirs(string pfx86, string pf, string localAppD

private static string[] SteamShaderCacheDirs(string pfx86, string pf)
{
var result = new List<string>();
List<string> result = [];
foreach (var root in SteamRoots(pfx86, pf))
result.Add(Path.Combine(root, "steamapps", "shadercache"));
foreach (var drive in DriveInfo.GetDrives().Where(d => d.DriveType == DriveType.Fixed && d.IsReady))
Expand Down Expand Up @@ -316,7 +316,7 @@ private static string[] RiotLogDirs(string localAppData, string pfx86, string pf
private static CleanupResult Clean(IReadOnlyList<CleanupCategory> categories, IProgress<ScanProgress>? progress, CancellationToken ct)
{
long freed = 0;
var errors = new List<string>();
List<string> errors = [];
var filesDeleted = 0;
var selected = categories.Where(c => c.IsSelected).ToList();
var total = selected.Count;
Expand Down Expand Up @@ -401,7 +401,7 @@ private static IEnumerable<string> EnumerateFiles(string root, CancellationToken

private static IEnumerable<string> EnumerateDirectoriesDepthFirst(string root, CancellationToken ct)
{
var all = new List<string>();
List<string> all = [];
var stack = new Stack<string>();
stack.Push(root);
while (stack.Count > 0 && !ct.IsCancellationRequested)
Expand Down
Loading
Loading