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
8 changes: 7 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,10 @@ Key services:
- `DiskHealthService` — pulls SMART data through WMI. Its verdict is the sentence for
`DiskHealthReport.Finding`, the first of Windows' failing or warning status, 90% wear, 70 °C or I/O errors, and
`DiskHealthReport.Condition` (Fine, Watch, Failing, Unknown) follows the same finding, so every tab judges a drive
alike (#2866). `HealthPercent` stays the life-left gauge on System Health's card.
alike (#2866). `HealthPercent` stays the life-left gauge on System Health's card. `ReadAsync` returns a
`DiskHealthRead`, the drives and the fault that cut the read short, for a caller that keeps what it reads;
`CollectAsync` gives the same drives without saying. A run of failed reads is logged once, and an internal
constructor takes the read itself, so a test can make one fail (#2782).
- `MemoryTestService` — scans WHEA / MemoryDiagnostics events. A log it cannot read is thrown,
never returned as zero errors, so the Dashboard and System Health can say the check did not run.
`Summarize` counts the records by `MemoryEvents`' IDs, and stops at the first one older than 30 days.
Expand Down Expand Up @@ -2001,6 +2004,9 @@ Key services:
hand it stand-ins; it leaves out sensors that are not the chip's temperature (Intel's Distance to
TjMax, an offset Zen Tctl) and does not update storage when storage is not asked for. It does not
poll: the Dashboard asks every 2 s while it is visible, and the Resource History sampler every 10 s.
A drive LibreHardwareMonitor cannot name is named from `DiskHealthService`'s list. It keeps those names
for the session and the drive temperatures it reads without elevation for 30 s, each only from a read
Windows answered in full (#2782).
- `ActivityLogService` — persists the last 200 user actions (60 before Recent Changes read them over as
long as 90 days, #1507) to a JSON file for the Dashboard's recent-activity card and Recent Changes: 22
kinds of action, from 29 call sites. The six destructive operations (deep cleanup, browser clean,
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ That paragraph is not decoration: the release workflow copies each entry verbati
the GitHub release body and the announcement discussion, so it is the first thing a
prospective user reads. CI fails a pull request whose newest entry is missing it.

## [1.136.104] - 2026-11-03

**The Dashboard names the drives on its temperature card again after a disk read fails, and System Health says when it could not read the drives.**

### Fixed
- When a read of the drives failed, which a WMI service still starting at logon is enough for, the Dashboard's
temperature card kept the failed read as if it were the drives. Run as administrator, a drive the sensors could
not name stayed "Drive 1" until SysManager was restarted; without administrator, the drive temperatures were gone
for 30 seconds. Only a read Windows answers in full is kept now, a failed one is tried again at the next refresh,
two seconds later, and a read that keeps failing is written to the log once rather than at every refresh. The
names were kept like this since 1.52.94, the temperatures since 1.75.21.
- Run as administrator, the temperature card asked Windows for the drive models in a query Windows does not accept,
at every refresh, and logged the refusal each time. It never named a drive, so it is gone, and the card names the
drives from the list System Health reads, as it already did. It was there since 1.17.0.
- System Health's Run SMART check said "Collected 0 disk report(s)." when Windows did not answer at all. It now says
the check failed, and why, and a read that stopped partway says so after the drives it did read.

## [1.136.103] - 2026-11-03

**Recent Changes leaves out a failed install on Windows in any language, and keeps updates with their own program.**
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,8 @@ Edit Windows environment variables without the cramped built-in dialog:
- OS / CPU / RAM / storage overview
- SMART data per disk: temperature, life remaining, power-on hours, read/write errors —
fullest when SysManager runs as administrator. A figure the drive does not report reads "—",
with an empty bar, rather than a value it never measured, and no errors reads 0
with an empty bar, rather than a value it never measured, and no errors reads 0. A check
Windows does not answer says it failed, and why, rather than that it found no drives
- Colour-coded verdict per drive. A drive Windows gives no health for, with nothing read from it,
says so instead of being called Healthy, and a failing drive's health gauge is red like its verdict
- A drive's temperature takes the same colours here and on the Dashboard: green to 50 °C, blue while
Expand Down Expand Up @@ -2339,7 +2340,9 @@ offers, "rate us" prompts:
visible. Without administrator rights only the NVIDIA graphics card and the disks' own (SMART)
temperatures are available, and the card says so. The processor and motherboard temperatures
also need the PawnIO driver, which SysManager does not install: without it the processor shows
"Requires driver", as administrator too, and the card says why
"Requires driver", as administrator too, and the card says why. A drive the sensors leave
unnamed takes its name from the drive list System Health reads, and a read of that list that
Windows does not answer is tried again at the next refresh rather than kept
- **Recent Activity** — the five most recent things SysManager changed on this PC, with how long
ago, kept current while the Dashboard is shown: cleanups, deletes, uninstalls, the Dashboard's Update All Apps, privacy and DNS changes,
restore points, shredded files, game mode, and what Undo Changes put back. Not every tab writes
Expand Down

This file was deleted.

40 changes: 40 additions & 0 deletions SysManager/SysManager.Tests/ArchitectureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18457,6 +18457,46 @@ public void EveryManagementObjectSearcher_GoesThroughTheSharedWmiHelper()
+ "so that read has no time limit (#2845):\n " + string.Join("\n ", offenders));
}

/// <summary>A sort clause, which WQL does not have.</summary>
[GeneratedRegex(@"\bORDER\s+BY\b")]
private static partial Regex SortClause();

/// <summary>The start of a WMI query written in a string literal.</summary>
[GeneratedRegex(@"""SELECT\s")]
private static partial Regex WmiQueryLiteral();

/// <summary>
/// No WMI query in the app sorts what it reads with ORDER BY.
/// </summary>
/// <remarks>
/// WQL has no ORDER BY, and WMI does not skip the clause: it refuses the whole query as invalid.
/// <c>TemperatureService</c> asked for the disk models <c>ORDER BY Index</c> from #595 on, so the query failed on
/// every elevated Dashboard poll, every 2 seconds and with a log line each time, and the names it was there for
/// were never once read (#2782). Nothing else in the app is SQL, so the clause anywhere outside a comment is a
/// WMI query that cannot run; one that needs an order sorts the results itself. Matched in capitals, as every
/// query here is written, so a sentence that happens to say "in order by size" is not taken for one.
/// </remarks>
[Fact]
public void NoWmiQuery_SortsWithOrderBy()
{
var app = TestPaths.AppProject();
var queries = 0;
var offenders = new List<string>();
foreach (var path in SourceFilesUnder(app, ".cs").OrderBy(p => p, StringComparer.Ordinal))
{
var code = WithoutComments(File.ReadAllText(path));
queries += WmiQueryLiteral().Count(code);
if (SortClause().IsMatch(code)) offenders.Add(Path.GetRelativePath(app, path));
}

// Floor: 25 queries when this was written. Far fewer means the files stopped being found, and then "no query
// sorts" holds of nothing.
Assert.True(queries >= 20, $"only {queries} WMI queries were read in the app, so this checked almost none.");
Assert.True(offenders.Count == 0,
"these files sort a WMI query with ORDER BY, which WMI refuses as an invalid query (#2782):\n "
+ string.Join("\n ", offenders));
}

/// <summary>
/// The three values the 300 ms snapshot poll refreshes must come from syscalls, never from WMI.
/// </summary>
Expand Down
59 changes: 59 additions & 0 deletions SysManager/SysManager.Tests/DiskHealthServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,65 @@ public void ApplyVerdict_TempAt69_StillHealthy()
Assert.Contains("Healthy", r.Verdict);
}

// ---------- a read that fails, and its log (#2782) ----------

private const string Unavailable = "The RPC server is unavailable.";
private static readonly DiskHealthRead Worked =
new([new DiskHealthReport { FriendlyName = "Samsung SSD 990 PRO 2TB" }]);
private static readonly DiskHealthRead Failed = new([], Unavailable);

[Fact]
public void ARead_IsComplete_OnlyWithNoFault()
{
Assert.True(Worked.Complete);
Assert.False(Failed.Complete);
Assert.False(new DiskHealthRead([], "").Complete);
}

[Fact]
public async Task ReadAsync_SaysTheReadFailed_AndCollectAsync_StillGivesWhatWasRead()
{
// CollectAsync's callers judge each drive they are given and take none as not checked, so for them a read cut
// short is still the drives it read. ReadAsync is for a caller that keeps what it reads.
var disk = new DiskHealthReport { FriendlyName = "WDC WD40EZAZ-00SF3B0" };
var service = new DiskHealthService(() => new DiskHealthRead([disk], "Time out"), _ => { });

var read = await service.ReadAsync();
var collected = await service.CollectAsync();

Assert.False(read.Complete);
Assert.Equal("Time out", read.Fault);
Assert.Same(disk, Assert.Single(read.Disks));
Assert.Same(disk, Assert.Single(collected));
}

[Fact]
public async Task ARunOfFailedReads_IsLoggedOnce_AndAgainAfterAReadThatWorked()
{
// The Dashboard reads every 2 seconds. A fault that lasts was a log line each time, which pushes the history a
// bug report needs out of the rotating log.
var reads = new Queue<DiskHealthRead>([Failed, Failed, Failed, Worked, Failed]);
List<string> logged = [];
var service = new DiskHealthService(reads.Dequeue, logged.Add);

for (var read = 0; read < 5; read++)
await service.ReadAsync();

Assert.Equal([Unavailable, Unavailable], logged);
}

[Fact]
public async Task AReadThatWorked_IsNotLogged()
{
List<string> logged = [];
var service = new DiskHealthService(() => Worked, logged.Add);

await service.ReadAsync();
await service.CollectAsync();

Assert.Empty(logged);
}

// ---------- ToDouble / ToInt / ToLong helpers ----------

private static double? InvokeToDouble(object? o)
Expand Down
33 changes: 32 additions & 1 deletion SysManager/SysManager.Tests/SystemHealthViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ namespace SysManager.Tests;
[Collection("ProcessWideStatics")]
public class SystemHealthViewModelTests
{
private static SystemHealthViewModel NewVm(MemoryTestService? memTest = null, IPowerShellRunner? runner = null) => new(new SystemInfoService(), new DiskHealthService(), memTest ?? new MemoryTestService(), new FixedDriveService(), runner ?? new PowerShellRunner(), new BiosService());
private static SystemHealthViewModel NewVm(MemoryTestService? memTest = null, IPowerShellRunner? runner = null,
DiskHealthService? diskHealth = null) =>
new(new SystemInfoService(), diskHealth ?? new DiskHealthService(), memTest ?? new MemoryTestService(),
new FixedDriveService(), runner ?? new PowerShellRunner(), new BiosService());

// ---------- construction ----------

Expand Down Expand Up @@ -125,6 +128,34 @@ public async Task CheckMemoryErrors_WhenTheLogCannotBeRead_SaysItCouldNotCheck()
Assert.False(vm.IsBusy);
}

// ---------- the disk check that could not read the drives (#2782) ----------

[Fact]
public async Task CheckDiskHealth_WhenWindowsDoesNotAnswer_SaysTheReadFailed()
{
// The service reports a failed read in what it returns rather than by throwing, so the status read "Collected
// 0 disk report(s)." as if there were no drives, and the catch for a failure could never run.
var failing = new DiskHealthService(() => new DiskHealthRead([], "The RPC server is unavailable."), _ => { });
var vm = NewVm(diskHealth: failing);
await vm.InitializationComplete;

await vm.CheckDiskHealthCommand.ExecuteAsync(null);

Assert.Equal("CheckDiskHealth failed: The RPC server is unavailable.", vm.StatusMessage);
Assert.Empty(vm.DiskHealth);
Assert.False(vm.IsBusy);
}

[Theory]
[InlineData(null, "Collected 1 disk report(s).")]
[InlineData("Time out", "Collected 1 disk report(s), then the read failed: Time out")]
public void DescribeDiskRead_SaysWhenTheDrivesReadAreNotAllOfThem(string? fault, string expected)
{
var read = new DiskHealthRead([new DiskHealthReport { FriendlyName = "Samsung SSD 990 PRO 2TB" }], fault);

Assert.Equal(expected, SystemHealthViewModel.DescribeDiskRead(read));
}

// ---------- commands ----------

[Theory]
Expand Down
Loading