mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
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:
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
AddBuiltInCommands(services);
|
||||
AddBuiltInCommands(services, appInfoService.ConfigDirectory);
|
||||
|
||||
AddCoreServices(services, appInfoService);
|
||||
|
||||
@@ -134,8 +134,10 @@ public partial class App : Application, IDisposable
|
||||
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.
|
||||
var allApps = new AllAppsCommandProvider();
|
||||
var files = new IndexerCommandsProvider();
|
||||
@@ -166,8 +168,7 @@ public partial class App : Application, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Couldn't load winget");
|
||||
Logger.LogError(ex.ToString());
|
||||
Logger.LogError("Couldn't load winget", ex);
|
||||
}
|
||||
|
||||
services.AddSingleton<ICommandProvider, WindowsTerminalCommandsProvider>();
|
||||
@@ -178,7 +179,37 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,17 @@ using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class CPUStats : IDisposable
|
||||
internal sealed partial class CPUStats : PerformanceCounterSourceBase, IDisposable
|
||||
{
|
||||
// CPU counters
|
||||
private readonly PerformanceCounter _procPerf = new("Processor Information", "% Processor Utility", "_Total");
|
||||
private readonly PerformanceCounter _procPerformance = new("Processor Information", "% Processor Performance", "_Total");
|
||||
private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total");
|
||||
private readonly PerformanceCounter? _procPerf;
|
||||
private readonly PerformanceCounter? _procPerformance;
|
||||
private readonly PerformanceCounter? _procFrequency;
|
||||
private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new();
|
||||
private bool _processCountersInitialized;
|
||||
private bool _cpuCounterReadFailureLogged;
|
||||
private bool _processCounterEnumerationFailureLogged;
|
||||
private bool _processCounterReadFailureLogged;
|
||||
|
||||
internal sealed class ProcessStats
|
||||
{
|
||||
@@ -42,25 +46,62 @@ internal sealed partial class CPUStats : IDisposable
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
if (_procPerf is not null)
|
||||
{
|
||||
CpuUsage = _procPerf.NextValue() / 100;
|
||||
}
|
||||
|
||||
var usageMs = timer.ElapsedMilliseconds;
|
||||
if (_procFrequency is not null && _procPerformance is not null)
|
||||
{
|
||||
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
|
||||
}
|
||||
|
||||
var speedMs = timer.ElapsedMilliseconds - usageMs;
|
||||
lock (CpuChartValues)
|
||||
{
|
||||
@@ -73,7 +114,10 @@ internal sealed partial class CPUStats : IDisposable
|
||||
|
||||
if (includeTopProcesses)
|
||||
{
|
||||
foreach (var processCounter in _cpuCounters)
|
||||
EnsureCPUProcessCountersInitialized();
|
||||
|
||||
var countersToRemove = new List<Process>();
|
||||
foreach (var processCounter in _cpuCounters.ToArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -82,12 +126,19 @@ internal sealed partial class CPUStats : IDisposable
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters.");
|
||||
_cpuCounters.Remove(processCounter.Key);
|
||||
countersToRemove.Add(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}]");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogFailureOnce(ref _cpuCounterReadFailureLogged, "Failed while reading CPU performance counters.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal string CreateCPUImageUrl()
|
||||
{
|
||||
@@ -134,9 +190,9 @@ internal sealed partial class CPUStats : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_procPerf.Dispose();
|
||||
_procPerformance.Dispose();
|
||||
_procFrequency.Dispose();
|
||||
_procPerf?.Dispose();
|
||||
_procPerformance?.Dispose();
|
||||
_procFrequency?.Dispose();
|
||||
|
||||
foreach (var counter in _cpuCounters.Values)
|
||||
{
|
||||
|
||||
@@ -3,22 +3,23 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class DataManager : IDisposable
|
||||
{
|
||||
private readonly SystemData _systemData;
|
||||
private readonly SystemData _systemData = SystemData.Shared;
|
||||
private readonly DataType _dataType;
|
||||
private readonly Timer _updateTimer;
|
||||
private readonly Action _updateAction;
|
||||
private bool _updateFailureLogged;
|
||||
|
||||
private const int OneSecondInMilliseconds = 1000;
|
||||
|
||||
public DataManager(DataType type, Action updateWidget)
|
||||
{
|
||||
_systemData = new SystemData();
|
||||
_updateAction = updateWidget;
|
||||
_dataType = type;
|
||||
|
||||
@@ -30,37 +31,42 @@ internal sealed partial class DataManager : IDisposable
|
||||
|
||||
private void GetMemoryData()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
lock (_systemData.MemoryStats)
|
||||
{
|
||||
SystemData.MemStats.GetData();
|
||||
_systemData.MemoryStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetNetworkData()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
lock (_systemData.NetworkStats)
|
||||
{
|
||||
SystemData.NetStats.GetData();
|
||||
_systemData.NetworkStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetGPUData()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
lock (_systemData.GPUStats)
|
||||
{
|
||||
SystemData.GPUStats.GetData();
|
||||
_systemData.GPUStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var firstUpdateBlockSuffix = GetFirstUpdateBlockSuffix();
|
||||
var isTracked = firstUpdateBlockSuffix is not null && PerformanceMonitorCommandsProvider.CrashSentinel.BeginBlock(firstUpdateBlockSuffix);
|
||||
|
||||
try
|
||||
{
|
||||
switch (_dataType)
|
||||
{
|
||||
@@ -94,38 +100,71 @@ internal sealed partial class DataManager : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
if (isTracked)
|
||||
{
|
||||
PerformanceMonitorCommandsProvider.CrashSentinel.CompleteBlock(firstUpdateBlockSuffix!);
|
||||
}
|
||||
|
||||
_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()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
lock (_systemData.MemoryStats)
|
||||
{
|
||||
return SystemData.MemStats;
|
||||
return _systemData.MemoryStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal NetworkStats GetNetworkStats()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
lock (_systemData.NetworkStats)
|
||||
{
|
||||
return SystemData.NetStats;
|
||||
return _systemData.NetworkStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal GPUStats GetGPUStats()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
lock (_systemData.GPUStats)
|
||||
{
|
||||
return SystemData.GPUStats;
|
||||
return _systemData.GPUStats;
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
_systemData.Dispose();
|
||||
_updateTimer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Text;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class GPUStats : IDisposable
|
||||
internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposable
|
||||
{
|
||||
// Performance counter category & counter names
|
||||
private const string GpuEngineCategoryName = "GPU Engine";
|
||||
@@ -32,7 +32,7 @@ internal sealed partial class GPUStats : IDisposable
|
||||
private const string TemperatureUnavailable = "--";
|
||||
|
||||
// Batch read via category - single kernel transition per tick
|
||||
private readonly PerformanceCounterCategory _gpuEngineCategory = new(GpuEngineCategoryName);
|
||||
private readonly PerformanceCounterCategory? _gpuEngineCategory;
|
||||
|
||||
// Discovered physical GPU IDs
|
||||
private readonly HashSet<int> _knownPhysIds = [];
|
||||
@@ -41,6 +41,8 @@ internal sealed partial class GPUStats : IDisposable
|
||||
|
||||
// Previous raw samples for computing cooked (delta-based) values
|
||||
private Dictionary<string, CounterSample> _previousSamples = [];
|
||||
private bool _gpuEnumerationFailureLogged;
|
||||
private bool _gpuReadFailureLogged;
|
||||
|
||||
public sealed class Data
|
||||
{
|
||||
@@ -57,11 +59,20 @@ internal sealed partial class GPUStats : IDisposable
|
||||
|
||||
public GPUStats()
|
||||
{
|
||||
_gpuEngineCategory = CreatePerformanceCounterCategory(GpuEngineCategoryName);
|
||||
|
||||
GetGPUPerfCounters();
|
||||
LoadGPUsFromCounters();
|
||||
}
|
||||
|
||||
public void GetGPUPerfCounters()
|
||||
{
|
||||
if (_gpuEngineCategory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// There are really 4 different things we should be tracking the usage
|
||||
// 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()
|
||||
{
|
||||
@@ -119,6 +135,11 @@ internal sealed partial class GPUStats : IDisposable
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
if (_gpuEngineCategory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@ using Windows.Win32;
|
||||
|
||||
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 _memCached = new("Memory", "Cache Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memCommittedLimit = new("Memory", "Commit Limit", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolPaged = new("Memory", "Pool Paged Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolNonPaged = new("Memory", "Pool Nonpaged Bytes", string.Empty);
|
||||
private readonly PerformanceCounter? _memCommitted;
|
||||
private readonly PerformanceCounter? _memCached;
|
||||
private readonly PerformanceCounter? _memCommittedLimit;
|
||||
private readonly PerformanceCounter? _memPoolPaged;
|
||||
private readonly PerformanceCounter? _memPoolNonPaged;
|
||||
private bool _memoryCounterReadFailureLogged;
|
||||
|
||||
public float MemUsage
|
||||
{
|
||||
@@ -60,6 +61,15 @@ internal sealed partial class MemoryStats : IDisposable
|
||||
|
||||
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()
|
||||
{
|
||||
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
|
||||
@@ -77,11 +87,18 @@ internal sealed partial class MemoryStats : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
MemCached = (ulong)_memCached.NextValue();
|
||||
MemCommitted = (ulong)_memCommitted.NextValue();
|
||||
MemCommitLimit = (ulong)_memCommittedLimit.NextValue();
|
||||
MemPagedPool = (ulong)_memPoolPaged.NextValue();
|
||||
MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue();
|
||||
try
|
||||
{
|
||||
MemCached = (ulong)(_memCached?.NextValue() ?? 0);
|
||||
MemCommitted = (ulong)(_memCommitted?.NextValue() ?? 0);
|
||||
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()
|
||||
@@ -91,10 +108,10 @@ internal sealed partial class MemoryStats : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_memCommitted.Dispose();
|
||||
_memCached.Dispose();
|
||||
_memCommittedLimit.Dispose();
|
||||
_memPoolPaged.Dispose();
|
||||
_memPoolNonPaged.Dispose();
|
||||
_memCommitted?.Dispose();
|
||||
_memCached?.Dispose();
|
||||
_memCommittedLimit?.Dispose();
|
||||
_memPoolPaged?.Dispose();
|
||||
_memPoolNonPaged?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Common;
|
||||
|
||||
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 bool _networkCounterReadFailureLogged;
|
||||
|
||||
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
|
||||
|
||||
@@ -42,18 +44,45 @@ internal sealed partial class NetworkStats : IDisposable
|
||||
|
||||
private void InitNetworkPerfCounters()
|
||||
{
|
||||
var perfCounterCategory = new PerformanceCounterCategory("Network Interface");
|
||||
try
|
||||
{
|
||||
var perfCounterCategory = CreatePerformanceCounterCategory("Network Interface");
|
||||
if (perfCounterCategory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceNames = perfCounterCategory.GetInstanceNames();
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
var instanceCounters = new List<PerformanceCounter>();
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName));
|
||||
try
|
||||
{
|
||||
var bytesSent = CreatePerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName, logFailure: false);
|
||||
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);
|
||||
NetChartValues.Add(instanceName, new List<float>());
|
||||
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()
|
||||
@@ -88,9 +117,9 @@ internal sealed partial class NetworkStats : IDisposable
|
||||
maxUsage = usage;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log.Error(ex, "Error getting network data.");
|
||||
LogFailureOnce(ref _networkCounterReadFailureLogged, "Failed while reading network performance counters.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,53 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
// 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.Threading;
|
||||
using CoreWidgetProvider.Helpers;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -10,30 +14,129 @@ namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
|
||||
public partial class PerformanceMonitorCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly ICommandItem[] _commands;
|
||||
private readonly ICommandItem _band;
|
||||
public const string ProviderIdValue = "PerformanceMonitor";
|
||||
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");
|
||||
Id = "PerformanceMonitor";
|
||||
Id = ProviderIdValue;
|
||||
Icon = Icons.PerformanceMonitorIcon;
|
||||
|
||||
var page = new PerformanceWidgetsPage(false);
|
||||
var band = new PerformanceWidgetsPage(true);
|
||||
_band = new CommandItem(band) { Title = DisplayName };
|
||||
_commands = [
|
||||
new CommandItem(page) { Title = DisplayName },
|
||||
];
|
||||
if (softDisabled)
|
||||
{
|
||||
SetDisabledState();
|
||||
}
|
||||
else
|
||||
{
|
||||
SetEnabledState();
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _commands;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,32 @@
|
||||
<data name="Performance_Monitor_Title" xml:space="preserve">
|
||||
<value>Performance monitor</value>
|
||||
</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">
|
||||
<value>CPU Usage</value>
|
||||
</data>
|
||||
|
||||
Reference in New Issue
Block a user