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,45 @@
// 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.
namespace Microsoft.CmdPal.Common;
/// <summary>
/// Shared constants for the extension load sentinel file used by
/// <c>ProviderLoadGuard</c> and provider-specific crash sentinels to
/// coordinate crash detection across process lifetimes.
/// </summary>
public static class ExtensionLoadState
{
/// <summary>
/// Name of the sentinel JSON file written to the config directory.
/// Both the app-level guard and individual extension sentinels must
/// read and write the same file for crash detection to work.
/// </summary>
public const string SentinelFileName = "extensionLoadState.json";
/// <summary>
/// JSON property name storing the owning provider id for a guarded block.
/// </summary>
public const string ProviderIdKey = "providerId";
/// <summary>
/// JSON property name indicating a guarded block was active when the
/// process exited.
/// </summary>
public const string LoadingKey = "loading";
/// <summary>
/// JSON property name storing the consecutive crash count for a guarded
/// block.
/// </summary>
public const string CrashCountKey = "crashCount";
/// <summary>
/// Shared lock that must be held around every read-modify-write cycle
/// on the sentinel file. Both <c>ProviderLoadGuard</c> and
/// provider-specific crash sentinels run in the same process and would
/// otherwise race on the file, silently dropping entries.
/// </summary>
public static readonly object SentinelFileLock = new();
}

View File

