From dafe02ff7d10e30387bd87f6953f7087d04ae292 Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Wed, 17 Jun 2026 19:33:05 +0300 Subject: [PATCH] fix: harden WMI/event-log error handling in health score, memory check, and known folders --- CHANGELOG.md | 7 +++ SysManager/SysManager/Helpers/KnownFolders.cs | 46 +++++++++++-------- .../SysManager/Services/HealthScoreService.cs | 6 +++ .../SysManager/Services/MemoryTestService.cs | 22 +++++++-- SysManager/SysManager/SysManager.csproj | 6 +-- 5 files changed, 60 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3abfd..5f2a1dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [1.20.40] - 2026-06-17 + +### Fixed +- **The Health Score no longer fails outright when a system query hits a transient WMI error.** A repository or RPC fault while reading system info, disk health, or battery could throw an error the score didn't handle, failing the whole calculation; each source now degrades gracefully and the score is still produced from the rest. +- **The memory-error check no longer counts a *passing* memory test as an error.** It counted every Windows Memory Diagnostic result, including the "no errors found" result (event 1101), as a problem; it now counts only the actual error result (event 1201). +- **Known-folder lookup (Downloads, Documents, etc.) now falls back correctly when the system call fails.** The call's failure code was being ignored, so a failed lookup could return an empty path instead of using the standard fallback location; the result is now checked and the fallback applies. + ## [1.20.39] - 2026-06-17 ### Fixed diff --git a/SysManager/SysManager/Helpers/KnownFolders.cs b/SysManager/SysManager/Helpers/KnownFolders.cs index 3599de3..ed8ba0e 100644 --- a/SysManager/SysManager/Helpers/KnownFolders.cs +++ b/SysManager/SysManager/Helpers/KnownFolders.cs @@ -23,7 +23,7 @@ internal static partial class KnownFolders private static readonly Guid Videos = new("18989B1D-99B5-455B-841C-AB7C74E4DDFC"); [LibraryImport("shell32.dll", StringMarshalling = StringMarshalling.Utf16)] - private static partial void SHGetKnownFolderPath( + private static partial int SHGetKnownFolderPath( in Guid rfid, uint dwFlags, nint hToken, @@ -49,27 +49,33 @@ private static partial void SHGetKnownFolderPath( private static string GetPath(Guid folderId) { + // The import returns the HRESULT. Because this is a plain (non-COM-interface) + // LibraryImport, a failure does NOT throw — it returns a non-zero HRESULT and a + // null/empty path. Check the HRESULT (and guard the path) and fall back to the + // SpecialFolder equivalent on any failure. try { - SHGetKnownFolderPath(folderId, 0, nint.Zero, out var path); - return path; - } - catch (COMException) - { - // Fallback to Environment.SpecialFolder if P/Invoke fails - return folderId == Downloads - ? Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads") - : folderId == Documents - ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) - : folderId == Desktop - ? Environment.GetFolderPath(Environment.SpecialFolder.Desktop) - : folderId == Pictures - ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) - : folderId == Music - ? Environment.GetFolderPath(Environment.SpecialFolder.MyMusic) - : folderId == Videos - ? Environment.GetFolderPath(Environment.SpecialFolder.MyVideos) - : string.Empty; + int hr = SHGetKnownFolderPath(folderId, 0, nint.Zero, out var path); + if (hr >= 0 && !string.IsNullOrEmpty(path)) + return path; } + catch (COMException) { /* fall through to the SpecialFolder fallback below */ } + + return Fallback(folderId); } + + private static string Fallback(Guid folderId) => + folderId == Downloads + ? Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads") + : folderId == Documents + ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + : folderId == Desktop + ? Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + : folderId == Pictures + ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) + : folderId == Music + ? Environment.GetFolderPath(Environment.SpecialFolder.MyMusic) + : folderId == Videos + ? Environment.GetFolderPath(Environment.SpecialFolder.MyVideos) + : string.Empty; } diff --git a/SysManager/SysManager/Services/HealthScoreService.cs b/SysManager/SysManager/Services/HealthScoreService.cs index f21c3e0..0e7889c 100644 --- a/SysManager/SysManager/Services/HealthScoreService.cs +++ b/SysManager/SysManager/Services/HealthScoreService.cs @@ -47,16 +47,22 @@ public async Task ComputeAsync(CancellationToken ct = default IReadOnlyList? disks = null; BatteryInfo? battery = null; + // WMI enumeration (Get()) can throw COMException on repository/RPC failures, not + // just ManagementException — without this arm a transient WMI fault crashes the + // whole health score instead of degrading to a partial result. try { snapshot = await sysTask.ConfigureAwait(false); } catch (System.Management.ManagementException ex) { Log.Warning("HealthScore: system info failed: {Error}", ex.Message); } + catch (System.Runtime.InteropServices.COMException ex) { Log.Warning("HealthScore: system info WMI COM error: 0x{HResult:X8}", ex.HResult); } catch (InvalidOperationException ex) { Log.Warning("HealthScore: system info failed: {Error}", ex.Message); } try { disks = await diskTask.ConfigureAwait(false); } catch (System.Management.ManagementException ex) { Log.Warning("HealthScore: disk health failed: {Error}", ex.Message); } + catch (System.Runtime.InteropServices.COMException ex) { Log.Warning("HealthScore: disk health WMI COM error: 0x{HResult:X8}", ex.HResult); } catch (InvalidOperationException ex) { Log.Warning("HealthScore: disk health failed: {Error}", ex.Message); } try { battery = await batteryTask.ConfigureAwait(false); } catch (System.Management.ManagementException ex) { Log.Warning("HealthScore: battery failed: {Error}", ex.Message); } + catch (System.Runtime.InteropServices.COMException ex) { Log.Warning("HealthScore: battery WMI COM error: 0x{HResult:X8}", ex.HResult); } catch (InvalidOperationException ex) { Log.Warning("HealthScore: battery failed: {Error}", ex.Message); } // Compute component scores diff --git a/SysManager/SysManager/Services/MemoryTestService.cs b/SysManager/SysManager/Services/MemoryTestService.cs index ba4cdd4..9c12302 100644 --- a/SysManager/SysManager/Services/MemoryTestService.cs +++ b/SysManager/SysManager/Services/MemoryTestService.cs @@ -40,25 +40,39 @@ public async Task CheckErrorLogsAsync(CancellationToken ct = { ReverseDirection = true }); var cutoff = DateTime.Now.AddDays(-30); - System.Diagnostics.Eventing.Reader.EventRecord? rec; - while ((rec = reader.ReadEvent()) != null && !ct.IsCancellationRequested) + // Check cancellation BEFORE reading so a record read at the moment of + // cancellation isn't left to the GC; the read result is always wrapped + // in using(rec) below. + while (!ct.IsCancellationRequested && reader.ReadEvent() is { } rec) { using (rec) { if (rec.TimeCreated.HasValue && rec.TimeCreated.Value < cutoff) break; var provider = rec.ProviderName ?? ""; + bool counted = false; if (provider.Contains("WHEA")) { // Memory-related WHEA events are ID 17 / 18 / 19 / 20 typically if (rec.Id == 17 || rec.Id == 18 || rec.Id == 19 || rec.Id == 20) + { wheaCount++; + counted = true; + } } else if (provider.Contains("MemoryDiagnostics")) { - diagCount++; + // 1201 = errors detected. 1101 = test passed (no errors), which + // must NOT count as a memory error (previously any ID counted, + // turning a clean test into a false warning). + if (rec.Id == 1201) + { + diagCount++; + counted = true; + } } - if (rec.TimeCreated.HasValue && (lastError is null || rec.TimeCreated.Value > lastError)) + // Only advance lastError for records that actually count as errors. + if (counted && rec.TimeCreated.HasValue && (lastError is null || rec.TimeCreated.Value > lastError)) lastError = rec.TimeCreated.Value; } } diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj index d684861..ed6cb9b 100644 --- a/SysManager/SysManager/SysManager.csproj +++ b/SysManager/SysManager/SysManager.csproj @@ -10,9 +10,9 @@ SysManager true NU1603;NU1701 - 1.20.39 - 1.20.39.0 - 1.20.39.0 + 1.20.40 + 1.20.40.0 + 1.20.40.0 SysManager SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup. https://github.com/laurentiu021/SystemManager