CmdPal: Harden performance monitor and enable crash recovery (#46541)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR has two parts:

1. Hardens the managed paths in the Performance Monitor extension to
catch everything we can.
1. Adds crash recovery for cases where something fails in a way we
cannot handle.

## Pictures? Pictures!

<img width="1060" height="591" alt="image"
src="https://github.com/user-attachments/assets/ee91c610-32eb-4117-b9b8-6bbc40b9b426"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #46522
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jiří Polášek
2026-03-28 00:39:26 +01:00
committed by GitHub
parent f686155d9b
commit 943c2a1ff5
15 changed files with 1338 additions and 184 deletions

View File

@@ -0,0 +1,163 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
using System.Text.Json.Nodes;
using Microsoft.CmdPal.Common.Helpers;
namespace Microsoft.CmdPal.Common.UnitTests.Helpers;
[TestClass]
public class ProviderLoadGuardTests
{
private readonly List<string> _temporaryDirectories = [];
[TestMethod]
public void EnterAndExit_PersistAndClearGuardedBlock()
{
var configDirectory = CreateTempDirectory();
var sentinelPath = GetSentinelPath(configDirectory);
var guard = new ProviderLoadGuard(configDirectory);
guard.Enter("Provider.Block", "Provider");
var state = ReadState(sentinelPath);
var entry = state["Provider.Block"] as JsonObject;
Assert.IsNotNull(entry);
Assert.AreEqual("Provider", entry[ExtensionLoadState.ProviderIdKey]?.GetValue<string>());
Assert.AreEqual(true, entry[ExtensionLoadState.LoadingKey]?.GetValue<bool>());
Assert.AreEqual(0, entry[ExtensionLoadState.CrashCountKey]?.GetValue<int>());
guard.Exit("Provider.Block");
Assert.IsFalse(File.Exists(sentinelPath));
}
[TestMethod]
public void Constructor_DisablesProviderAfterSecondConsecutiveCrash()
{
var configDirectory = CreateTempDirectory();
var sentinelPath = GetSentinelPath(configDirectory);
WriteState(
sentinelPath,
new JsonObject
{
["Provider.Block"] = CreateEntry("Provider", loading: true, crashCount: 1),
});
var guard = new ProviderLoadGuard(configDirectory);
Assert.IsTrue(guard.IsProviderDisabled("Provider"));
var state = ReadState(sentinelPath);
var entry = state["Provider.Block"] as JsonObject;
Assert.IsNotNull(entry);
Assert.AreEqual(false, entry[ExtensionLoadState.LoadingKey]?.GetValue<bool>());
Assert.AreEqual(2, entry[ExtensionLoadState.CrashCountKey]?.GetValue<int>());
}
[TestMethod]
public void ClearProvider_RemovesDisabledStateAndOnlyMatchingEntries()
{
var configDirectory = CreateTempDirectory();
var sentinelPath = GetSentinelPath(configDirectory);
WriteState(
sentinelPath,
new JsonObject
{
["CustomBlock"] = CreateEntry("Provider", loading: false, crashCount: 2),
["OtherProvider.Block"] = CreateEntry("OtherProvider", loading: false, crashCount: 2),
});
var guard = new ProviderLoadGuard(configDirectory);
Assert.IsTrue(guard.IsProviderDisabled("Provider"));
Assert.IsTrue(guard.IsProviderDisabled("OtherProvider"));
guard.ClearProvider("Provider");
Assert.IsFalse(guard.IsProviderDisabled("Provider"));
Assert.IsTrue(guard.IsProviderDisabled("OtherProvider"));
var state = ReadState(sentinelPath);
Assert.IsFalse(state.ContainsKey("CustomBlock"));
Assert.IsTrue(state.ContainsKey("OtherProvider.Block"));
}
[DataTestMethod]
[DataRow("")]
[DataRow("{")]
[DataRow("[]")]
public void Constructor_RecoversFromInvalidSentinelContents(string invalidSentinelContents)
{
var configDirectory = CreateTempDirectory();
var sentinelPath = GetSentinelPath(configDirectory);
File.WriteAllText(sentinelPath, invalidSentinelContents);
var guard = new ProviderLoadGuard(configDirectory);
Assert.IsFalse(guard.IsProviderDisabled("Provider"));
Assert.IsFalse(File.Exists(sentinelPath));
guard.Enter("Provider.Block", "Provider");
var state = ReadState(sentinelPath);
Assert.IsTrue(state.ContainsKey("Provider.Block"));
}
[TestCleanup]
public void Cleanup()
{
foreach (var directory in _temporaryDirectories)
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, recursive: true);
}
}
}
private string CreateTempDirectory()
{
var directory = Path.Combine(Path.GetTempPath(), "CmdPal.ProviderLoadGuardTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
_temporaryDirectories.Add(directory);
return directory;
}
private static JsonObject CreateEntry(string providerId, bool loading, int crashCount)
{
return new JsonObject
{
[ExtensionLoadState.ProviderIdKey] = providerId,
[ExtensionLoadState.LoadingKey] = loading,
[ExtensionLoadState.CrashCountKey] = crashCount,
};
}
private static string GetSentinelPath(string configDirectory)
{
return Path.Combine(configDirectory, ExtensionLoadState.SentinelFileName);
}
private static JsonObject ReadState(string sentinelPath)
{
if (JsonNode.Parse(File.ReadAllText(sentinelPath)) is JsonObject state)
{
return state;
}
throw new AssertFailedException($"Sentinel state at '{sentinelPath}' was not a JSON object.");
}
private static void WriteState(string sentinelPath, JsonObject state)
{
Directory.CreateDirectory(Path.GetDirectoryName(sentinelPath)!);
File.WriteAllText(sentinelPath, state.ToJsonString());
}
}