@@ -0,0 +1,241 @@
// 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.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Tracks guarded provider blocks in the shared extension-load sentinel file so
/// callers can fail closed after repeated native crashes that bypass managed
/// exception handling.
/// </summary>
public sealed class ProviderCrashSentinel
{
private readonly string _providerId;
private readonly Lock _sentinelLock = new();
private readonly HashSet<string> _completedBlocks = [];
private readonly HashSet<string> _activeBlocks = [];
public ProviderCrashSentinel(string providerId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(providerId);
_providerId = providerId;
}
public bool BeginBlock(string blockSuffix)
{
var blockId = CreateBlockId(blockSuffix);
lock (_sentinelLock)
{
if (_completedBlocks.Contains(blockId) || !_activeBlocks.Add(blockId))
{
return false;
}
UpdateState(
state =>
{
var entry = GetOrCreateEntry(state, blockId);
entry[ExtensionLoadState.LoadingKey] = true;
});
return true;
}
}
public void CompleteBlock(string blockSuffix)
{
var blockId = CreateBlockId(blockSuffix);
lock (_sentinelLock)
{
if (!_activeBlocks.Remove(blockId))
{
return;
}
_completedBlocks.Add(blockId);
UpdateState(state => state.Remove(blockId));
}
}
public void CancelBlock(string blockSuffix)
{
var blockId = CreateBlockId(blockSuffix);
lock (_sentinelLock)
{
if (!_activeBlocks.Remove(blockId))
{
return;
}
UpdateState(state => state.Remove(blockId));
}
}
public void ClearProviderState()
{
lock (_sentinelLock)
{
_completedBlocks.RemoveWhere(blockId => blockId.StartsWith(_providerId + ".", StringComparison.Ordinal));
_activeBlocks.RemoveWhere(blockId => blockId.StartsWith(_providerId + ".", StringComparison.Ordinal));
UpdateState(
state =>
{
var keysToRemove = state
.Where(kvp => IsProviderEntry(kvp.Key, kvp.Value as JsonObject))
.Select(kvp => kvp.Key)
.ToArray();
foreach (var key in keysToRemove)
{
state.Remove(key);
}
});
}
}
private void UpdateState(Action<JsonObject> update)
{
try
{
lock (ExtensionLoadState.SentinelFileLock)
{
var sentinelPath = GetSentinelPath();
var state = LoadState(sentinelPath);
update(state);
SaveState(sentinelPath, state);
}
}
catch (Exception ex)
{
CoreLogger.LogError($"Failed to update crash sentinel state for provider '{_providerId}'.", ex);
}
}
private static JsonObject LoadState(string sentinelPath)
{
if (!File.Exists(sentinelPath))
{
return [];
}
try
{
var json = File.ReadAllText(sentinelPath);
if (string.IsNullOrWhiteSpace(json))
{
CoreLogger.LogWarning($"Crash sentinel state file '{sentinelPath}' was empty. Treating as empty state.");
DeleteInvalidStateFile(sentinelPath);
return [];
}
if (JsonNode.Parse(json) is JsonObject state)
{
return state;
}
CoreLogger.LogError($"Crash sentinel state file '{sentinelPath}' did not contain a JSON object. Treating as empty state.");
DeleteInvalidStateFile(sentinelPath);
}
catch (JsonException ex)
{
CoreLogger.LogError($"Failed to parse crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
DeleteInvalidStateFile(sentinelPath);
}
catch (IOException ex)
{
CoreLogger.LogError($"Failed to read crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
}
catch (UnauthorizedAccessException ex)
{
CoreLogger.LogError($"Access denied reading crash sentinel state from '{sentinelPath}'. Treating as empty state.", ex);
}
return [];
}
private static void DeleteInvalidStateFile(string sentinelPath)
{
try
{
File.Delete(sentinelPath);
}
catch (IOException ex)
{
CoreLogger.LogError($"Failed to delete invalid crash sentinel state file '{sentinelPath}'.", ex);
}
catch (UnauthorizedAccessException ex)
{
CoreLogger.LogError($"Access denied deleting invalid crash sentinel state file '{sentinelPath}'.", ex);
}
}
private static void SaveState(string sentinelPath, JsonObject state)
{
if (state.Count == 0)
{
if (File.Exists(sentinelPath))
{
File.Delete(sentinelPath);
}
return;
}
var directory = Path.GetDirectoryName(sentinelPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var tempPath = sentinelPath + ".tmp";
File.WriteAllText(tempPath, state.ToJsonString());
File.Move(tempPath, sentinelPath, overwrite: true);
}
private JsonObject GetOrCreateEntry(JsonObject state, string blockId)
{
if (state[blockId] is JsonObject existing)
{
existing[ExtensionLoadState.ProviderIdKey] = _providerId;
return existing;
}
var entry = new JsonObject
{
[ExtensionLoadState.ProviderIdKey] = _providerId,
[ExtensionLoadState.LoadingKey] = false,
[ExtensionLoadState.CrashCountKey] = 0,
};
state[blockId] = entry;
return entry;
}
private bool IsProviderEntry(string blockId, JsonObject? entry)
{
var providerId = entry?[ExtensionLoadState.ProviderIdKey]?.GetValue<string>();
if (string.Equals(providerId, _providerId, StringComparison.Ordinal))
{
return true;
}
return blockId.StartsWith(_providerId + ".", StringComparison.Ordinal);
}
private string CreateBlockId(string blockSuffix)
{
return $"{_providerId}.{blockSuffix}";
}
private static string GetSentinelPath()
{
return Path.Combine(Utilities.BaseSettingsPath("Microsoft.CmdPal"), ExtensionLoadState.SentinelFileName);
}
}

View File

@@ -0,0 +1,231 @@
// 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.Text.Json.Nodes;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Reads the shared provider crash sentinel file at startup, increments crash
/// counts for blocks left marked as active, and determines which providers
/// should be soft-disabled for the current session.
/// </summary>
public sealed class ProviderLoadGuard
{
private const int MaxConsecutiveCrashes = 2;
private readonly string _sentinelPath;
private readonly HashSet<string> _disabledProviders = [];
public ProviderLoadGuard(string configDirectory)
{
_sentinelPath = Path.Combine(configDirectory, ExtensionLoadState.SentinelFileName);
DetectCrashes();
}
/// <summary>
/// Returns true if the provider has been disabled due to repeated crashes
/// in one of its tracked guarded blocks.
/// </summary>
public bool IsProviderDisabled(string providerId) => _disabledProviders.Contains(providerId);
/// <summary>
/// Call immediately before attempting a guarded operation.
/// Marks the block as "loading" in the sentinel file so that a
/// subsequent native crash leaves evidence on disk.
/// </summary>
public void Enter(string blockId, string providerId)
{
UpdateState(state =>
{
var entry = GetOrCreateEntry(state, blockId, providerId);
entry[ExtensionLoadState.LoadingKey] = true;
});
}
/// <summary>
/// Call after a guarded operation succeeds or fails gracefully via managed
/// exception. Clears the loading flag and removes the block entry.
/// </summary>
public void Exit(string blockId)
{
UpdateState(state => state.Remove(blockId));
}
/// <summary>
/// Removes any persisted crash state for a provider so it can be retried
/// on the next launch.
/// </summary>
public void ClearProvider(string providerId)
{
_disabledProviders.Remove(providerId);
UpdateState(state =>
{
var keysToRemove = state
.Where(kvp => TryGetProviderId(kvp.Key, kvp.Value as JsonObject) == providerId)
.Select(kvp => kvp.Key)
.ToArray();
foreach (var key in keysToRemove)
{
state.Remove(key);
}
});
}
private void DetectCrashes()
{
// Read the sentinel file once at startup to detect providers that
// crashed on the previous launch, then write back the updated state.
lock (ExtensionLoadState.SentinelFileLock)
{
var state = ReadState();
var keysToCheck = state.Select(kvp => kvp.Key).ToArray();
foreach (var key in keysToCheck)
{
if (state[key] is not JsonObject entry)
{
continue;
}
var providerId = TryGetProviderId(key, entry);
var wasLoading = entry[ExtensionLoadState.LoadingKey]?.GetValue<bool>() ?? false;
if (wasLoading)
{
// The guarded block was active when the process died.
var crashCount = (entry[ExtensionLoadState.CrashCountKey]?.GetValue<int>() ?? 0) + 1;
entry[ExtensionLoadState.CrashCountKey] = crashCount;
entry[ExtensionLoadState.LoadingKey] = false;
if (crashCount >= MaxConsecutiveCrashes)
{
_disabledProviders.Add(providerId);
CoreLogger.LogError($"Provider '{providerId}' disabled after {crashCount} consecutive crash(es) in guarded block '{key}'.");
}
else
{
CoreLogger.LogWarning($"Guarded block '{key}' for provider '{providerId}' crashed on previous launch (crash {crashCount}/{MaxConsecutiveCrashes}). Will retry.");
}
}
var currentCrashCount = entry[ExtensionLoadState.CrashCountKey]?.GetValue<int>() ?? 0;
if (currentCrashCount >= MaxConsecutiveCrashes)
{
// Persist disabled state from a previous session.
_disabledProviders.Add(providerId);
}
if (!(entry[ExtensionLoadState.LoadingKey]?.GetValue<bool>() ?? false) && currentCrashCount == 0)
{
state.Remove(key);
}
}
WriteState(state);
}
}
/// <summary>
/// Reads the sentinel file, applies a mutation, and writes it back
/// under <see cref="ExtensionLoadState.SentinelFileLock"/> to prevent
/// concurrent writers from clobbering each other's entries.
/// </summary>
private void UpdateState(Action<JsonObject> mutate)
{
try
{
lock (ExtensionLoadState.SentinelFileLock)
{
var state = ReadState();
mutate(state);
WriteState(state);
}
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to update extension load sentinel file.", ex);
}
}
private static JsonObject GetOrCreateEntry(JsonObject state, string blockId, string providerId)
{
if (state[blockId] is JsonObject existing)
{
existing[ExtensionLoadState.ProviderIdKey] = providerId;
return existing;
}
var entry = new JsonObject
{
[ExtensionLoadState.ProviderIdKey] = providerId,
[ExtensionLoadState.LoadingKey] = false,
[ExtensionLoadState.CrashCountKey] = 0,
};
state[blockId] = entry;
return entry;
}
private static string TryGetProviderId(string blockId, JsonObject? entry)
{
var providerId = entry?[ExtensionLoadState.ProviderIdKey]?.GetValue<string>();
if (!string.IsNullOrWhiteSpace(providerId))
{
return providerId;
}
var separatorIndex = blockId.IndexOf('.');
return separatorIndex > 0 ? blockId[..separatorIndex] : blockId;
}
private JsonObject ReadState()
{
try
{
if (File.Exists(_sentinelPath))
{
var json = File.ReadAllText(_sentinelPath);
return JsonNode.Parse(json)?.AsObject() ?? [];
}
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to read extension load sentinel file.", ex);
}
return [];
}
private void WriteState(JsonObject state)
{
try
{
if (state.Count == 0)
{
if (File.Exists(_sentinelPath))
{
File.Delete(_sentinelPath);
}
return;
}
var directory = Path.GetDirectoryName(_sentinelPath);
if (directory != null)
{
Directory.CreateDirectory(directory);
}
var tempPath = _sentinelPath + ".tmp";
File.WriteAllText(tempPath, state.ToJsonString());
File.Move(tempPath, _sentinelPath, overwrite: true);
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to write extension load sentinel file.", ex);
}
}
}

View File

@@ -125,7 +125,7 @@ public partial class App : Application, IDisposable
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext()); services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
AddBuiltInCommands(services); AddBuiltInCommands(services, appInfoService.ConfigDirectory);
AddCoreServices(services, appInfoService); AddCoreServices(services, appInfoService);
@@ -134,8 +134,10 @@ public partial class App : Application, IDisposable
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }
private static void AddBuiltInCommands(ServiceCollection services) private static void AddBuiltInCommands(ServiceCollection services, string configDirectory)
{ {
var providerLoadGuard = new ProviderLoadGuard(configDirectory);
// Built-in Commands. Order matters - this is the order they'll be presented by default. // Built-in Commands. Order matters - this is the order they'll be presented by default.
var allApps = new AllAppsCommandProvider(); var allApps = new AllAppsCommandProvider();
var files = new IndexerCommandsProvider(); var files = new IndexerCommandsProvider();
@@ -166,8 +168,7 @@ public partial class App : Application, IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("Couldn't load winget"); Logger.LogError("Couldn't load winget", ex);
Logger.LogError(ex.ToString());
} }
services.AddSingleton<ICommandProvider, WindowsTerminalCommandsProvider>(); services.AddSingleton<ICommandProvider, WindowsTerminalCommandsProvider>();
@@ -178,7 +179,37 @@ public partial class App : Application, IDisposable
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>(); services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>(); services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>(); services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
var performanceMonitorSoftDisabled = providerLoadGuard.IsProviderDisabled(PerformanceMonitorCommandsProvider.ProviderIdValue);
if (performanceMonitorSoftDisabled)
{
Logger.LogWarning("Performance monitor is temporarily disabled after repeated crashes. Loading placeholder pages instead of activating performance counters.");
}
if (!performanceMonitorSoftDisabled)
{
providerLoadGuard.Enter(PerformanceMonitorCommandsProvider.ProviderLoadGuardBlockId, PerformanceMonitorCommandsProvider.ProviderIdValue);
}
try
{
var performanceMonitor = new PerformanceMonitorCommandsProvider(performanceMonitorSoftDisabled);
services.AddSingleton<ICommandProvider>(performanceMonitor);
if (!performanceMonitorSoftDisabled)
{
providerLoadGuard.Exit(PerformanceMonitorCommandsProvider.ProviderLoadGuardBlockId);
}
}
catch (Exception ex)
{
if (!performanceMonitorSoftDisabled)
{
providerLoadGuard.Exit(PerformanceMonitorCommandsProvider.ProviderLoadGuardBlockId);
}
Logger.LogError("Couldn't load performance monitor", ex);
}
} }
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue) private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)

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());
}
}

