diff --git a/CHANGELOG.md b/CHANGELOG.md index 878eecc9..eaeb9b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.48.10] - 2026-05-15 + +### Fixed +- **DiskHealthService (CQ-001)** — ManagementObjectCollection and ManagementObject + instances now properly disposed via `using` statements, preventing COM resource + leaks during SMART/reliability queries. +- **SpeedTestService (CQ-003)** — Ookla CLI process now has a 5-minute independent + timeout via linked CancellationTokenSource, preventing indefinite hangs. + +### Changed +- **CONTRIBUTING.md** — corrected .NET SDK reference from 8 to 9; added + SysManager.IntegrationTests to project layout. +- **SECURITY.md** — updated supported versions table to reflect 0.48.x as latest. + ## [0.48.9] - 2026-05-14 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 363e3873..1e4f8399 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 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0). +- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0). - Git 2.40+. - A Windows account with admin rights (needed to test elevated features; the app itself runs fine unelevated for everything else). @@ -75,7 +75,8 @@ SysManager/ │ ├── Views/ # XAML + minimal code-behind │ ├── Helpers/ # small utilities, converters │ └── Resources/ # icons, generated assets -├── SysManager.Tests/ # xUnit unit + integration tests +├── SysManager.Tests/ # xUnit unit tests +├── SysManager.IntegrationTests/ # integration tests (local only, not CI) └── SysManager.UITests/ # FlaUI UI-automation tests ``` diff --git a/SECURITY.md b/SECURITY.md index 8520f509..e5ca946a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,11 +12,10 @@ older build, the first step is usually to update. | Version | Supported | | ------- | ------------------ | +| 0.48.x | :white_check_mark: | | 0.47.x | :white_check_mark: | -| 0.46.x | :white_check_mark: | -| 0.45.x | :white_check_mark: | -| 0.44.x | :x: | -| < 0.44 | :x: | +| 0.46.x | :x: | +| < 0.46 | :x: | ## Reporting a vulnerability diff --git a/SysManager/SysManager/Services/DiskHealthService.cs b/SysManager/SysManager/Services/DiskHealthService.cs index 60ca6fd1..4fd3aae1 100644 --- a/SysManager/SysManager/Services/DiskHealthService.cs +++ b/SysManager/SysManager/Services/DiskHealthService.cs @@ -29,24 +29,28 @@ private static IReadOnlyList Collect() // First pull MSFT_PhysicalDisk for basic info. using var searcher = new ManagementObjectSearcher(scope, new ObjectQuery("SELECT ObjectId, FriendlyName, MediaType, BusType, Size, HealthStatus FROM MSFT_PhysicalDisk")); - foreach (ManagementObject mo in searcher.Get()) + using var collection = searcher.Get(); + foreach (ManagementObject mo in collection) { - var report = new DiskHealthReport + using (mo) { - FriendlyName = mo["FriendlyName"]?.ToString() ?? "Disk", - MediaType = MapMedia(Convert.ToUInt32(mo["MediaType"] ?? 0u)), - BusType = MapBus(Convert.ToUInt32(mo["BusType"] ?? 0u)), - SizeGB = Math.Round(Convert.ToDouble(mo["Size"] ?? 0) / 1024d / 1024d / 1024d, 0), - HealthStatus = MapHealth(Convert.ToUInt32(mo["HealthStatus"] ?? 0u)) - }; - - // Get reliability counters for this disk. - var objectId = mo["ObjectId"]?.ToString(); - if (!string.IsNullOrEmpty(objectId)) - EnrichWithReliability(scope, objectId, report); - - ApplyVerdict(report); - results.Add(report); + var report = new DiskHealthReport + { + FriendlyName = mo["FriendlyName"]?.ToString() ?? "Disk", + MediaType = MapMedia(Convert.ToUInt32(mo["MediaType"] ?? 0u)), + BusType = MapBus(Convert.ToUInt32(mo["BusType"] ?? 0u)), + SizeGB = Math.Round(Convert.ToDouble(mo["Size"] ?? 0) / 1024d / 1024d / 1024d, 0), + HealthStatus = MapHealth(Convert.ToUInt32(mo["HealthStatus"] ?? 0u)) + }; + + // Get reliability counters for this disk. + var objectId = mo["ObjectId"]?.ToString(); + if (!string.IsNullOrEmpty(objectId)) + EnrichWithReliability(scope, objectId, report); + + ApplyVerdict(report); + results.Add(report); + } } } catch (ManagementException) @@ -69,17 +73,21 @@ private static void EnrichWithReliability(ManagementScope scope, string objectId var query = new ObjectQuery( $"ASSOCIATORS OF {{MSFT_PhysicalDisk.ObjectId=\"{safeId}\"}} WHERE AssocClass=MSFT_PhysicalDiskToStorageReliabilityCounter"); using var searcher = new ManagementObjectSearcher(scope, query); - foreach (ManagementObject mo in searcher.Get()) + using var collection = searcher.Get(); + foreach (ManagementObject mo in collection) { - report.TemperatureC = ToDouble(mo["Temperature"]); - report.TemperatureMaxC = ToDouble(mo["TemperatureMax"]); - var wear = ToInt(mo["Wear"]); - if (wear.HasValue) report.WearPercent = wear; - report.PowerOnHours = ToLong(mo["PowerOnHours"]); - report.ReadErrors = ToLong(mo["ReadErrorsTotal"]); - report.WriteErrors = ToLong(mo["WriteErrorsTotal"]); - report.StartStopCount = ToLong(mo["StartStopCycleCount"]); - return; // one counter per disk + using (mo) + { + report.TemperatureC = ToDouble(mo["Temperature"]); + report.TemperatureMaxC = ToDouble(mo["TemperatureMax"]); + var wear = ToInt(mo["Wear"]); + if (wear.HasValue) report.WearPercent = wear; + report.PowerOnHours = ToLong(mo["PowerOnHours"]); + report.ReadErrors = ToLong(mo["ReadErrorsTotal"]); + report.WriteErrors = ToLong(mo["WriteErrorsTotal"]); + report.StartStopCount = ToLong(mo["StartStopCycleCount"]); + return; // one counter per disk + } } } catch (ManagementException) { /* driver may not expose counters */ } diff --git a/SysManager/SysManager/Services/SpeedTestService.cs b/SysManager/SysManager/Services/SpeedTestService.cs index 63df3944..15802f2d 100644 --- a/SysManager/SysManager/Services/SpeedTestService.cs +++ b/SysManager/SysManager/Services/SpeedTestService.cs @@ -196,13 +196,18 @@ public async Task RunOoklaAsync( }; // Start the process on a thread-pool thread so Process.Start() - // never blocks the UI thread. + // never blocks the UI thread. Link a 5-minute timeout to prevent + // indefinite hangs if the Ookla CLI freezes (CQ-003). + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeoutCts.CancelAfter(TimeSpan.FromMinutes(5)); + var linked = timeoutCts.Token; + using var proc = new Process(); proc.StartInfo = psi; - await Task.Run(() => proc.Start(), ct).ConfigureAwait(false); - var stdout = await proc.StandardOutput.ReadToEndAsync(ct); - var stderr = await proc.StandardError.ReadToEndAsync(ct); - await proc.WaitForExitAsync(ct); + await Task.Run(() => proc.Start(), linked).ConfigureAwait(false); + var stdout = await proc.StandardOutput.ReadToEndAsync(linked); + var stderr = await proc.StandardError.ReadToEndAsync(linked); + await proc.WaitForExitAsync(linked); if (proc.ExitCode != 0) {