diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 86f423a3..c7f3ab22 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e4f8399..bbb27282 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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). diff --git a/README.md b/README.md index 0f5b06f9..b4dc16ad 100644 --- a/README.md +++ b/README.md @@ -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) --- diff --git a/SECURITY.md b/SECURITY.md index 308ceed8..dcbc6675 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 diff --git a/SysManager/SysManager.Tests/BulkObservableCollectionTests.cs b/SysManager/SysManager.Tests/BulkObservableCollectionTests.cs new file mode 100644 index 00000000..6600a6bd --- /dev/null +++ b/SysManager/SysManager.Tests/BulkObservableCollectionTests.cs @@ -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(); + collection.Add(1); + collection.Add(2); + collection.Add(3); + + collection.ReplaceWith(Array.Empty()); + + Assert.Empty(collection); + } + + [Fact] + public void ReplaceWith_PopulatesWithNewItems() + { + var collection = new BulkObservableCollection(); + 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(); + collection.Add(1); + collection.Add(2); + + var resetEvents = new List(); + 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(); + collection.Add(1); + collection.Add(2); + + var events = new List(); + 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(); + + Assert.Throws(() => collection.ReplaceWith(null!)); + } + + [Fact] + public void ReplaceWith_EmptyEnumerable_ResultsInEmptyCollection() + { + var collection = new BulkObservableCollection(); + collection.Add("existing"); + + collection.ReplaceWith(Enumerable.Empty()); + + Assert.Empty(collection); + } +} diff --git a/SysManager/SysManager.Tests/OperationLockServiceTests.cs b/SysManager/SysManager.Tests/OperationLockServiceTests.cs index e11f4db8..b71c0923 100644 --- a/SysManager/SysManager.Tests/OperationLockServiceTests.cs +++ b/SysManager/SysManager.Tests/OperationLockServiceTests.cs @@ -7,6 +7,7 @@ namespace SysManager.Tests; +[Collection("OperationLock")] public class OperationLockServiceTests { private OperationLockService Sut => OperationLockService.Instance; diff --git a/SysManager/SysManager.Tests/TestCollections.cs b/SysManager/SysManager.Tests/TestCollections.cs index e7bbcb2d..f41872aa 100644 --- a/SysManager/SysManager.Tests/TestCollections.cs +++ b/SysManager/SysManager.Tests/TestCollections.cs @@ -10,3 +10,10 @@ namespace SysManager.Tests; /// [CollectionDefinition("Network", DisableParallelization = true)] public class NetworkCollection { } + +/// +/// Groups tests that use the shared OperationLockService singleton so they +/// run sequentially and avoid cross-test lock contention. +/// +[CollectionDefinition("OperationLock", DisableParallelization = true)] +public class OperationLockCollection { } diff --git a/SysManager/SysManager.Tests/WingetTableParserTests.cs b/SysManager/SysManager.Tests/WingetTableParserTests.cs new file mode 100644 index 00000000..aa6b176f --- /dev/null +++ b/SysManager/SysManager.Tests/WingetTableParserTests.cs @@ -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(); + var result = WingetTableParser.Parse(lines, HeaderPattern, SummaryPattern); + Assert.Empty(result); + } + + [Fact] + public void Parse_NoHeader_ReturnsEmpty() + { + var lines = new List + { + "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 + { + "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 + { + "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 + { + "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 + { + "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); + } +} diff --git a/SysManager/SysManager/Services/AppAlertService.cs b/SysManager/SysManager/Services/AppAlertService.cs index 73aed35d..67dcfc2e 100644 --- a/SysManager/SysManager/Services/AppAlertService.cs +++ b/SysManager/SysManager/Services/AppAlertService.cs @@ -127,14 +127,14 @@ public static IReadOnlyList 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; @@ -201,7 +201,7 @@ private void CheckRegistry(object? state) /// private void RaiseNewAppDetected(AppInstallEntry entry) { - if (_syncContext != null) + if (_syncContext is not null) _syncContext.Post(_ => NewAppDetected?.Invoke(entry), null); else NewAppDetected?.Invoke(entry); @@ -209,7 +209,7 @@ private void RaiseNewAppDetected(AppInstallEntry entry) private static List GetMonitoredDirectories() { - var dirs = new List(); + List dirs = []; var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); var pfx86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); diff --git a/SysManager/SysManager/Services/AppBlockerService.cs b/SysManager/SysManager/Services/AppBlockerService.cs index d839f87d..50ab0494 100644 --- a/SysManager/SysManager/Services/AppBlockerService.cs +++ b/SysManager/SysManager/Services/AppBlockerService.cs @@ -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); @@ -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); @@ -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; } @@ -160,17 +160,17 @@ public static IReadOnlyList 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 { diff --git a/SysManager/SysManager/Services/DeepCleanupService.cs b/SysManager/SysManager/Services/DeepCleanupService.cs index e2a107a9..93e4e066 100644 --- a/SysManager/SysManager/Services/DeepCleanupService.cs +++ b/SysManager/SysManager/Services/DeepCleanupService.cs @@ -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(); + List result = []; foreach (var root in SteamRoots(pfx86, pf)) { result.Add(Path.Combine(root, "appcache")); @@ -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(); + List 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)) @@ -316,7 +316,7 @@ private static string[] RiotLogDirs(string localAppData, string pfx86, string pf private static CleanupResult Clean(IReadOnlyList categories, IProgress? progress, CancellationToken ct) { long freed = 0; - var errors = new List(); + List errors = []; var filesDeleted = 0; var selected = categories.Where(c => c.IsSelected).ToList(); var total = selected.Count; @@ -401,7 +401,7 @@ private static IEnumerable EnumerateFiles(string root, CancellationToken private static IEnumerable EnumerateDirectoriesDepthFirst(string root, CancellationToken ct) { - var all = new List(); + List all = []; var stack = new Stack(); stack.Push(root); while (stack.Count > 0 && !ct.IsCancellationRequested) diff --git a/SysManager/SysManager/Services/DiskHealthService.cs b/SysManager/SysManager/Services/DiskHealthService.cs index 440c9c60..710bdc21 100644 --- a/SysManager/SysManager/Services/DiskHealthService.cs +++ b/SysManager/SysManager/Services/DiskHealthService.cs @@ -146,7 +146,7 @@ private static void ApplyVerdict(DiskHealthReport r) } // All good - var bits = new List(); + List bits = []; if (r.TemperatureC.HasValue) bits.Add($"{r.TemperatureC:F0} °C"); if (r.WearPercent.HasValue) bits.Add($"wear {r.WearPercent}%"); if (r.PowerOnHours.HasValue) bits.Add($"{r.PowerOnHours} h on"); @@ -160,7 +160,7 @@ private static void ApplyVerdict(DiskHealthReport r) private static double? ToDouble(object? o) { - if (o == null) return null; + if (o is null) return null; try { var v = Convert.ToDouble(o); return Math.Abs(v) < 1e-9 ? null : v; } catch (FormatException) { return null; } catch (OverflowException) { return null; } @@ -169,7 +169,7 @@ private static void ApplyVerdict(DiskHealthReport r) private static int? ToInt(object? o) { - if (o == null) return null; + if (o is null) return null; try { return Convert.ToInt32(o); } catch (FormatException) { return null; } catch (OverflowException) { return null; } @@ -178,7 +178,7 @@ private static void ApplyVerdict(DiskHealthReport r) private static long? ToLong(object? o) { - if (o == null) return null; + if (o is null) return null; try { var v = Convert.ToInt64(o); return v == 0 ? null : v; } catch (FormatException) { return null; } catch (OverflowException) { return null; } diff --git a/SysManager/SysManager/Services/EventLogService.cs b/SysManager/SysManager/Services/EventLogService.cs index 189e34bb..d5d644d4 100644 --- a/SysManager/SysManager/Services/EventLogService.cs +++ b/SysManager/SysManager/Services/EventLogService.cs @@ -48,7 +48,7 @@ private async IAsyncEnumerable ReadInternal( EventRecord? rec = null; try { rec = reader.ReadEvent(); } catch (EventLogException) { continue; } - if (rec == null) yield break; + if (rec is null) yield break; FriendlyEventEntry? entry = null; try { entry = Project(rec, opt.LogName); } @@ -56,7 +56,7 @@ private async IAsyncEnumerable ReadInternal( catch (InvalidOperationException) { /* skip malformed record */ } finally { rec.Dispose(); } - if (entry == null) continue; + if (entry is null) continue; EventExplainer.Enrich(entry); emitted++; @@ -133,7 +133,7 @@ private static string FirstLine(string s) /// private static string BuildXPath(EventLogQueryOptions opt) { - var clauses = new List(); + List clauses = []; if (opt.Severities is { Count: > 0 }) { diff --git a/SysManager/SysManager/Services/HealthScoreService.cs b/SysManager/SysManager/Services/HealthScoreService.cs index 4bc6a2c2..eee62924 100644 --- a/SysManager/SysManager/Services/HealthScoreService.cs +++ b/SysManager/SysManager/Services/HealthScoreService.cs @@ -100,7 +100,7 @@ public async Task ComputeAsync(CancellationToken ct = default internal static int ComputeDiskScore(IReadOnlyList? disks) { - if (disks == null || disks.Count == 0) return 100; + if (disks is null || disks.Count == 0) return 100; // Use the worst disk's health percentage, or map status string int worstScore = disks.Select(d => d.HealthPercent ?? d.HealthStatus switch @@ -115,7 +115,7 @@ internal static int ComputeDiskScore(IReadOnlyList? disks) internal static int ComputeRamScore(SystemSnapshot? snapshot) { - if (snapshot == null) return 100; + if (snapshot is null) return 100; double usedPct = snapshot.Memory.UsedPercent; // Linear scale: 0% used = 100 score, 100% used = 0 score @@ -134,7 +134,7 @@ internal static int ComputeRamScore(SystemSnapshot? snapshot) internal static int ComputeUptimeScore(SystemSnapshot? snapshot) { - if (snapshot == null) return 100; + if (snapshot is null) return 100; double days = snapshot.Os.Uptime.TotalDays; return days switch @@ -150,7 +150,7 @@ internal static int ComputeUptimeScore(SystemSnapshot? snapshot) internal static int ComputeBatteryScore(BatteryInfo? battery) { - if (battery == null || !battery.HasBattery) return 100; + if (battery is null || !battery.HasBattery) return 100; double health = battery.HealthPercent; // -1 means capacity data unavailable (no admin for root\WMI). @@ -174,10 +174,10 @@ private static List BuildRecommendations( bool hasBattery, SystemSnapshot? snapshot, IReadOnlyList? disks, BatteryInfo? battery) { - var recs = new List(); + List recs = []; // Uptime - if (uptimeScore <= 70 && snapshot != null) + if (uptimeScore <= 70 && snapshot is not null) { int days = (int)snapshot.Os.Uptime.TotalDays; recs.Add(new HealthRecommendation @@ -188,7 +188,7 @@ private static List BuildRecommendations( } // Disk - if (diskScore < 80 && disks != null) + if (diskScore < 80 && disks is not null) { var worst = disks.OrderBy(d => d.HealthPercent ?? 100).FirstOrDefault(); string diskName = worst?.FriendlyName ?? "Disk"; @@ -200,7 +200,7 @@ private static List BuildRecommendations( } // RAM - if (ramScore < 75 && snapshot != null) + if (ramScore < 75 && snapshot is not null) { recs.Add(new HealthRecommendation { @@ -210,7 +210,7 @@ private static List BuildRecommendations( } // Battery - if (hasBattery && batteryScore < 80 && battery != null) + if (hasBattery && batteryScore < 80 && battery is not null) { recs.Add(new HealthRecommendation { diff --git a/SysManager/SysManager/Services/IconExtractorService.cs b/SysManager/SysManager/Services/IconExtractorService.cs index 8556ef38..9812d66b 100644 --- a/SysManager/SysManager/Services/IconExtractorService.cs +++ b/SysManager/SysManager/Services/IconExtractorService.cs @@ -76,7 +76,7 @@ public sealed partial class IconExtractorService if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath)) { var icon = GetIcon(filePath); - if (icon != _appFallback.Value && icon != null) + if (icon != _appFallback.Value && icon is not null) return icon; } @@ -84,7 +84,7 @@ public sealed partial class IconExtractorService if (!string.IsNullOrWhiteSpace(processName)) { var found = FindExecutableByName(processName); - if (found != null) + if (found is not null) return GetIcon(found); // Known Windows system processes → Windows icon @@ -117,7 +117,7 @@ public sealed partial class IconExtractorService if (!string.IsNullOrWhiteSpace(displayIconPath)) { var icon = GetIcon(displayIconPath); - if (icon != _appFallback.Value && icon != null) + if (icon != _appFallback.Value && icon is not null) return icon; } @@ -132,7 +132,7 @@ public sealed partial class IconExtractorService var match = exes.FirstOrDefault(e => Path.GetFileNameWithoutExtension(e) .Contains(appName.Split(' ')[0], StringComparison.OrdinalIgnoreCase)); - if (match != null) + if (match is not null) return GetIcon(match); } if (exes.Length > 0) @@ -271,7 +271,7 @@ internal static string NormalizePath(string raw) .Where(idx => idx > 0) .Select(idx => clean[..idx].Trim('"', ' ')) .FirstOrDefault(File.Exists); - if (separatorMatch != null) return separatorMatch; + if (separatorMatch is not null) return separatorMatch; // Progressive space-split var parts = clean.Split(' '); @@ -292,7 +292,7 @@ internal static string NormalizePath(string raw) if (!string.IsNullOrEmpty(exeName)) { var found = SearchInPath(exeName); - if (found != null) return found; + if (found is not null) return found; } return clean; @@ -373,7 +373,7 @@ internal static string NormalizePath(string raw) private static string? ResolveExecutablePath(string exeName) { var found = SearchInPath(exeName); - if (found != null) return found; + if (found is not null) return found; // Search one level deep in Program Files var programDirs = new[] @@ -390,7 +390,7 @@ internal static string NormalizePath(string raw) var match = Directory.GetDirectories(baseDir) .Select(subDir => Path.Join(subDir, exeName)) .FirstOrDefault(File.Exists); - if (match != null) return match; + if (match is not null) return match; } catch (UnauthorizedAccessException ex) { Log.Debug(ex, "Access denied scanning {Dir} for {Exe}", baseDir, exeName); } catch (IOException ex) { Log.Debug(ex, "I/O error scanning {Dir} for {Exe}", baseDir, exeName); } diff --git a/SysManager/SysManager/Services/MemoryTestService.cs b/SysManager/SysManager/Services/MemoryTestService.cs index bce6672d..e0404caa 100644 --- a/SysManager/SysManager/Services/MemoryTestService.cs +++ b/SysManager/SysManager/Services/MemoryTestService.cs @@ -58,7 +58,7 @@ public async Task CheckErrorLogsAsync(CancellationToken ct = { diagCount++; } - if (rec.TimeCreated.HasValue && (lastError == null || rec.TimeCreated.Value > lastError)) + if (rec.TimeCreated.HasValue && (lastError is null || rec.TimeCreated.Value > lastError)) lastError = rec.TimeCreated.Value; } } diff --git a/SysManager/SysManager/Services/NetworkRepairService.cs b/SysManager/SysManager/Services/NetworkRepairService.cs index 8ab96c77..f62e28e8 100644 --- a/SysManager/SysManager/Services/NetworkRepairService.cs +++ b/SysManager/SysManager/Services/NetworkRepairService.cs @@ -10,13 +10,16 @@ namespace SysManager.Services; /// Runs common network repair commands: DNS flush, Winsock reset, TCP/IP reset. /// Each method captures stdout/stderr and returns a . /// -public sealed class NetworkRepairService +public sealed class NetworkRepairService : IDisposable { private readonly PowerShellRunner _ps; private readonly SemaphoreSlim _gate = new(1, 1); public NetworkRepairService(PowerShellRunner ps) => _ps = ps; + /// + public void Dispose() => _gate.Dispose(); + /// /// Flush the DNS resolver cache. Does not require a reboot. /// diff --git a/SysManager/SysManager/Services/PerformanceService.cs b/SysManager/SysManager/Services/PerformanceService.cs index e679d0a6..a612c242 100644 --- a/SysManager/SysManager/Services/PerformanceService.cs +++ b/SysManager/SysManager/Services/PerformanceService.cs @@ -90,7 +90,7 @@ public async Task TakeSnapshotAsync(CancellationToken ct = def GameModeEnabled: ReadGameMode(), XboxGameBarEnabled: ReadXboxGameBarEnabled(), XboxGameDvrEnabled: ReadXboxGameDvrEnabled(), - GpuDynamicPstate: nvidiaKey != null && !ReadGpuMaxPerformance(nvidiaKey), + GpuDynamicPstate: nvidiaKey is not null && !ReadGpuMaxPerformance(nvidiaKey), ProcessorMinPercentAc: await ReadProcessorMinPercentAsync(ct).ConfigureAwait(false), NvidiaSubKey: nvidiaKey); } @@ -144,8 +144,8 @@ public async Task ReadProfileAsync(CancellationToken ct = de profile.XboxGameBarDisabled = !ReadXboxGameBarEnabled() || !ReadXboxGameDvrEnabled(); var nvidiaKey = FindNvidiaSubKey(); - profile.HasNvidiaGpu = nvidiaKey != null; - if (nvidiaKey != null) + profile.HasNvidiaGpu = nvidiaKey is not null; + if (nvidiaKey is not null) { profile.NvidiaGpuName = ReadNvidiaGpuName(nvidiaKey); profile.GpuMaxPerformance = ReadGpuMaxPerformance(nvidiaKey); @@ -166,7 +166,7 @@ public async Task ReadProfileAsync(CancellationToken ct = de public async Task<(string Name, string Guid)> GetActivePlanAsync(CancellationToken ct = default) { await _psGate.WaitAsync(ct).ConfigureAwait(false); - var lines = new List(); + List lines = []; void OnLine(PowerShellLine l) => lines.Add(l.Text); _ps.LineReceived += OnLine; try { await _ps.RunProcessAsync("powercfg.exe", "/getactivescheme", ct, PowerShellRunner.OemEncoding); } @@ -213,7 +213,7 @@ public async Task EnsureUltimatePerformancePlanAsync(CancellationToken c var existingGuid = await FindPlanGuidByNameAsync("Ultimate Performance", ct).ConfigureAwait(false); if (!string.IsNullOrEmpty(existingGuid)) return existingGuid; - var lines = new List(); + List lines = []; void OnLine(PowerShellLine l) => lines.Add(l.Text); await _psGate.WaitAsync(ct).ConfigureAwait(false); _ps.LineReceived += OnLine; @@ -237,7 +237,7 @@ public async Task EnsureUltimatePerformancePlanAsync(CancellationToken c public async Task FindPlanGuidByNameAsync(string nameSubstring, CancellationToken ct = default) { await _psGate.WaitAsync(ct).ConfigureAwait(false); - var lines = new List(); + List lines = []; void OnLine(PowerShellLine l) => lines.Add(l.Text); _ps.LineReceived += OnLine; try { await _ps.RunProcessAsync("powercfg.exe", "/list", ct, PowerShellRunner.OemEncoding); } @@ -292,10 +292,10 @@ internal static bool ReadGameMode() try { using var key = Registry.CurrentUser.OpenSubKey(GameBarKey); - if (key == null) return true; // Windows default = ON + if (key is null) return true; // Windows default = ON var val = key.GetValue("AllowAutoGameMode"); // If key exists but value doesn't, default is ON - if (val == null) return true; + if (val is null) return true; return val is int i && i == 1; } catch (SecurityException) { return true; } @@ -324,9 +324,9 @@ internal static bool ReadXboxGameBarEnabled() try { using var key = Registry.CurrentUser.OpenSubKey(GameDvrKey); - if (key == null) return true; // default = enabled + if (key is null) return true; // default = enabled var val = key.GetValue("AppCaptureEnabled"); - if (val == null) return true; + if (val is null) return true; return val is int i && i == 1; } catch (SecurityException) { return true; } @@ -338,9 +338,9 @@ internal static bool ReadXboxGameDvrEnabled() try { using var key = Registry.CurrentUser.OpenSubKey(GameConfigStoreKey); - if (key == null) return true; + if (key is null) return true; var val = key.GetValue("GameDVR_Enabled"); - if (val == null) return true; + if (val is null) return true; return val is int i && i == 1; } catch (SecurityException) { return true; } @@ -376,14 +376,14 @@ public static void SetXboxGameBar(bool enabled) try { using var classRoot = Registry.LocalMachine.OpenSubKey(GpuClassRoot); - if (classRoot == null) return null; + if (classRoot is null) return null; foreach (var subName in classRoot.GetSubKeyNames().Where(s => int.TryParse(s, out _))) { try { using var sub = classRoot.OpenSubKey(subName); - if (sub == null) continue; + if (sub is null) continue; var driverDesc = sub.GetValue("DriverDesc")?.ToString() ?? ""; var provider = sub.GetValue("ProviderName")?.ToString() ?? ""; @@ -422,7 +422,7 @@ internal static bool ReadGpuMaxPerformance(string subKey) try { using var key = Registry.LocalMachine.OpenSubKey($@"{GpuClassRoot}\{subKey}"); - if (key == null) return false; + if (key is null) return false; var val = key.GetValue("DisableDynamicPstate"); return val is int i && i == 1; } @@ -442,7 +442,7 @@ public static bool SetGpuMaxPerformance(string subKey, bool maxPerformance) { using var key = Registry.LocalMachine.OpenSubKey( $@"{GpuClassRoot}\{subKey}", writable: true); - if (key == null) return false; + if (key is null) return false; if (maxPerformance) key.SetValue("DisableDynamicPstate", 1, RegistryValueKind.DWord); @@ -467,7 +467,7 @@ public static bool SetGpuMaxPerformance(string subKey, bool maxPerformance) internal async Task ReadProcessorMinPercentAsync(CancellationToken ct = default) { await _psGate.WaitAsync(ct).ConfigureAwait(false); - var lines = new List(); + List lines = []; void OnLine(PowerShellLine l) => lines.Add(l.Text); _ps.LineReceived += OnLine; try @@ -613,7 +613,7 @@ public async Task RestoreFromSnapshotAsync(OriginalSnapshot snapshot, Cancellati SetXboxGameBar(snapshot.XboxGameBarEnabled && snapshot.XboxGameDvrEnabled); // GPU - if (snapshot.NvidiaSubKey != null) + if (snapshot.NvidiaSubKey is not null) SetGpuMaxPerformance(snapshot.NvidiaSubKey, !snapshot.GpuDynamicPstate); // Processor state diff --git a/SysManager/SysManager/Services/PingMonitorService.cs b/SysManager/SysManager/Services/PingMonitorService.cs index d2a0c5c3..4402d41c 100644 --- a/SysManager/SysManager/Services/PingMonitorService.cs +++ b/SysManager/SysManager/Services/PingMonitorService.cs @@ -130,7 +130,7 @@ private async Task PingOnceAsync(PingTarget target, CancellationToken ct) private void RaiseSampleReceived(PingSample sample) { var handlers = SampleReceived?.GetInvocationList(); - if (handlers == null) return; + if (handlers is null) return; foreach (var h in handlers) { try { ((Action)h).Invoke(sample); } diff --git a/SysManager/SysManager/Services/PowerShellRunner.cs b/SysManager/SysManager/Services/PowerShellRunner.cs index 4e207cbc..6b43978c 100644 --- a/SysManager/SysManager/Services/PowerShellRunner.cs +++ b/SysManager/SysManager/Services/PowerShellRunner.cs @@ -73,7 +73,7 @@ public async Task> RunAsync( using var ps = PowerShell.Create(); ps.Runspace = runspace; ps.AddScript(script); - if (parameters != null) + if (parameters is not null) { foreach (var kv in parameters) ps.AddParameter(kv.Key, kv.Value); diff --git a/SysManager/SysManager/Services/ProcessDescriptionService.cs b/SysManager/SysManager/Services/ProcessDescriptionService.cs index 4d86b1be..58806233 100644 --- a/SysManager/SysManager/Services/ProcessDescriptionService.cs +++ b/SysManager/SysManager/Services/ProcessDescriptionService.cs @@ -105,14 +105,14 @@ private static Dictionary LoadDatabase() var resourceName = assembly.GetManifestResourceNames() .FirstOrDefault(n => n.EndsWith("ProcessDescriptions.json", StringComparison.OrdinalIgnoreCase)); - if (resourceName == null) + if (resourceName is null) { Log.Warning("ProcessDescriptions.json embedded resource not found"); return db; } using var stream = assembly.GetManifestResourceStream(resourceName); - if (stream == null) + if (stream is null) { Log.Warning("Could not open ProcessDescriptions.json stream"); return db; @@ -123,7 +123,7 @@ private static Dictionary LoadDatabase() PropertyNameCaseInsensitive = true }); - if (entries == null) return db; + if (entries is null) return db; foreach (var e in entries.Where(e => !string.IsNullOrWhiteSpace(e.Name))) { diff --git a/SysManager/SysManager/Services/ShortcutCleanerService.cs b/SysManager/SysManager/Services/ShortcutCleanerService.cs index 0f18784b..49c5ef65 100644 --- a/SysManager/SysManager/Services/ShortcutCleanerService.cs +++ b/SysManager/SysManager/Services/ShortcutCleanerService.cs @@ -103,7 +103,7 @@ public static int DeleteShortcuts(IEnumerable shortcuts, bool to private static List<(string Label, string Path)> GetScanLocations() { - var locations = new List<(string, string)>(); + List<(string, string)> locations = []; var userDesktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); var publicDesktop = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory); diff --git a/SysManager/SysManager/Services/SpeedTestHistoryService.cs b/SysManager/SysManager/Services/SpeedTestHistoryService.cs index 9e66458f..740b7597 100644 --- a/SysManager/SysManager/Services/SpeedTestHistoryService.cs +++ b/SysManager/SysManager/Services/SpeedTestHistoryService.cs @@ -14,7 +14,7 @@ namespace SysManager.Services; /// service degradation over time. Stores up to /// results per engine (HTTP / Ookla), oldest entries are trimmed on save. /// -public sealed class SpeedTestHistoryService +public sealed class SpeedTestHistoryService : IDisposable { public const int MaxPerEngine = 20; @@ -33,6 +33,9 @@ public sealed class SpeedTestHistoryService // acts as an async-compatible mutex. private readonly SemaphoreSlim _fileLock = new(1, 1); + /// + public void Dispose() => _fileLock.Dispose(); + /// /// Loads all saved results from disk. Returns empty list on any error. /// @@ -49,7 +52,7 @@ private async Task> LoadCoreAsync(CancellationToken ct) var json = await File.ReadAllTextAsync(HistoryPath, ct).ConfigureAwait(false); var entries = JsonSerializer.Deserialize>(json, JsonOpts); - if (entries == null) return new List(); + if (entries is null) return new List(); return entries.Select(e => new SpeedTestResult( e.Engine ?? "HTTP", @@ -128,7 +131,7 @@ public async Task ClearAsync(string? engine = null, CancellationToken ct = defau await _fileLock.WaitAsync(ct).ConfigureAwait(false); try { - if (engine == null) + if (engine is null) { if (File.Exists(HistoryPath)) File.Delete(HistoryPath); diff --git a/SysManager/SysManager/Services/SpeedTestService.cs b/SysManager/SysManager/Services/SpeedTestService.cs index a5da90b3..12446ff4 100644 --- a/SysManager/SysManager/Services/SpeedTestService.cs +++ b/SysManager/SysManager/Services/SpeedTestService.cs @@ -57,7 +57,7 @@ private static async Task MeasurePingAsync(string host) try { using var p = new Ping(); - var samples = new List(); + List samples = []; for (int i = 0; i < 4; i++) { var r = await p.SendPingAsync(host, 2000); @@ -388,7 +388,7 @@ await Task.Run(() => #pragma warning disable SYSLIB0057 // CreateFromSignedFile is obsolete — no direct replacement for Authenticode verification var cert = System.Security.Cryptography.X509Certificates.X509Certificate.CreateFromSignedFile(exe); #pragma warning restore SYSLIB0057 - if (cert == null || !cert.Subject.Contains("Ookla", StringComparison.OrdinalIgnoreCase)) + if (cert is null || !cert.Subject.Contains("Ookla", StringComparison.OrdinalIgnoreCase)) { Log.Warning("Ookla speedtest.exe Authenticode subject mismatch: {Subject}", cert?.Subject ?? "none"); try { File.Delete(exe); } diff --git a/SysManager/SysManager/Services/StartupService.cs b/SysManager/SysManager/Services/StartupService.cs index ea6874f9..fd5f430e 100644 --- a/SysManager/SysManager/Services/StartupService.cs +++ b/SysManager/SysManager/Services/StartupService.cs @@ -96,7 +96,7 @@ private static void ReadStartupFolder(string folderPath, string locationLabel, L try { var shellType = Type.GetTypeFromProgID("WScript.Shell"); - if (shellType != null) + if (shellType is not null) { shell = Activator.CreateInstance(shellType)!; shortcut = ((dynamic)shell).CreateShortcut(file); @@ -117,8 +117,8 @@ private static void ReadStartupFolder(string folderPath, string locationLabel, L } finally { - if (shortcut != null) Marshal.ReleaseComObject(shortcut); - if (shell != null) Marshal.ReleaseComObject(shell); + if (shortcut is not null) Marshal.ReleaseComObject(shortcut); + if (shell is not null) Marshal.ReleaseComObject(shell); } } @@ -152,17 +152,17 @@ private static void ReadScheduledTasks(List results) { using var key = Registry.LocalMachine.OpenSubKey( @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks", writable: false); - if (key == null) return; + if (key is null) return; foreach (var subKeyName in key.GetSubKeyNames()) { try { using var taskKey = key.OpenSubKey(subKeyName, writable: false); - if (taskKey == null) continue; + if (taskKey is null) continue; var triggers = taskKey.GetValue("Triggers") as byte[]; - if (triggers == null || triggers.Length < 4) continue; + if (triggers is null || triggers.Length < 4) continue; var path = taskKey.GetValue("Path")?.ToString() ?? ""; var uri = taskKey.GetValue("URI")?.ToString() ?? path; @@ -222,7 +222,7 @@ private static void ReadRunKey(RegistryKey root, string keyPath, StartupSource s try { using var key = root.OpenSubKey(keyPath, writable: false); - if (key == null) return; + if (key is null) return; var rootName = root == Registry.CurrentUser ? "HKCU" : "HKLM"; @@ -277,7 +277,7 @@ private static void ApplyApprovedState(List entries) _ => null }; - if (approved != null && approved.TryGetValue(entry.ValueName, out var blob) && blob.Length >= 1) + if (approved is not null && approved.TryGetValue(entry.ValueName, out var blob) && blob.Length >= 1) { // Windows uses bit 0 to indicate disabled state: // 02/06 = enabled (even), 03/07 = disabled (odd). @@ -293,7 +293,7 @@ private static void ApplyApprovedState(List entries) try { using var key = root.OpenSubKey(keyPath, writable: false); - if (key == null) return null; + if (key is null) return null; var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var name in key.GetValueNames().Where(n => key.GetValue(n) is byte[])) @@ -328,7 +328,7 @@ public static bool SetEnabled(StartupEntry entry, bool enabled) }; using var key = root.OpenSubKey(approvedPath, writable: true); - if (key == null) + if (key is null) { entry.StatusText = "Error — StartupApproved key not found"; return false; @@ -413,7 +413,7 @@ private static bool SetTaskSchedulerEnabled(StartupEntry entry, bool enabled) }; using var proc = System.Diagnostics.Process.Start(psi); - if (proc == null) + if (proc is null) { entry.StatusText = "Error — could not start schtasks"; return false; diff --git a/SysManager/SysManager/Services/SystemInfoService.cs b/SysManager/SysManager/Services/SystemInfoService.cs index 23436677..e97de7eb 100644 --- a/SysManager/SysManager/Services/SystemInfoService.cs +++ b/SysManager/SysManager/Services/SystemInfoService.cs @@ -148,7 +148,7 @@ private static MemoryInfo QueryMemory() double usedGB = totalGB - freeGB; double pct = totalGB > 0 ? usedGB / totalGB * 100.0 : 0; - var modules = new List(); + List modules = []; using (var s = new ManagementObjectSearcher("SELECT BankLabel,Manufacturer,Capacity,Speed,PartNumber FROM Win32_PhysicalMemory")) { using var modCollection = s.Get(); @@ -171,7 +171,7 @@ private static MemoryInfo QueryMemory() private static List QueryDisks() { - var list = new List(); + List list = []; // Use Storage namespace for MSFT_PhysicalDisk (gives HealthStatus / MediaType) try { diff --git a/SysManager/SysManager/Services/TracerouteService.cs b/SysManager/SysManager/Services/TracerouteService.cs index 8d61c96f..fd4f065f 100644 --- a/SysManager/SysManager/Services/TracerouteService.cs +++ b/SysManager/SysManager/Services/TracerouteService.cs @@ -32,7 +32,7 @@ public async Task> RunAsync(string host, Cancellati ct.ThrowIfCancellationRequested(); var options = new PingOptions(ttl, true); - var latencies = new List(); + List latencies = []; IPAddress? replyAddress = null; IPStatus lastStatus = IPStatus.Unknown; @@ -69,7 +69,7 @@ public async Task> RunAsync(string host, Cancellati // Await reverse DNS with a short timeout before emitting the hop. // This ensures hop.HostName is populated when the UI receives it. - if (replyAddress != null) + if (replyAddress is not null) { var addr = replyAddress; try @@ -99,7 +99,7 @@ public async Task> RunAsync(string host, Cancellati private void RaiseHopCompleted(TracerouteHop hop) { var handlers = HopCompleted?.GetInvocationList(); - if (handlers == null) return; + if (handlers is null) return; foreach (var h in handlers) { try { ((Action)h).Invoke(hop); } diff --git a/SysManager/SysManager/Services/TrayIconService.cs b/SysManager/SysManager/Services/TrayIconService.cs index 2479e0b6..c0cb8999 100644 --- a/SysManager/SysManager/Services/TrayIconService.cs +++ b/SysManager/SysManager/Services/TrayIconService.cs @@ -152,7 +152,7 @@ private async Task UpdateTooltipAsync() try { var snapshot = await _sysInfo.CaptureAsync(); - if (_trayIcon == null) return; + if (_trayIcon is null) return; var tooltip = $"SysManager\n" + $"CPU: {snapshot.Cpu.LoadPercent:0}% | " + @@ -202,7 +202,7 @@ internal void CheckAndNotify(SystemSnapshot snapshot) // Disk health warning var unhealthyDisk = snapshot.Disks.FirstOrDefault(d => d.HealthStatus != "Healthy"); - if (unhealthyDisk != null && now - _lastDiskNotification > NotificationCooldown) + if (unhealthyDisk is not null && now - _lastDiskNotification > NotificationCooldown) { _lastDiskNotification = now; ShowNotification("Disk Health Warning", diff --git a/SysManager/SysManager/Services/TuneUpService.cs b/SysManager/SysManager/Services/TuneUpService.cs index d4e0e836..a34c6eeb 100644 --- a/SysManager/SysManager/Services/TuneUpService.cs +++ b/SysManager/SysManager/Services/TuneUpService.cs @@ -81,7 +81,7 @@ public async Task RunAsync( // Step 4: Disk SMART progress?.Report((3, "Checking disk health…")); - var diskSummaries = new List(); + List diskSummaries = []; try { var reports = await _diskHealth.CollectAsync(ct); diff --git a/SysManager/SysManager/Services/UninstallerService.cs b/SysManager/SysManager/Services/UninstallerService.cs index 4b4cd059..384e5e12 100644 --- a/SysManager/SysManager/Services/UninstallerService.cs +++ b/SysManager/SysManager/Services/UninstallerService.cs @@ -28,7 +28,7 @@ public event Action? LineReceived /// public async Task> ListInstalledAsync(CancellationToken ct = default) { - var captured = new List(); + List captured = []; void Collect(PowerShellLine l) { if (l.Kind == OutputKind.Output) captured.Add(l.Text); @@ -125,7 +125,7 @@ internal static void EnrichFromRegistry(List apps) try { using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(regPath); - if (key == null) continue; + if (key is null) continue; EnrichFromRegistryKey(key, lookup); } @@ -138,7 +138,7 @@ internal static void EnrichFromRegistry(List apps) { using var hkcuKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey( @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"); - if (hkcuKey != null) + if (hkcuKey is not null) EnrichFromRegistryKey(hkcuKey, lookup); } catch (System.Security.SecurityException) { /* skip protected HKCU key */ } @@ -154,7 +154,7 @@ private static void EnrichFromRegistryKey( try { using var sub = key.OpenSubKey(subName); - if (sub == null) continue; + if (sub is null) continue; var displayName = sub.GetValue("DisplayName") as string; if (string.IsNullOrWhiteSpace(displayName)) continue; @@ -186,7 +186,7 @@ private static void EnrichFromRegistryKey( app.UninstallString = uninst; } - if (app.Icon == null) + if (app.Icon is null) { var iconPath = sub.GetValue("DisplayIcon") as string; var installLoc = sub.GetValue("InstallLocation") as string; diff --git a/SysManager/SysManager/Services/UpdateService.cs b/SysManager/SysManager/Services/UpdateService.cs index ccea8ef2..dcfe179b 100644 --- a/SysManager/SysManager/Services/UpdateService.cs +++ b/SysManager/SysManager/Services/UpdateService.cs @@ -69,7 +69,7 @@ public sealed record ReleaseInfo( try { var dto = await Http.GetFromJsonAsync(url, ct).ConfigureAwait(false); - if (dto == null) { LastError = "GitHub returned an empty response."; return null; } + if (dto is null) { LastError = "GitHub returned an empty response."; return null; } return Map(dto); } catch (OperationCanceledException) @@ -101,7 +101,7 @@ public async Task> GetRecentAsync(int count = 10, Can { var url = $"https://api.github.com/repos/{Owner}/{Repo}/releases?per_page={count}"; var dto = await Http.GetFromJsonAsync(url, ct).ConfigureAwait(false); - if (dto == null) return []; + if (dto is null) return []; return dto.Select(Map).OfType().ToList(); } catch (OperationCanceledException) @@ -307,7 +307,7 @@ public static bool VerifyAuthenticode(string filePath) var cert = System.Security.Cryptography.X509Certificates.X509Certificate .CreateFromSignedFile(filePath); #pragma warning restore SYSLIB0057 - if (cert != null) + if (cert is not null) { Serilog.Log.Information("Update binary Authenticode verified: {Subject}", cert.Subject); return true; @@ -328,10 +328,10 @@ public static bool VerifyAuthenticode(string filePath) private static ReleaseInfo? Map(GhRelease dto) { - if (dto.TagName == null) return null; + if (dto.TagName is null) return null; if (dto.Prerelease || dto.Draft) return null; var version = ParseVersion(dto.TagName); - if (version == null) return null; + if (version is null) return null; var asset = dto.Assets?.FirstOrDefault(a => string.Equals(a.Name, AssetName, StringComparison.OrdinalIgnoreCase)); diff --git a/SysManager/SysManager/Services/WindowsFeaturesService.cs b/SysManager/SysManager/Services/WindowsFeaturesService.cs index c89fd9dc..2fdf11ba 100644 --- a/SysManager/SysManager/Services/WindowsFeaturesService.cs +++ b/SysManager/SysManager/Services/WindowsFeaturesService.cs @@ -24,7 +24,7 @@ public sealed partial class WindowsFeaturesService /// public async Task> ListFeaturesAsync(CancellationToken ct = default) { - var captured = new List(); + List captured = []; void Collect(PowerShellLine l) { if (l.Kind == OutputKind.Output) captured.Add(l.Text); @@ -55,7 +55,7 @@ await _runner.RunProcessAsync("powershell", Log.Information("Enabling Windows feature: {Feature}", featureName); - var captured = new List(); + List captured = []; void Collect(PowerShellLine l) { if (l.Kind == OutputKind.Output) captured.Add(l.Text); @@ -89,7 +89,7 @@ void Collect(PowerShellLine l) Log.Information("Disabling Windows feature: {Feature}", featureName); - var captured = new List(); + List captured = []; void Collect(PowerShellLine l) { if (l.Kind == OutputKind.Output) captured.Add(l.Text); diff --git a/SysManager/SysManager/Services/WingetService.cs b/SysManager/SysManager/Services/WingetService.cs index f69d51e9..c0860c9e 100644 --- a/SysManager/SysManager/Services/WingetService.cs +++ b/SysManager/SysManager/Services/WingetService.cs @@ -27,7 +27,7 @@ public event Action? LineReceived /// public async Task> ListUpgradableAsync(CancellationToken ct = default) { - var captured = new List(); + List captured = []; void Collect(PowerShellLine l) { diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj index 9b5221ff..f83947c2 100644 --- a/SysManager/SysManager/SysManager.csproj +++ b/SysManager/SysManager/SysManager.csproj @@ -10,9 +10,9 @@ SysManager true NU1603;NU1701 - 0.48.21 - 0.48.21.0 - 0.48.21.0 + 1.0.0 + 1.0.0.0 + 1.0.0.0 SysManager SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup. https://github.com/laurentiu021/SystemManager diff --git a/SysManager/SysManager/ViewModels/AboutViewModel.cs b/SysManager/SysManager/ViewModels/AboutViewModel.cs index eba8a7de..c108fa35 100644 --- a/SysManager/SysManager/ViewModels/AboutViewModel.cs +++ b/SysManager/SysManager/ViewModels/AboutViewModel.cs @@ -17,7 +17,7 @@ namespace SysManager.ViewModels; -public partial class AboutViewModel : ViewModelBase +public sealed partial class AboutViewModel : ViewModelBase { private readonly UpdateService _updates; private UpdateService.ReleaseInfo? _latest; diff --git a/SysManager/SysManager/ViewModels/AppAlertsViewModel.cs b/SysManager/SysManager/ViewModels/AppAlertsViewModel.cs index 98b199a1..ce3223f9 100644 --- a/SysManager/SysManager/ViewModels/AppAlertsViewModel.cs +++ b/SysManager/SysManager/ViewModels/AppAlertsViewModel.cs @@ -17,7 +17,7 @@ namespace SysManager.ViewModels; /// App Alerts tab — monitors for new application installations and shows /// a timestamped history of detected installs. /// -public partial class AppAlertsViewModel : ViewModelBase +public sealed partial class AppAlertsViewModel : ViewModelBase { private readonly AppAlertService _service = new(); private readonly Dispatcher _dispatcher; diff --git a/SysManager/SysManager/ViewModels/AppBlockerViewModel.cs b/SysManager/SysManager/ViewModels/AppBlockerViewModel.cs index 5ffd0b50..1ba3fac9 100644 --- a/SysManager/SysManager/ViewModels/AppBlockerViewModel.cs +++ b/SysManager/SysManager/ViewModels/AppBlockerViewModel.cs @@ -17,7 +17,7 @@ namespace SysManager.ViewModels; /// App Blocker tab — prevents selected applications from executing using /// Image File Execution Options (IFEO) registry mechanism. /// -public partial class AppBlockerViewModel : ViewModelBase +public sealed partial class AppBlockerViewModel : ViewModelBase { public ObservableCollection BlockedApps { get; } = new(); diff --git a/SysManager/SysManager/ViewModels/AppUpdatesViewModel.cs b/SysManager/SysManager/ViewModels/AppUpdatesViewModel.cs index 4481f199..bcf85820 100644 --- a/SysManager/SysManager/ViewModels/AppUpdatesViewModel.cs +++ b/SysManager/SysManager/ViewModels/AppUpdatesViewModel.cs @@ -12,7 +12,7 @@ namespace SysManager.ViewModels; -public partial class AppUpdatesViewModel : ViewModelBase +public sealed partial class AppUpdatesViewModel : ViewModelBase { private readonly WingetService _winget; private CancellationTokenSource? _cts; diff --git a/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs b/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs index 9c9f2d67..4ce04e1c 100644 --- a/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs +++ b/SysManager/SysManager/ViewModels/BatteryHealthViewModel.cs @@ -14,7 +14,7 @@ namespace SysManager.ViewModels; /// /// Battery Health tab — shows charge, health %, wear, cycles, runtime. /// -public partial class BatteryHealthViewModel : ViewModelBase +public sealed partial class BatteryHealthViewModel : ViewModelBase { private readonly BatteryService _service = new(); diff --git a/SysManager/SysManager/ViewModels/CleanupViewModel.cs b/SysManager/SysManager/ViewModels/CleanupViewModel.cs index 7e07eaef..f7b1dded 100644 --- a/SysManager/SysManager/ViewModels/CleanupViewModel.cs +++ b/SysManager/SysManager/ViewModels/CleanupViewModel.cs @@ -12,7 +12,7 @@ namespace SysManager.ViewModels; -public partial class CleanupViewModel : ViewModelBase +public sealed partial class CleanupViewModel : ViewModelBase { private readonly PowerShellRunner _runner; diff --git a/SysManager/SysManager/ViewModels/ConsoleViewModel.cs b/SysManager/SysManager/ViewModels/ConsoleViewModel.cs index f8c621b3..d56b37a2 100644 --- a/SysManager/SysManager/ViewModels/ConsoleViewModel.cs +++ b/SysManager/SysManager/ViewModels/ConsoleViewModel.cs @@ -14,7 +14,7 @@ namespace SysManager.ViewModels; /// Shared, scrollable console view-model. Each tab has its own instance. /// Lines are capped to avoid unbounded memory growth on long-running installs. /// -public partial class ConsoleViewModel : ObservableObject +public sealed partial class ConsoleViewModel : ObservableObject { private const int MaxLines = 5000; private readonly object _gate = new(); diff --git a/SysManager/SysManager/ViewModels/DashboardViewModel.cs b/SysManager/SysManager/ViewModels/DashboardViewModel.cs index bfba474d..1d6cfdbe 100644 --- a/SysManager/SysManager/ViewModels/DashboardViewModel.cs +++ b/SysManager/SysManager/ViewModels/DashboardViewModel.cs @@ -12,7 +12,7 @@ namespace SysManager.ViewModels; -public partial class DashboardViewModel : ViewModelBase +public sealed partial class DashboardViewModel : ViewModelBase { private readonly SystemInfoService _sys; private readonly TuneUpService _tuneUp; diff --git a/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs b/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs index 3a6dfca9..382195e4 100644 --- a/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs +++ b/SysManager/SysManager/ViewModels/DeepCleanupViewModel.cs @@ -14,7 +14,7 @@ namespace SysManager.ViewModels; -public partial class DeepCleanupViewModel : ViewModelBase +public sealed partial class DeepCleanupViewModel : ViewModelBase { private readonly DeepCleanupService _cleanup = new(); private readonly LargeFileScanner _largeFiles = new(); diff --git a/SysManager/SysManager/ViewModels/DiskAnalyzerViewModel.cs b/SysManager/SysManager/ViewModels/DiskAnalyzerViewModel.cs index af64243b..36448186 100644 --- a/SysManager/SysManager/ViewModels/DiskAnalyzerViewModel.cs +++ b/SysManager/SysManager/ViewModels/DiskAnalyzerViewModel.cs @@ -17,7 +17,7 @@ namespace SysManager.ViewModels; /// Disk Analyzer tab — shows space breakdown by top-level folders. /// Read-only: only "Show in Explorer" is offered. /// -public partial class DiskAnalyzerViewModel : ViewModelBase +public sealed partial class DiskAnalyzerViewModel : ViewModelBase { private readonly DiskAnalyzerService _service = new(); private CancellationTokenSource? _cts; diff --git a/SysManager/SysManager/ViewModels/DriversViewModel.cs b/SysManager/SysManager/ViewModels/DriversViewModel.cs index 956d2a76..7bc1d2b0 100644 --- a/SysManager/SysManager/ViewModels/DriversViewModel.cs +++ b/SysManager/SysManager/ViewModels/DriversViewModel.cs @@ -14,7 +14,7 @@ namespace SysManager.ViewModels; -public partial class DriversViewModel : ViewModelBase +public sealed partial class DriversViewModel : ViewModelBase { private readonly PowerShellRunner _runner; private CancellationTokenSource? _cts; diff --git a/SysManager/SysManager/ViewModels/DuplicateFileViewModel.cs b/SysManager/SysManager/ViewModels/DuplicateFileViewModel.cs index 8ab31e54..76a2d163 100644 --- a/SysManager/SysManager/ViewModels/DuplicateFileViewModel.cs +++ b/SysManager/SysManager/ViewModels/DuplicateFileViewModel.cs @@ -18,7 +18,7 @@ namespace SysManager.ViewModels; /// content and shows them grouped by hash. Read-only: only "Show in /// Explorer" and "Copy path" are offered. /// -public partial class DuplicateFileViewModel : ViewModelBase +public sealed partial class DuplicateFileViewModel : ViewModelBase { private readonly DuplicateFileService _service = new(); private CancellationTokenSource? _cts; diff --git a/SysManager/SysManager/ViewModels/LogsViewModel.cs b/SysManager/SysManager/ViewModels/LogsViewModel.cs index 912b266a..6bc312a3 100644 --- a/SysManager/SysManager/ViewModels/LogsViewModel.cs +++ b/SysManager/SysManager/ViewModels/LogsViewModel.cs @@ -23,7 +23,7 @@ namespace SysManager.ViewModels; /// live via a CollectionView, and shows a plain-English explanation for the /// selected entry. Supports export to CSV and jumping to the raw log file. /// -public partial class LogsViewModel : ViewModelBase +public sealed partial class LogsViewModel : ViewModelBase { private readonly EventLogService _eventLogs = new(); private readonly SynchronizationContext? _sync; diff --git a/SysManager/SysManager/ViewModels/MainWindowViewModel.cs b/SysManager/SysManager/ViewModels/MainWindowViewModel.cs index 060916f8..9331dacf 100644 --- a/SysManager/SysManager/ViewModels/MainWindowViewModel.cs +++ b/SysManager/SysManager/ViewModels/MainWindowViewModel.cs @@ -12,7 +12,7 @@ namespace SysManager.ViewModels; -public partial class MainWindowViewModel : ObservableObject, IDisposable +public sealed partial class MainWindowViewModel : ObservableObject, IDisposable { public DashboardViewModel Dashboard { get; } public AppUpdatesViewModel AppUpdates { get; } diff --git a/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs b/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs index b1c6bfb7..1d1b201a 100644 --- a/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs +++ b/SysManager/SysManager/ViewModels/NetworkRepairViewModel.cs @@ -12,7 +12,7 @@ namespace SysManager.ViewModels; /// DNS flush, Winsock reset, TCP/IP reset. -public partial class NetworkRepairViewModel : ViewModelBase +public sealed partial class NetworkRepairViewModel : ViewModelBase { public NetworkSharedState Shared { get; } diff --git a/SysManager/SysManager/ViewModels/PerformanceViewModel.cs b/SysManager/SysManager/ViewModels/PerformanceViewModel.cs index fb990f6d..759da1e6 100644 --- a/SysManager/SysManager/ViewModels/PerformanceViewModel.cs +++ b/SysManager/SysManager/ViewModels/PerformanceViewModel.cs @@ -22,7 +22,7 @@ namespace SysManager.ViewModels; /// • Confirmation dialog before every destructive action. /// • GPU changes warn about reboot requirement. /// -public partial class PerformanceViewModel : ViewModelBase +public sealed partial class PerformanceViewModel : ViewModelBase { private readonly PerformanceService _service; private PerformanceService.OriginalSnapshot? _snapshot; diff --git a/SysManager/SysManager/ViewModels/PingViewModel.cs b/SysManager/SysManager/ViewModels/PingViewModel.cs index 6c88d3ea..718dd769 100644 --- a/SysManager/SysManager/ViewModels/PingViewModel.cs +++ b/SysManager/SysManager/ViewModels/PingViewModel.cs @@ -13,7 +13,7 @@ namespace SysManager.ViewModels; /// Live ping monitoring: targets, presets, latency chart, health verdict. /// Delegates shared state (targets, buffers, pinger) to . /// -public partial class PingViewModel : ViewModelBase +public sealed partial class PingViewModel : ViewModelBase { public NetworkSharedState Shared { get; } diff --git a/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs b/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs index bc7b77a4..0652bf09 100644 --- a/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs +++ b/SysManager/SysManager/ViewModels/ProcessManagerViewModel.cs @@ -16,7 +16,7 @@ namespace SysManager.ViewModels; /// Process Manager tab — lists running processes with memory/thread info, /// allows kill and open file location. /// -public partial class ProcessManagerViewModel : ViewModelBase +public sealed partial class ProcessManagerViewModel : ViewModelBase { private readonly ProcessManagerService _service = new(); diff --git a/SysManager/SysManager/ViewModels/ServicesViewModel.cs b/SysManager/SysManager/ViewModels/ServicesViewModel.cs index 3835bd94..f337b641 100644 --- a/SysManager/SysManager/ViewModels/ServicesViewModel.cs +++ b/SysManager/SysManager/ViewModels/ServicesViewModel.cs @@ -18,7 +18,7 @@ namespace SysManager.ViewModels; /// Services tab — lists all Windows services with gaming recommendations, /// allows start/stop and startup type changes. /// -public partial class ServicesViewModel : ViewModelBase +public sealed partial class ServicesViewModel : ViewModelBase { private readonly PowerShellRunner _ps = new(); private List _allServices = new(); diff --git a/SysManager/SysManager/ViewModels/ShortcutCleanerViewModel.cs b/SysManager/SysManager/ViewModels/ShortcutCleanerViewModel.cs index df0d6c70..7dd30185 100644 --- a/SysManager/SysManager/ViewModels/ShortcutCleanerViewModel.cs +++ b/SysManager/SysManager/ViewModels/ShortcutCleanerViewModel.cs @@ -15,7 +15,7 @@ namespace SysManager.ViewModels; /// /// Shortcut Cleaner tab — scans for broken .lnk files and allows deletion. /// -public partial class ShortcutCleanerViewModel : ViewModelBase +public sealed partial class ShortcutCleanerViewModel : ViewModelBase { private readonly ShortcutCleanerService _service = new(); private CancellationTokenSource? _cts; diff --git a/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs b/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs index 409dd8ea..22f0f51a 100644 --- a/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs +++ b/SysManager/SysManager/ViewModels/SpeedTestViewModel.cs @@ -13,7 +13,7 @@ namespace SysManager.ViewModels; /// HTTP + Ookla speed tests with persistent history. -public partial class SpeedTestViewModel : ViewModelBase +public sealed partial class SpeedTestViewModel : ViewModelBase { public NetworkSharedState Shared { get; } private readonly SpeedTestHistoryService _history = new(); diff --git a/SysManager/SysManager/ViewModels/StartupViewModel.cs b/SysManager/SysManager/ViewModels/StartupViewModel.cs index e31d45e8..f30ff72e 100644 --- a/SysManager/SysManager/ViewModels/StartupViewModel.cs +++ b/SysManager/SysManager/ViewModels/StartupViewModel.cs @@ -16,7 +16,7 @@ namespace SysManager.ViewModels; /// Startup Manager tab — lists all programs that run at Windows boot /// and lets the user enable/disable them non-destructively. /// -public partial class StartupViewModel : ViewModelBase +public sealed partial class StartupViewModel : ViewModelBase { private readonly StartupService _service = new(); private readonly List _allEntries = new(); diff --git a/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs b/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs index bcdb19b2..61580842 100644 --- a/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs +++ b/SysManager/SysManager/ViewModels/SystemHealthViewModel.cs @@ -15,7 +15,7 @@ namespace SysManager.ViewModels; -public partial class SystemHealthViewModel : ViewModelBase +public sealed partial class SystemHealthViewModel : ViewModelBase { private readonly SystemInfoService _sys; private readonly DiskHealthService _diskHealth = new(); diff --git a/SysManager/SysManager/ViewModels/TracerouteViewModel.cs b/SysManager/SysManager/ViewModels/TracerouteViewModel.cs index e7fb59a5..e554d94e 100644 --- a/SysManager/SysManager/ViewModels/TracerouteViewModel.cs +++ b/SysManager/SysManager/ViewModels/TracerouteViewModel.cs @@ -14,7 +14,7 @@ namespace SysManager.ViewModels; /// Auto-traceroute + manual trace. Has its own Start/Stop for the /// auto-trace monitor, independent of the ping monitor. /// -public partial class TracerouteViewModel : ViewModelBase +public sealed partial class TracerouteViewModel : ViewModelBase { public NetworkSharedState Shared { get; } private CancellationTokenSource? _traceCts; diff --git a/SysManager/SysManager/ViewModels/UninstallerViewModel.cs b/SysManager/SysManager/ViewModels/UninstallerViewModel.cs index 6129c253..2f0664c6 100644 --- a/SysManager/SysManager/ViewModels/UninstallerViewModel.cs +++ b/SysManager/SysManager/ViewModels/UninstallerViewModel.cs @@ -14,7 +14,7 @@ namespace SysManager.ViewModels; /// /// Uninstaller tab — lists installed apps, filter, select, uninstall. /// -public partial class UninstallerViewModel : ViewModelBase +public sealed partial class UninstallerViewModel : ViewModelBase { private readonly UninstallerService _service; private readonly Action _lineHandler; diff --git a/SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs b/SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs index e5422a72..d3a0c368 100644 --- a/SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs +++ b/SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs @@ -16,7 +16,7 @@ namespace SysManager.ViewModels; /// Windows Features tab — lists optional features, allows enable/disable toggle. /// Requires administrator privileges for modifications. /// -public partial class WindowsFeaturesViewModel : ViewModelBase +public sealed partial class WindowsFeaturesViewModel : ViewModelBase { private readonly WindowsFeaturesService _service; private CancellationTokenSource? _scanCts; diff --git a/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs b/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs index 2ad0c0e9..d4a53323 100644 --- a/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs +++ b/SysManager/SysManager/ViewModels/WindowsUpdateViewModel.cs @@ -14,7 +14,7 @@ namespace SysManager.ViewModels; -public partial class WindowsUpdateViewModel : ViewModelBase +public sealed partial class WindowsUpdateViewModel : ViewModelBase { private readonly PowerShellRunner _runner; private CancellationTokenSource? _cts; diff --git a/SysManager/SysManager/Views/AboutView.xaml.cs b/SysManager/SysManager/Views/AboutView.xaml.cs index ed66ae9b..0d0359ce 100644 --- a/SysManager/SysManager/Views/AboutView.xaml.cs +++ b/SysManager/SysManager/Views/AboutView.xaml.cs @@ -1,5 +1,6 @@ // SysManager · AboutView — version info + update management UI // Author: laurentiu021 · https://github.com/laurentiu021/SystemManager +// License: MIT using System.Windows.Controls;