View File

@@ -9,13 +9,17 @@ using System.Linq;
namespace CoreWidgetProvider.Helpers; namespace CoreWidgetProvider.Helpers;
internal sealed partial class CPUStats : IDisposable internal sealed partial class CPUStats : PerformanceCounterSourceBase, IDisposable
{ {
// CPU counters // CPU counters
private readonly PerformanceCounter _procPerf = new("Processor Information", "% Processor Utility", "_Total"); private readonly PerformanceCounter? _procPerf;
private readonly PerformanceCounter _procPerformance = new("Processor Information", "% Processor Performance", "_Total"); private readonly PerformanceCounter? _procPerformance;
private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total"); private readonly PerformanceCounter? _procFrequency;
private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new(); private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new();
private bool _processCountersInitialized;
private bool _cpuCounterReadFailureLogged;
private bool _processCounterEnumerationFailureLogged;
private bool _processCounterReadFailureLogged;
internal sealed class ProcessStats internal sealed class ProcessStats
{ {
@@ -42,25 +46,62 @@ internal sealed partial class CPUStats : IDisposable
new ProcessStats() new ProcessStats()
]; ];
InitCPUPerfCounters(); _procPerf = CreatePerformanceCounter("Processor Information", "% Processor Utility", "_Total");
_procPerformance = CreatePerformanceCounter("Processor Information", "% Processor Performance", "_Total");
_procFrequency = CreatePerformanceCounter("Processor Information", "Processor Frequency", "_Total");
} }
private void InitCPUPerfCounters() private void EnsureCPUProcessCountersInitialized()
{
if (_processCountersInitialized)
{
return;
}
_processCountersInitialized = true;
try
{ {
var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0); var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
foreach (var process in allProcesses) foreach (var process in allProcesses)
{ {
_cpuCounters.Add(process, new PerformanceCounter("Process", "% Processor Time", process.ProcessName, true)); try
{
var counter = CreatePerformanceCounter("Process", "% Processor Time", process.ProcessName, logFailure: false);
if (counter is not null)
{
_cpuCounters.Add(process, counter);
}
}
catch (Exception)
{
// Skip processes whose counters cannot be created.
}
}
}
catch (Exception ex)
{
LogFailureOnce(ref _processCounterEnumerationFailureLogged, "Failed to initialize CPU process performance counters.", ex);
} }
} }
public void GetData(bool includeTopProcesses) public void GetData(bool includeTopProcesses)
{
try
{ {
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
if (_procPerf is not null)
{
CpuUsage = _procPerf.NextValue() / 100; CpuUsage = _procPerf.NextValue() / 100;
}
var usageMs = timer.ElapsedMilliseconds; var usageMs = timer.ElapsedMilliseconds;
if (_procFrequency is not null && _procPerformance is not null)
{
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100); CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
}
var speedMs = timer.ElapsedMilliseconds - usageMs; var speedMs = timer.ElapsedMilliseconds - usageMs;
lock (CpuChartValues) lock (CpuChartValues)
{ {
@@ -73,7 +114,10 @@ internal sealed partial class CPUStats : IDisposable
if (includeTopProcesses) if (includeTopProcesses)
{ {
foreach (var processCounter in _cpuCounters) EnsureCPUProcessCountersInitialized();
var countersToRemove = new List<Process>();
foreach (var processCounter in _cpuCounters.ToArray())
{ {
try try
{ {
@@ -82,12 +126,19 @@ internal sealed partial class CPUStats : IDisposable
} }
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
// _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters."); countersToRemove.Add(processCounter.Key);
_cpuCounters.Remove(processCounter.Key);
} }
catch (Exception) catch (Exception ex)
{ {
// _log.Error(ex, "Error going through process counters."); LogFailureOnce(ref _processCounterReadFailureLogged, "Failed while reading CPU process performance counters.", ex);
}
}
foreach (var process in countersToRemove)
{
if (_cpuCounters.Remove(process, out var counter))
{
counter.Dispose();
} }
} }
@@ -106,6 +157,11 @@ internal sealed partial class CPUStats : IDisposable
// CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]"); // CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]");
} }
catch (Exception ex)
{
LogFailureOnce(ref _cpuCounterReadFailureLogged, "Failed while reading CPU performance counters.", ex);
}
}
internal string CreateCPUImageUrl() internal string CreateCPUImageUrl()
{ {
@@ -134,9 +190,9 @@ internal sealed partial class CPUStats : IDisposable
public void Dispose() public void Dispose()
{ {
_procPerf.Dispose(); _procPerf?.Dispose();
_procPerformance.Dispose(); _procPerformance?.Dispose();
_procFrequency.Dispose(); _procFrequency?.Dispose();
foreach (var counter in _cpuCounters.Values) foreach (var counter in _cpuCounters.Values)
{ {

View File

@@ -3,22 +3,23 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using Microsoft.CmdPal.Ext.PerformanceMonitor;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
namespace CoreWidgetProvider.Helpers; namespace CoreWidgetProvider.Helpers;
internal sealed partial class DataManager : IDisposable internal sealed partial class DataManager : IDisposable
{ {
private readonly SystemData _systemData; private readonly SystemData _systemData = SystemData.Shared;
private readonly DataType _dataType; private readonly DataType _dataType;
private readonly Timer _updateTimer; private readonly Timer _updateTimer;
private readonly Action _updateAction; private readonly Action _updateAction;
private bool _updateFailureLogged;
private const int OneSecondInMilliseconds = 1000; private const int OneSecondInMilliseconds = 1000;
public DataManager(DataType type, Action updateWidget) public DataManager(DataType type, Action updateWidget)
{ {
_systemData = new SystemData();
_updateAction = updateWidget; _updateAction = updateWidget;
_dataType = type; _dataType = type;
@@ -30,37 +31,42 @@ internal sealed partial class DataManager : IDisposable
private void GetMemoryData() private void GetMemoryData()
{ {
lock (SystemData.MemStats) lock (_systemData.MemoryStats)
{ {
SystemData.MemStats.GetData(); _systemData.MemoryStats.GetData();
} }
} }
private void GetNetworkData() private void GetNetworkData()
{ {
lock (SystemData.NetStats) lock (_systemData.NetworkStats)
{ {
SystemData.NetStats.GetData(); _systemData.NetworkStats.GetData();
} }
} }
private void GetGPUData() private void GetGPUData()
{ {
lock (SystemData.GPUStats) lock (_systemData.GPUStats)
{ {
SystemData.GPUStats.GetData(); _systemData.GPUStats.GetData();
} }
} }
private void GetCPUData(bool includeTopProcesses) private void GetCPUData(bool includeTopProcesses)
{ {
lock (SystemData.CpuStats) lock (_systemData.CpuStats)
{ {
SystemData.CpuStats.GetData(includeTopProcesses); _systemData.CpuStats.GetData(includeTopProcesses);
} }
} }
private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
var firstUpdateBlockSuffix = GetFirstUpdateBlockSuffix();
var isTracked = firstUpdateBlockSuffix is not null && PerformanceMonitorCommandsProvider.CrashSentinel.BeginBlock(firstUpdateBlockSuffix);
try
{ {
switch (_dataType) switch (_dataType)
{ {
@@ -94,38 +100,71 @@ internal sealed partial class DataManager : IDisposable
} }
} }
if (isTracked)
{
PerformanceMonitorCommandsProvider.CrashSentinel.CompleteBlock(firstUpdateBlockSuffix!);
}
_updateAction?.Invoke(); _updateAction?.Invoke();
} }
catch (Exception ex)
{
if (isTracked)
{
PerformanceMonitorCommandsProvider.CrashSentinel.CancelBlock(firstUpdateBlockSuffix!);
}
_updateTimer.Stop();
if (!_updateFailureLogged)
{
_updateFailureLogged = true;
Microsoft.CmdPal.Common.CoreLogger.LogError($"Unexpected exception while updating performance monitor data for {_dataType}. Timer stopped.", ex);
}
}
}
private string? GetFirstUpdateBlockSuffix()
{
return _dataType switch
{
DataType.CPU => "CPU.FirstUpdate",
DataType.CpuWithTopProcesses => "CPU.FirstUpdate",
DataType.GPU => "GPU.FirstUpdate",
DataType.Memory => "Memory.FirstUpdate",
DataType.Network => "Network.FirstUpdate",
_ => null,
};
}
internal MemoryStats GetMemoryStats() internal MemoryStats GetMemoryStats()
{ {
lock (SystemData.MemStats) lock (_systemData.MemoryStats)
{ {
return SystemData.MemStats; return _systemData.MemoryStats;
} }
} }
internal NetworkStats GetNetworkStats() internal NetworkStats GetNetworkStats()
{ {
lock (SystemData.NetStats) lock (_systemData.NetworkStats)
{ {
return SystemData.NetStats; return _systemData.NetworkStats;
} }
} }
internal GPUStats GetGPUStats() internal GPUStats GetGPUStats()
{ {
lock (SystemData.GPUStats) lock (_systemData.GPUStats)
{ {
return SystemData.GPUStats; return _systemData.GPUStats;
} }
} }
internal CPUStats GetCPUStats() internal CPUStats GetCPUStats()
{ {
lock (SystemData.CpuStats) lock (_systemData.CpuStats)
{ {
return SystemData.CpuStats; return _systemData.CpuStats;
} }
} }
@@ -141,7 +180,6 @@ internal sealed partial class DataManager : IDisposable
public void Dispose() public void Dispose()
{ {
_systemData.Dispose();
_updateTimer.Dispose(); _updateTimer.Dispose();
} }
} }

View File

@@ -10,7 +10,7 @@ using System.Text;
namespace CoreWidgetProvider.Helpers; namespace CoreWidgetProvider.Helpers;
internal sealed partial class GPUStats : IDisposable internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposable
{ {
// Performance counter category & counter names // Performance counter category & counter names
private const string GpuEngineCategoryName = "GPU Engine"; private const string GpuEngineCategoryName = "GPU Engine";
@@ -32,7 +32,7 @@ internal sealed partial class GPUStats : IDisposable
private const string TemperatureUnavailable = "--"; private const string TemperatureUnavailable = "--";
// Batch read via category - single kernel transition per tick // Batch read via category - single kernel transition per tick
private readonly PerformanceCounterCategory _gpuEngineCategory = new(GpuEngineCategoryName); private readonly PerformanceCounterCategory? _gpuEngineCategory;
// Discovered physical GPU IDs // Discovered physical GPU IDs
private readonly HashSet<int> _knownPhysIds = []; private readonly HashSet<int> _knownPhysIds = [];
@@ -41,6 +41,8 @@ internal sealed partial class GPUStats : IDisposable
// Previous raw samples for computing cooked (delta-based) values // Previous raw samples for computing cooked (delta-based) values
private Dictionary<string, CounterSample> _previousSamples = []; private Dictionary<string, CounterSample> _previousSamples = [];
private bool _gpuEnumerationFailureLogged;
private bool _gpuReadFailureLogged;
public sealed class Data public sealed class Data
{ {
@@ -57,11 +59,20 @@ internal sealed partial class GPUStats : IDisposable
public GPUStats() public GPUStats()
{ {
_gpuEngineCategory = CreatePerformanceCounterCategory(GpuEngineCategoryName);
GetGPUPerfCounters(); GetGPUPerfCounters();
LoadGPUsFromCounters(); LoadGPUsFromCounters();
} }
public void GetGPUPerfCounters() public void GetGPUPerfCounters()
{
if (_gpuEngineCategory is null)
{
return;
}
try
{ {
// There are really 4 different things we should be tracking the usage // There are really 4 different things we should be tracking the usage
// of. Similar to how the instance name ends with `3D`, the following // of. Similar to how the instance name ends with `3D`, the following
@@ -99,6 +110,11 @@ internal sealed partial class GPUStats : IDisposable
} }
} }
} }
catch (Exception ex)
{
LogFailureOnce(ref _gpuEnumerationFailureLogged, "Failed while enumerating GPU performance counters.", ex);
}
}
public void LoadGPUsFromCounters() public void LoadGPUsFromCounters()
{ {
@@ -119,6 +135,11 @@ internal sealed partial class GPUStats : IDisposable
public void GetData() public void GetData()
{ {
if (_gpuEngineCategory is null)
{
return;
}
try try
{ {
// Single batch read - one kernel transition for ALL GPU Engine instances // Single batch read - one kernel transition for ALL GPU Engine instances
@@ -183,9 +204,9 @@ internal sealed partial class GPUStats : IDisposable
} }
} }
} }
catch (Exception) catch (Exception ex)
{ {
// Ignore errors from ReadCategory (e.g., category not available). LogFailureOnce(ref _gpuReadFailureLogged, "Failed while reading GPU performance counters.", ex);
} }
} }

View File

@@ -10,13 +10,14 @@ using Windows.Win32;
namespace CoreWidgetProvider.Helpers; namespace CoreWidgetProvider.Helpers;
internal sealed partial class MemoryStats : IDisposable internal sealed partial class MemoryStats : PerformanceCounterSourceBase, IDisposable
{ {
private readonly PerformanceCounter _memCommitted = new("Memory", "Committed Bytes", string.Empty); private readonly PerformanceCounter? _memCommitted;
private readonly PerformanceCounter _memCached = new("Memory", "Cache Bytes", string.Empty); private readonly PerformanceCounter? _memCached;
private readonly PerformanceCounter _memCommittedLimit = new("Memory", "Commit Limit", string.Empty); private readonly PerformanceCounter? _memCommittedLimit;
private readonly PerformanceCounter _memPoolPaged = new("Memory", "Pool Paged Bytes", string.Empty); private readonly PerformanceCounter? _memPoolPaged;
private readonly PerformanceCounter _memPoolNonPaged = new("Memory", "Pool Nonpaged Bytes", string.Empty); private readonly PerformanceCounter? _memPoolNonPaged;
private bool _memoryCounterReadFailureLogged;
public float MemUsage public float MemUsage
{ {
@@ -60,6 +61,15 @@ internal sealed partial class MemoryStats : IDisposable
public List<float> MemChartValues { get; set; } = new(); public List<float> MemChartValues { get; set; } = new();
public MemoryStats()
{
_memCommitted = CreatePerformanceCounter("Memory", "Committed Bytes");
_memCached = CreatePerformanceCounter("Memory", "Cache Bytes");
_memCommittedLimit = CreatePerformanceCounter("Memory", "Commit Limit");
_memPoolPaged = CreatePerformanceCounter("Memory", "Pool Paged Bytes");
_memPoolNonPaged = CreatePerformanceCounter("Memory", "Pool Nonpaged Bytes");
}
public void GetData() public void GetData()
{ {
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default; Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
@@ -77,11 +87,18 @@ internal sealed partial class MemoryStats : IDisposable
} }
} }
MemCached = (ulong)_memCached.NextValue(); try
MemCommitted = (ulong)_memCommitted.NextValue(); {
MemCommitLimit = (ulong)_memCommittedLimit.NextValue(); MemCached = (ulong)(_memCached?.NextValue() ?? 0);
MemPagedPool = (ulong)_memPoolPaged.NextValue(); MemCommitted = (ulong)(_memCommitted?.NextValue() ?? 0);
MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue(); MemCommitLimit = (ulong)(_memCommittedLimit?.NextValue() ?? 0);
MemPagedPool = (ulong)(_memPoolPaged?.NextValue() ?? 0);
MemNonPagedPool = (ulong)(_memPoolNonPaged?.NextValue() ?? 0);
}
catch (Exception ex)
{
LogFailureOnce(ref _memoryCounterReadFailureLogged, "Failed while reading memory performance counters.", ex);
}
} }
public string CreateMemImageUrl() public string CreateMemImageUrl()
@@ -91,10 +108,10 @@ internal sealed partial class MemoryStats : IDisposable
public void Dispose() public void Dispose()
{ {
_memCommitted.Dispose(); _memCommitted?.Dispose();
_memCached.Dispose(); _memCached?.Dispose();
_memCommittedLimit.Dispose(); _memCommittedLimit?.Dispose();
_memPoolPaged.Dispose(); _memPoolPaged?.Dispose();
_memPoolNonPaged.Dispose(); _memPoolNonPaged?.Dispose();
} }
} }

View File

@@ -6,12 +6,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.CmdPal.Common;
namespace CoreWidgetProvider.Helpers; namespace CoreWidgetProvider.Helpers;
internal sealed partial class NetworkStats : IDisposable internal sealed partial class NetworkStats : PerformanceCounterSourceBase, IDisposable
{ {
private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new(); private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new();
private bool _networkCounterReadFailureLogged;
private Dictionary<string, Data> NetworkUsages { get; set; } = new(); private Dictionary<string, Data> NetworkUsages { get; set; } = new();
@@ -42,18 +44,45 @@ internal sealed partial class NetworkStats : IDisposable
private void InitNetworkPerfCounters() private void InitNetworkPerfCounters()
{ {
var perfCounterCategory = new PerformanceCounterCategory("Network Interface"); try
{
var perfCounterCategory = CreatePerformanceCounterCategory("Network Interface");
if (perfCounterCategory is null)
{
return;
}
var instanceNames = perfCounterCategory.GetInstanceNames(); var instanceNames = perfCounterCategory.GetInstanceNames();
foreach (var instanceName in instanceNames) foreach (var instanceName in instanceNames)
{ {
var instanceCounters = new List<PerformanceCounter>(); try
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName)); {
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName)); var bytesSent = CreatePerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName, logFailure: false);
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName)); var bytesReceived = CreatePerformanceCounter("Network Interface", "Bytes Received/sec", instanceName, logFailure: false);
var currentBandwidth = CreatePerformanceCounter("Network Interface", "Current Bandwidth", instanceName, logFailure: false);
if (bytesSent is null || bytesReceived is null || currentBandwidth is null)
{
bytesSent?.Dispose();
bytesReceived?.Dispose();
currentBandwidth?.Dispose();
continue;
}
var instanceCounters = new List<PerformanceCounter> { bytesSent, bytesReceived, currentBandwidth };
_networkCounters.Add(instanceName, instanceCounters); _networkCounters.Add(instanceName, instanceCounters);
NetChartValues.Add(instanceName, new List<float>()); NetChartValues.Add(instanceName, new List<float>());
NetworkUsages.Add(instanceName, new Data()); NetworkUsages.Add(instanceName, new Data());
} }
catch (Exception)
{
// Skip interfaces whose counters cannot be initialized.
}
}
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to initialize network performance counters.", ex);
}
} }
public void GetData() public void GetData()
@@ -88,9 +117,9 @@ internal sealed partial class NetworkStats : IDisposable
maxUsage = usage; maxUsage = usage;
} }
} }
catch (Exception) catch (Exception ex)
{ {
// Log.Error(ex, "Error getting network data."); LogFailureOnce(ref _networkCounterReadFailureLogged, "Failed while reading network performance counters.", ex);
} }
} }
} }

View File

@@ -0,0 +1,66 @@
// 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;
using System.Diagnostics;
using Microsoft.CmdPal.Common;
namespace CoreWidgetProvider.Helpers;
internal abstract class PerformanceCounterSourceBase
{
protected PerformanceCounter? CreatePerformanceCounter(string categoryName, string counterName, string instanceName = "", bool readOnly = true, bool logFailure = true)
{
try
{
return new PerformanceCounter(categoryName, counterName, instanceName, readOnly);
}
catch (Exception ex)
{
if (logFailure)
{
var suffix = string.IsNullOrEmpty(instanceName) ? string.Empty : $@"\{instanceName}";
CoreLogger.LogError($@"Failed to initialize performance counter '{categoryName}\{counterName}{suffix}'.", ex);
}
return null;
}
}
protected PerformanceCounterCategory? CreatePerformanceCounterCategory(string categoryName, bool logFailure = true)
{
try
{
if (!PerformanceCounterCategory.Exists(categoryName))
{
if (logFailure)
{
CoreLogger.LogError($@"Performance counter category '{categoryName}' does not exist on this system.");
}
return null;
}
return new PerformanceCounterCategory(categoryName);
}
catch (Exception ex)
{
if (logFailure)
{
CoreLogger.LogError($@"Failed to initialize performance counter category '{categoryName}'.", ex);
}
return null;
}
}
protected void LogFailureOnce(ref bool hasLoggedFailure, string message, Exception ex)
{
if (!hasLoggedFailure)
{
hasLoggedFailure = true;
CoreLogger.LogError(message, ex);
}
}
}

View File

@@ -3,24 +3,53 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using Microsoft.CmdPal.Ext.PerformanceMonitor;
namespace CoreWidgetProvider.Helpers; namespace CoreWidgetProvider.Helpers;
internal sealed partial class SystemData : IDisposable internal sealed partial class SystemData
{ {
public static MemoryStats MemStats { get; set; } = new MemoryStats(); public static SystemData Shared { get; } = new();
public static NetworkStats NetStats { get; set; } = new NetworkStats(); private readonly Lazy<MemoryStats> _memoryStats = new(() => CreateGuarded("Memory.Initialize", static () => new MemoryStats()));
private readonly Lazy<NetworkStats> _networkStats = new(() => CreateGuarded("Network.Initialize", static () => new NetworkStats()));
private readonly Lazy<GPUStats> _gpuStats = new(() => CreateGuarded("GPU.Initialize", static () => new GPUStats()));
private readonly Lazy<CPUStats> _cpuStats = new(() => CreateGuarded("CPU.Initialize", static () => new CPUStats()));
public static GPUStats GPUStats { get; set; } = new GPUStats(); public MemoryStats MemoryStats => _memoryStats.Value;
public static CPUStats CpuStats { get; set; } = new CPUStats(); public NetworkStats NetworkStats => _networkStats.Value;
public SystemData() public GPUStats GPUStats => _gpuStats.Value;
public CPUStats CpuStats => _cpuStats.Value;
private SystemData()
{ {
} }
public void Dispose() private static T CreateGuarded<T>(string blockSuffix, Func<T> factory)
{ {
var isTracked = PerformanceMonitorCommandsProvider.CrashSentinel.BeginBlock(blockSuffix);
try
{
var value = factory();
if (isTracked)
{
PerformanceMonitorCommandsProvider.CrashSentinel.CompleteBlock(blockSuffix);
}
return value;
}
catch
{
if (isTracked)
{
PerformanceMonitorCommandsProvider.CrashSentinel.CancelBlock(blockSuffix);
}
throw;
}
} }
} }

View File

@@ -2,7 +2,11 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using CoreWidgetProvider.Helpers; using CoreWidgetProvider.Helpers;
using Microsoft.CmdPal.Common;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -10,30 +14,129 @@ namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
public partial class PerformanceMonitorCommandsProvider : CommandProvider public partial class PerformanceMonitorCommandsProvider : CommandProvider
{ {
private readonly ICommandItem[] _commands; public const string ProviderIdValue = "PerformanceMonitor";
private readonly ICommandItem _band; public const string ProviderLoadGuardBlockId = ProviderIdValue + ".ProviderLoad";
public const string PageIdValue = "com.microsoft.cmdpal.performanceWidget";
public PerformanceMonitorCommandsProvider() internal static ProviderCrashSentinel CrashSentinel { get; } = new(ProviderIdValue);
private readonly Lock _stateLock = new();
private ICommandItem[] _commands = [];
private ICommandItem _band = new CommandItem();
private PerformanceWidgetsPage? _mainPage;
private PerformanceWidgetsPage? _bandPage;
private bool _softDisabled;
public PerformanceMonitorCommandsProvider(bool softDisabled = false)
{ {
DisplayName = Resources.GetResource("Performance_Monitor_Title"); DisplayName = Resources.GetResource("Performance_Monitor_Title");
Id = "PerformanceMonitor"; Id = ProviderIdValue;
Icon = Icons.PerformanceMonitorIcon; Icon = Icons.PerformanceMonitorIcon;
var page = new PerformanceWidgetsPage(false); if (softDisabled)
var band = new PerformanceWidgetsPage(true); {
_band = new CommandItem(band) { Title = DisplayName }; SetDisabledState();
_commands = [ }
new CommandItem(page) { Title = DisplayName }, else
]; {
SetEnabledState();
}
} }
public override ICommandItem[] TopLevelCommands() public override ICommandItem[] TopLevelCommands()
{
lock (_stateLock)
{ {
return _commands; return _commands;
} }
}
public override ICommandItem[]? GetDockBands() public override ICommandItem[]? GetDockBands()
{ {
return new ICommandItem[] { _band }; lock (_stateLock)
{
return [_band];
}
}
public bool TryReactivateImmediately()
{
lock (_stateLock)
{
if (!_softDisabled)
{
return true;
}
try
{
CrashSentinel.ClearProviderState();
SetEnabledState();
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to reactivate Performance Monitor in the current session. Keeping placeholder pages loaded.", ex);
return false;
}
}
RaiseItemsChanged();
return true;
}
public override void Dispose()
{
lock (_stateLock)
{
DisposeActivePages();
}
GC.SuppressFinalize(this);
base.Dispose();
}
private void SetDisabledState()
{
DisposeActivePages();
var page = new PerformanceMonitorDisabledPage(this);
var band = new PerformanceMonitorDisabledPage(this);
_band = new CommandItem(band)
{
Title = Resources.GetResource("Performance_Monitor_Disabled_Band_Title"),
Subtitle = DisplayName,
};
_commands =
[
new CommandItem(page)
{
Title = DisplayName,
Subtitle = Resources.GetResource("Performance_Monitor_Disabled_Subtitle"),
},
];
_softDisabled = true;
}
private void SetEnabledState()
{
DisposeActivePages();
_mainPage = new PerformanceWidgetsPage(false);
_bandPage = new PerformanceWidgetsPage(true);
_band = new CommandItem(_bandPage) { Title = DisplayName };
_commands =
[
new CommandItem(_mainPage) { Title = DisplayName },
];
_softDisabled = false;
}
private void DisposeActivePages()
{
_mainPage?.Dispose();
_mainPage = null;
_bandPage?.Dispose();
_bandPage = null;
} }
} }

View File

@@ -0,0 +1,58 @@
// 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 CoreWidgetProvider.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
internal sealed partial class PerformanceMonitorDisabledPage : ContentPage
{
private readonly MarkdownContent _content;
public PerformanceMonitorDisabledPage(PerformanceMonitorCommandsProvider provider)
{
Id = PerformanceMonitorCommandsProvider.PageIdValue;
Name = Resources.GetResource("Performance_Monitor_Disabled_Title");
Title = Resources.GetResource("Performance_Monitor_Title");
Icon = Icons.PerformanceMonitorIcon;
_content = new MarkdownContent(Resources.GetResource("Performance_Monitor_Disabled_Body"));
Commands =
[
new CommandContextItem(new ReactivatePerformanceMonitorCommand(provider)),
];
}
public override IContent[] GetContent()
{
return [_content];
}
private sealed partial class ReactivatePerformanceMonitorCommand(PerformanceMonitorCommandsProvider provider) : InvokableCommand
{
private readonly PerformanceMonitorCommandsProvider _provider = provider;
public override string Id => "com.microsoft.cmdpal.performanceWidget.reactivate";
public override IconInfo Icon => Icons.NavigateForwardIcon;
public override string Name => Resources.GetResource("Performance_Monitor_Reenable_Title");
public override ICommandResult Invoke()
{
if (_provider.TryReactivateImmediately())
{
return CommandResult.ShowToast(new ToastArgs
{
Message = Resources.GetResource("Performance_Monitor_Reenable_Success"),
Result = CommandResult.GoHome(),
});
}
return CommandResult.ShowToast(Resources.GetResource("Performance_Monitor_Reenable_Failed"));
}
}
}

View File

@@ -189,6 +189,32 @@
<data name="Performance_Monitor_Title" xml:space="preserve"> <data name="Performance_Monitor_Title" xml:space="preserve">
<value>Performance monitor</value> <value>Performance monitor</value>
</data> </data>
<data name="Performance_Monitor_Disabled_Title" xml:space="preserve">
<value>Performance monitor is temporarily disabled</value>
</data>
<data name="Performance_Monitor_Disabled_Subtitle" xml:space="preserve">
<value>Temporarily disabled after repeated startup crashes</value>
</data>
<data name="Performance_Monitor_Disabled_Band_Title" xml:space="preserve">
<value>Perf disabled</value>
</data>
<data name="Performance_Monitor_Disabled_Body" xml:space="preserve">
<value>Performance monitor was temporarily disabled after repeated crashes while initializing or reading performance counters.
Select **Re-enable now** to clear the crash guard and restore the Performance Monitor command and dock band in the current session.</value>
</data>
<data name="Performance_Monitor_Reenable_Title" xml:space="preserve">
<value>Re-enable now</value>
</data>
<data name="Performance_Monitor_Reenable_Subtitle" xml:space="preserve">
<value>Clear the crash guard and restore Performance Monitor in this session</value>
</data>
<data name="Performance_Monitor_Reenable_Success" xml:space="preserve">
<value>Performance monitor has been restored for this session.</value>
</data>
<data name="Performance_Monitor_Reenable_Failed" xml:space="preserve">
<value>Performance monitor could not be restored right now. It will stay disabled for this session.</value>
</data>
<data name="CPU_Usage_Title" xml:space="preserve"> <data name="CPU_Usage_Title" xml:space="preserve">
<value>CPU Usage</value> <value>CPU Usage</value>
</data> </data>