CmdPal: Aggregate net adapter stats and default adapter selection (#49678)

## Summary of the Pull Request

This PR introduces a new virtual network adapter in performance monitor
that sums up data from all physical adapters. That way users will see
any network usage, and for most of them it might be good enough. For the
rest, this PR adds a new command to set any network adapter as a
default.

- Switches from Performance Counters to raw GetIfTable2.
- Adds a new virtual network adapter "All" and uses it as a default.
- Adds a new command to set an actual default network adapter for the
perfmon.

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

- [ ] Closes: #xxx
<!-- - [ ] 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
This commit is contained in:
Jiří Polášek
2026-08-08 20:40:59 +02:00
committed by GitHub
parent 9cfba78b60
commit 03c571e1fc
9 changed files with 584 additions and 118 deletions

View File

@@ -0,0 +1,138 @@
// 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.Collections.Generic;
using CoreWidgetProvider.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests;
[TestClass]
public class NetworkStatsTests
{
private static readonly Guid EthernetGuid = new("11111111-1111-1111-1111-111111111111");
private static readonly Guid WifiGuid = new("22222222-2222-2222-2222-222222222222");
[TestMethod]
public void ApplySnapshots_AddsAggregateBeforeIndividualAdapters()
{
var stats = CreateNetworkStats();
stats.ApplySnapshots(
[
new(1, EthernetGuid, "Ethernet", 10_000, 2_000, 1_000_000),
new(2, WifiGuid, "Wi-Fi", 20_000, 4_000, 2_000_000),
],
0);
stats.ApplySnapshots(
[
new(1, EthernetGuid, "Ethernet", 60_000, 27_000, 1_000_000),
new(2, WifiGuid, "Wi-Fi", 120_000, 29_000, 2_000_000),
],
1);
Assert.AreEqual("All physical network adapters", stats.GetNetworkName(0));
Assert.AreEqual("Ethernet", stats.GetNetworkName(1));
Assert.AreEqual("Wi-Fi", stats.GetNetworkName(2));
var aggregate = stats.GetNetworkUsage(0);
Assert.AreEqual(50_000f, aggregate.Sent);
Assert.AreEqual(150_000f, aggregate.Received);
Assert.AreEqual(8f * 200_000 / 3_000_000, aggregate.Usage, 0.0001f);
}
[TestMethod]
public void ApplySnapshots_TreatsCounterResetAsZeroDelta()
{
var stats = CreateNetworkStats();
stats.ApplySnapshots([new(1, EthernetGuid, "Ethernet", 10_000, 5_000, 1_000_000)], 0);
stats.ApplySnapshots([new(1, EthernetGuid, "Ethernet", 1_000, 5_500, 1_000_000)], 2);
var ethernet = stats.GetNetworkUsage(1);
Assert.AreEqual(0f, ethernet.Received);
Assert.AreEqual(250f, ethernet.Sent);
}
[TestMethod]
public void ApplySnapshots_ReappearingAdapterStartsWithZeroRate()
{
var stats = CreateNetworkStats();
stats.ApplySnapshots([new(1, EthernetGuid, "USB Ethernet", 10_000, 5_000, 1_000_000)], 0);
stats.ApplySnapshots([], 1);
stats.ApplySnapshots([new(1, EthernetGuid, "USB Ethernet", 100_000, 50_000, 1_000_000)], 1);
var ethernet = stats.GetNetworkUsage(1);
Assert.AreEqual(0f, ethernet.Received);
Assert.AreEqual(0f, ethernet.Sent);
}
[TestMethod]
public void AdapterIds_DistinguishAggregateFromMissingAndResolveLateAdapter()
{
var stats = CreateNetworkStats();
var wifiAdapterId = "network-interface:" + WifiGuid.ToString("D");
Assert.AreEqual(-1, stats.GetNetworkIndex(wifiAdapterId));
stats.ApplySnapshots(
[
new(1, EthernetGuid, "Ethernet", 10_000, 2_000, 1_000_000),
new(2, WifiGuid, "Wi-Fi", 20_000, 4_000, 2_000_000),
],
0);
Assert.AreEqual(NetworkStats.AllPhysicalAdaptersId, stats.GetNetworkId(0));
Assert.AreEqual(0, stats.GetNetworkIndex(NetworkStats.AllPhysicalAdaptersId));
Assert.AreEqual(1, stats.GetNetworkIndex(stats.GetNetworkId(1)));
Assert.AreEqual(2, stats.GetNetworkIndex(wifiAdapterId.ToUpperInvariant()));
Assert.AreEqual(-1, stats.GetNetworkIndex("network-interface:missing"));
}
[TestMethod]
public void ApplySnapshots_UnknownLinkSpeedStillContributesBytesAndCapsAggregateUsage()
{
var stats = CreateNetworkStats();
stats.ApplySnapshots(
[
new(1, EthernetGuid, "Ethernet", 0, 0, 1_000_000),
new(2, WifiGuid, "Unknown speed", 0, 0, 0),
],
0);
stats.ApplySnapshots(
[
new(1, EthernetGuid, "Ethernet", 100_000, 0, 1_000_000),
new(2, WifiGuid, "Unknown speed", 50_000, 0, 0),
],
1);
var aggregate = stats.GetNetworkUsage(0);
Assert.AreEqual(150_000f, aggregate.Received);
Assert.AreEqual(1f, aggregate.Usage);
}
[TestMethod]
public void GetKnownLinkSpeed_IgnoresUnknownSpeedSentinel()
{
Assert.AreEqual(1_000_000UL, PhysicalNetworkInterfaceSnapshotProvider.GetKnownLinkSpeed(ulong.MaxValue, 1_000_000));
Assert.AreEqual(2_000_000UL, PhysicalNetworkInterfaceSnapshotProvider.GetKnownLinkSpeed(2_000_000, ulong.MaxValue));
Assert.AreEqual(0UL, PhysicalNetworkInterfaceSnapshotProvider.GetKnownLinkSpeed(ulong.MaxValue, ulong.MaxValue));
}
private static NetworkStats CreateNetworkStats()
{
return new NetworkStats(new UnusedSnapshotProvider(), "All physical network adapters");
}
private sealed class UnusedSnapshotProvider : IPhysicalNetworkInterfaceSnapshotProvider
{
public IReadOnlyList<PhysicalNetworkInterfaceSnapshot> GetSnapshots()
{
throw new InvalidOperationException("This provider is not used by these tests.");
}
}
}

View File

@@ -0,0 +1,42 @@
// 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.IO;
using CoreWidgetProvider.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests;
[TestClass]
public class SettingsManagerTests
{
[TestMethod]
public void DefaultNetworkAdapterId_RoundTripsThroughSettingsFile()
{
var directory = Path.Combine(Path.GetTempPath(), $"PerformanceMonitorSettingsTests-{Guid.NewGuid():N}");
var filePath = Path.Combine(directory, "performanceMonitor.settings.json");
var adapterId = "network-interface:11111111-1111-1111-1111-111111111111";
try
{
Directory.CreateDirectory(directory);
var settings = new SettingsManager(filePath);
Assert.AreEqual(NetworkStats.AllPhysicalAdaptersId, settings.DefaultNetworkAdapterId);
settings.SetDefaultNetworkAdapterId(adapterId);
var reloadedSettings = new SettingsManager(filePath);
Assert.AreEqual(adapterId, reloadedSettings.DefaultNetworkAdapterId);
}
finally
{
if (Directory.Exists(directory))
{
Directory.Delete(directory, true);
}
}
}
}

View File

@@ -5,179 +5,191 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Globalization;
using System.Threading;
using Microsoft.CmdPal.Common;
namespace CoreWidgetProvider.Helpers;
internal sealed partial class NetworkStats : PerformanceCounterSourceBase, IDisposable
internal sealed partial class NetworkStats
{
private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new();
private bool _networkCounterReadFailureLogged;
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
private Dictionary<string, List<float>> NetChartValues { get; set; } = new();
internal const string AllPhysicalAdaptersId = "all-physical-network-adapters";
private const string PhysicalAdapterIdPrefix = "network-interface:";
private readonly IPhysicalNetworkInterfaceSnapshotProvider _snapshotProvider;
private readonly string _allAdaptersName;
private readonly Dictionary<ulong, CounterSample> _previousSamples = new();
private readonly Dictionary<ulong, List<float>> _chartValues = new();
private readonly List<float> _allAdaptersChartValues = new();
private readonly List<ulong> _missingInterfaceIds = new();
private NetworkAdapterData[] _networkAdapters = [];
private long? _lastTimestamp;
private bool _snapshotReadFailureLogged;
public sealed class Data
{
public float Usage
{
get; set;
get; init;
}
public float Sent
{
get; set;
get; init;
}
public float Received
{
get; set;
get; init;
}
}
private sealed record NetworkAdapterData(string Id, string Name, Data Usage, List<float> ChartValues);
private readonly record struct CounterSample(ulong ReceivedBytes, ulong SentBytes);
public NetworkStats()
: this(new PhysicalNetworkInterfaceSnapshotProvider(), Resources.GetResource("All_Physical_Network_Adapters"))
{
InitNetworkPerfCounters();
GetData();
}
private void InitNetworkPerfCounters()
internal NetworkStats(IPhysicalNetworkInterfaceSnapshotProvider snapshotProvider, string allAdaptersName)
{
try
{
var perfCounterCategory = CreatePerformanceCounterCategory("Network Interface");
if (perfCounterCategory is null)
{
return;
}
var instanceNames = perfCounterCategory.GetInstanceNames();
foreach (var instanceName in instanceNames)
{
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);
}
_snapshotProvider = snapshotProvider;
_allAdaptersName = allAdaptersName;
_networkAdapters = [new(AllPhysicalAdaptersId, _allAdaptersName, new Data(), _allAdaptersChartValues)];
}
public void GetData()
{
float maxUsage = 0;
foreach (var networkCounterWithName in _networkCounters)
try
{
try
var snapshots = _snapshotProvider.GetSnapshots();
var timestamp = Stopwatch.GetTimestamp();
var elapsedSeconds = _lastTimestamp is long previousTimestamp
? Stopwatch.GetElapsedTime(previousTimestamp, timestamp).TotalSeconds
: 0;
ApplySnapshots(snapshots, elapsedSeconds);
_lastTimestamp = timestamp;
}
catch (Exception ex)
{
if (!_snapshotReadFailureLogged)
{
var sent = networkCounterWithName.Value[0].NextValue();
var received = networkCounterWithName.Value[1].NextValue();
var bandWidth = networkCounterWithName.Value[2].NextValue();
if (bandWidth == 0)
{
continue;
}
var usage = 8 * (sent + received) / bandWidth;
var name = networkCounterWithName.Key;
NetworkUsages[name].Sent = sent;
NetworkUsages[name].Received = received;
NetworkUsages[name].Usage = usage;
var chartValues = NetChartValues[name];
lock (chartValues)
{
ChartHelper.AddNextChartValue(usage * 100, chartValues);
}
if (usage > maxUsage)
{
maxUsage = usage;
}
}
catch (Exception ex)
{
LogFailureOnce(ref _networkCounterReadFailureLogged, "Failed while reading network performance counters.", ex);
_snapshotReadFailureLogged = true;
CoreLogger.LogError("Failed while reading physical network interface statistics.", ex);
}
}
}
internal void ApplySnapshots(IReadOnlyList<PhysicalNetworkInterfaceSnapshot> snapshots, double elapsedSeconds)
{
var currentInterfaceIds = new HashSet<ulong>(snapshots.Count);
var adapterMeasurements = new List<(PhysicalNetworkInterfaceSnapshot Snapshot, Data Usage)>(snapshots.Count);
double totalSent = 0;
double totalReceived = 0;
double totalBandwidth = 0;
foreach (var snapshot in snapshots)
{
currentInterfaceIds.Add(snapshot.InterfaceLuid);
var sent = 0d;
var received = 0d;
if (elapsedSeconds > 0 && _previousSamples.TryGetValue(snapshot.InterfaceLuid, out var previousSample))
{
sent = GetBytesPerSecond(previousSample.SentBytes, snapshot.SentBytes, elapsedSeconds);
received = GetBytesPerSecond(previousSample.ReceivedBytes, snapshot.ReceivedBytes, elapsedSeconds);
}
_previousSamples[snapshot.InterfaceLuid] = new(snapshot.ReceivedBytes, snapshot.SentBytes);
var usage = CreateUsage(sent, received, snapshot.LinkSpeed);
adapterMeasurements.Add((snapshot, usage));
totalSent += sent;
totalReceived += received;
totalBandwidth += snapshot.LinkSpeed;
}
RemoveMissingInterfaces(currentInterfaceIds);
var aggregateUsage = CreateUsage(totalSent, totalReceived, totalBandwidth);
AddChartValue(_allAdaptersChartValues, aggregateUsage.Usage);
var adapters = new NetworkAdapterData[adapterMeasurements.Count + 1];
adapters[0] = new(AllPhysicalAdaptersId, _allAdaptersName, aggregateUsage, _allAdaptersChartValues);
for (var index = 0; index < adapterMeasurements.Count; index++)
{
var (snapshot, usage) = adapterMeasurements[index];
if (!_chartValues.TryGetValue(snapshot.InterfaceLuid, out var chartValues))
{
chartValues = new List<float>();
_chartValues.Add(snapshot.InterfaceLuid, chartValues);
}
AddChartValue(chartValues, usage.Usage);
adapters[index + 1] = new(GetPhysicalAdapterId(snapshot), snapshot.Name, usage, chartValues);
}
Volatile.Write(ref _networkAdapters, adapters);
}
public string CreateNetImageUrl(int netChartIndex)
{
return ChartHelper.CreateImageUrl(NetChartValues.ElementAt(netChartIndex).Value, ChartHelper.ChartType.Net);
var adapters = Volatile.Read(ref _networkAdapters);
var resolvedIndex = ResolveIndex(netChartIndex, adapters.Length);
return ChartHelper.CreateImageUrl(adapters[resolvedIndex].ChartValues, ChartHelper.ChartType.Net);
}
public string GetNetworkName(int networkIndex)
{
if (NetChartValues.Count <= networkIndex)
var adapters = Volatile.Read(ref _networkAdapters);
return adapters[ResolveIndex(networkIndex, adapters.Length)].Name;
}
public string GetNetworkId(int networkIndex)
{
var adapters = Volatile.Read(ref _networkAdapters);
return adapters[ResolveIndex(networkIndex, adapters.Length)].Id;
}
public int GetNetworkIndex(string adapterId)
{
var adapters = Volatile.Read(ref _networkAdapters);
for (var index = 0; index < adapters.Length; index++)
{
return string.Empty;
if (string.Equals(adapters[index].Id, adapterId, StringComparison.OrdinalIgnoreCase))
{
return index;
}
}
return NetChartValues.ElementAt(networkIndex).Key;
return -1;
}
public Data GetNetworkUsage(int networkIndex)
{
if (NetChartValues.Count <= networkIndex)
{
return new Data();
}
var currNetworkName = NetChartValues.ElementAt(networkIndex).Key;
if (!NetworkUsages.TryGetValue(currNetworkName, out var value))
{
return new Data();
}
return value;
var adapters = Volatile.Read(ref _networkAdapters);
return adapters[ResolveIndex(networkIndex, adapters.Length)].Usage;
}
public int GetPrevNetworkIndex(int networkIndex)
{
if (NetChartValues.Count == 0)
var adapterCount = Volatile.Read(ref _networkAdapters).Length;
if (adapterCount == 0)
{
return 0;
}
if (networkIndex == 0)
{
return NetChartValues.Count - 1;
}
return networkIndex - 1;
return networkIndex <= 0 || networkIndex >= adapterCount ? adapterCount - 1 : networkIndex - 1;
}
public int GetNextNetworkIndex(int networkIndex)
{
if (NetChartValues.Count == 0)
{
return 0;
}
if (networkIndex == NetChartValues.Count - 1)
var adapterCount = Volatile.Read(ref _networkAdapters).Length;
if (adapterCount == 0 || networkIndex < 0 || networkIndex >= adapterCount - 1)
{
return 0;
}
@@ -185,14 +197,57 @@ internal sealed partial class NetworkStats : PerformanceCounterSourceBase, IDisp
return networkIndex + 1;
}
public void Dispose()
private static double GetBytesPerSecond(ulong previousValue, ulong currentValue, double elapsedSeconds)
{
foreach (var counterPair in _networkCounters)
return currentValue >= previousValue ? (currentValue - previousValue) / elapsedSeconds : 0;
}
private static int ResolveIndex(int requestedIndex, int adapterCount)
{
return (uint)requestedIndex < (uint)adapterCount ? requestedIndex : 0;
}
private static string GetPhysicalAdapterId(PhysicalNetworkInterfaceSnapshot snapshot)
{
return snapshot.InterfaceGuid != Guid.Empty
? PhysicalAdapterIdPrefix + snapshot.InterfaceGuid.ToString("D")
: PhysicalAdapterIdPrefix + snapshot.InterfaceLuid.ToString("X16", CultureInfo.InvariantCulture);
}
private static Data CreateUsage(double sent, double received, double bandwidth)
{
var usage = bandwidth > 0 ? Math.Min(8 * (sent + received) / bandwidth, 1) : 0;
return new Data
{
foreach (var counter in counterPair.Value)
Sent = (float)sent,
Received = (float)received,
Usage = (float)usage,
};
}
private static void AddChartValue(List<float> chartValues, float usage)
{
lock (chartValues)
{
ChartHelper.AddNextChartValue(usage * 100, chartValues);
}
}
private void RemoveMissingInterfaces(HashSet<ulong> currentInterfaceIds)
{
_missingInterfaceIds.Clear();
foreach (var interfaceId in _previousSamples.Keys)
{
if (!currentInterfaceIds.Contains(interfaceId))
{
counter.Dispose();
_missingInterfaceIds.Add(interfaceId);
}
}
foreach (var interfaceId in _missingInterfaceIds)
{
_previousSamples.Remove(interfaceId);
_chartValues.Remove(interfaceId);
}
}
}

View File

@@ -0,0 +1,93 @@
// 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.Collections.Generic;
using System.ComponentModel;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.NetworkManagement.IpHelper;
namespace CoreWidgetProvider.Helpers;
internal sealed class PhysicalNetworkInterfaceSnapshotProvider : IPhysicalNetworkInterfaceSnapshotProvider
{
public unsafe IReadOnlyList<PhysicalNetworkInterfaceSnapshot> GetSnapshots()
{
var result = PInvoke.GetIfTable2(out var table);
if (result != WIN32_ERROR.NO_ERROR)
{
throw new Win32Exception(unchecked((int)result));
}
if (table is null)
{
return [];
}
try
{
var snapshots = new List<PhysicalNetworkInterfaceSnapshot>(checked((int)table->NumEntries));
foreach (ref readonly var row in table->Table.AsSpan(checked((int)table->NumEntries)))
{
var flags = row.InterfaceAndOperStatusFlags;
if (!flags.HardwareInterface || flags.FilterInterface || flags.EndPointInterface)
{
continue;
}
var name = row.Description.ToString();
if (string.IsNullOrWhiteSpace(name))
{
name = row.Alias.ToString();
}
if (string.IsNullOrWhiteSpace(name))
{
name = row.InterfaceGuid.ToString();
}
snapshots.Add(new(
row.InterfaceLuid.Value,
row.InterfaceGuid,
name,
row.InOctets,
row.OutOctets,
GetKnownLinkSpeed(row.ReceiveLinkSpeed, row.TransmitLinkSpeed)));
}
snapshots.Sort(static (left, right) =>
{
var nameComparison = StringComparer.OrdinalIgnoreCase.Compare(left.Name, right.Name);
return nameComparison != 0 ? nameComparison : left.InterfaceLuid.CompareTo(right.InterfaceLuid);
});
return snapshots;
}
finally
{
PInvoke.FreeMibTable(table);
}
}
internal static ulong GetKnownLinkSpeed(ulong receiveLinkSpeed, ulong transmitLinkSpeed)
{
var knownReceiveLinkSpeed = receiveLinkSpeed == ulong.MaxValue ? 0 : receiveLinkSpeed;
var knownTransmitLinkSpeed = transmitLinkSpeed == ulong.MaxValue ? 0 : transmitLinkSpeed;
return Math.Max(knownReceiveLinkSpeed, knownTransmitLinkSpeed);
}
}
internal readonly record struct PhysicalNetworkInterfaceSnapshot(
ulong InterfaceLuid,
Guid InterfaceGuid,
string Name,
ulong ReceivedBytes,
ulong SentBytes,
ulong LinkSpeed);
internal interface IPhysicalNetworkInterfaceSnapshotProvider
{
IReadOnlyList<PhysicalNetworkInterfaceSnapshot> GetSnapshots();
}

View File

@@ -70,4 +70,6 @@ internal static class Icons
internal static IconInfo NavigateBackwardIcon => new("\uE72B"); // Previous icon
internal static IconInfo NavigateForwardIcon => new("\uE72A"); // Next icon
internal static IconInfo SetDefaultIcon => new("\uE73E"); // Accept icon
}

View File

@@ -4,3 +4,5 @@ CreateDXGIFactory1
IDXGIFactory1
IDXGIAdapter1
DXGI_ADAPTER_DESC1
GetIfTable2
FreeMibTable

View File

@@ -971,6 +971,7 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
private readonly DataManager _dataManager;
private readonly SettingsManager _settingsManager;
private int _networkIndex;
private bool _defaultNetworkInitialized;
public SystemNetworkUsageWidgetPage(SettingsManager settingsManager)
{
@@ -979,6 +980,7 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
Commands = [
new CommandContextItem(new PrevNetworkCommand(this) { Name = Resources.GetResource("Previous_Network_Title") }),
new CommandContextItem(new NextNetworkCommand(this) { Name = Resources.GetResource("Next_Network_Title") }),
new CommandContextItem(new SetDefaultNetworkCommand(this) { Name = Resources.GetResource("Set_Default_Network_Title") }),
new CommandContextItem(OpenTaskManagerCommand.Instance),
];
}
@@ -994,6 +996,16 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
var currentData = _dataManager.GetNetworkStats();
if (!_defaultNetworkInitialized)
{
var defaultNetworkIndex = currentData.GetNetworkIndex(_settingsManager.DefaultNetworkAdapterId);
if (defaultNetworkIndex >= 0)
{
_networkIndex = defaultNetworkIndex;
_defaultNetworkInitialized = true;
}
}
var dataDuration = timer.ElapsedMilliseconds;
var netName = currentData.GetNetworkName(_networkIndex);
@@ -1096,16 +1108,32 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
private void HandlePrevNetwork()
{
_defaultNetworkInitialized = true;
_networkIndex = _dataManager.GetNetworkStats().GetPrevNetworkIndex(_networkIndex);
UpdateWidget();
}
private void HandleNextNetwork()
{
_defaultNetworkInitialized = true;
_networkIndex = _dataManager.GetNetworkStats().GetNextNetworkIndex(_networkIndex);
UpdateWidget();
}
private ICommandResult HandleSetDefaultNetwork()
{
_defaultNetworkInitialized = true;
var networkStats = _dataManager.GetNetworkStats();
var networkName = networkStats.GetNetworkName(_networkIndex);
_settingsManager.SetDefaultNetworkAdapterId(networkStats.GetNetworkId(_networkIndex));
return CommandResult.ShowToast(new ToastArgs
{
Message = string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Set_Default_Network_Success"), networkName),
Result = CommandResult.KeepOpen(),
});
}
public void Dispose()
{
_dataManager.Dispose();
@@ -1150,6 +1178,25 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
return CommandResult.KeepOpen();
}
}
private sealed partial class SetDefaultNetworkCommand : InvokableCommand
{
private readonly SystemNetworkUsageWidgetPage _page;
public SetDefaultNetworkCommand(SystemNetworkUsageWidgetPage page)
{
_page = page;
}
public override string Id => "com.microsoft.cmdpal.network_widget.setDefault";
public override IconInfo Icon => Icons.SetDefaultIcon;
public override ICommandResult Invoke()
{
return _page.HandleSetDefaultNetwork();
}
}
}
internal sealed partial class SystemGPUUsageWidgetPage : WidgetPage, IDisposable

View File

@@ -4,7 +4,11 @@
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using CoreWidgetProvider.Helpers;
using Microsoft.CmdPal.Common;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
@@ -12,6 +16,9 @@ namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
internal sealed class SettingsManager : JsonSettingsManager
{
private const string Namespace = "performanceMonitor";
private static readonly JsonSerializerOptions _serializerOptions = new() { WriteIndented = true };
private readonly Lock _fileLock = new();
private string _defaultNetworkAdapterId = NetworkStats.AllPhysicalAdaptersId;
private static string Namespaced(string propertyName) => $"{Namespace}.{propertyName}";
@@ -30,6 +37,8 @@ internal sealed class SettingsManager : JsonSettingsManager
? unit
: SpeedUnit.BitsPerSecond;
public string DefaultNetworkAdapterId => _defaultNetworkAdapterId;
private readonly ChoiceSetSetting _diskSpeedUnit = new(
Namespaced(nameof(DiskSpeedUnit)),
Resources.GetResource("Disk_Speed_Unit_Setting_Title"),
@@ -53,14 +62,82 @@ internal sealed class SettingsManager : JsonSettingsManager
}
public SettingsManager()
: this(SettingsJsonPath())
{
FilePath = SettingsJsonPath();
}
internal SettingsManager(string filePath)
{
FilePath = filePath;
Settings.Add(_networkSpeedUnit);
Settings.Add(_diskSpeedUnit);
LoadSettings();
LoadDefaultNetworkAdapterId();
Settings.SettingsChanged += (_, _) => SaveSettings();
}
public void SetDefaultNetworkAdapterId(string adapterId)
{
if (string.IsNullOrWhiteSpace(adapterId) || string.Equals(_defaultNetworkAdapterId, adapterId, StringComparison.OrdinalIgnoreCase))
{
return;
}
_defaultNetworkAdapterId = adapterId;
SaveSettings();
}
public override void SaveSettings()
{
lock (_fileLock)
{
base.SaveSettings();
SaveDefaultNetworkAdapterId();
}
}
private void LoadDefaultNetworkAdapterId()
{
try
{
if (File.Exists(FilePath) && JsonNode.Parse(File.ReadAllText(FilePath)) is JsonObject savedSettings)
{
var value = savedSettings[Namespaced(nameof(DefaultNetworkAdapterId))]?.GetValue<string>();
if (!string.IsNullOrWhiteSpace(value))
{
_defaultNetworkAdapterId = value;
}
}
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to load the default network adapter setting.", ex);
}
}
private void SaveDefaultNetworkAdapterId()
{
try
{
var savedSettings = File.Exists(FilePath)
? JsonNode.Parse(File.ReadAllText(FilePath)) as JsonObject
: new JsonObject();
if (savedSettings is null)
{
CoreLogger.LogError("Failed to parse the performance monitor settings file as a JSON object.");
return;
}
savedSettings[Namespaced(nameof(DefaultNetworkAdapterId))] = _defaultNetworkAdapterId;
File.WriteAllText(FilePath, savedSettings.ToJsonString(_serializerOptions));
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to save the default network adapter setting.", ex);
}
}
}

View File

@@ -191,6 +191,16 @@
<data name="Next_Network_Title" xml:space="preserve">
<value>Next network</value>
</data>
<data name="Set_Default_Network_Title" xml:space="preserve">
<value>Set as default</value>
</data>
<data name="Set_Default_Network_Success" xml:space="preserve">
<value>Default network set to {0}</value>
<comment>{0} is the network adapter name.</comment>
</data>
<data name="All_Physical_Network_Adapters" xml:space="preserve">
<value>All physical network adapters</value>
</data>
<data name="NetworkUsage_Widget_Template.Ethernet_Heading" xml:space="preserve">
<value>Ethernet</value>
</data>
@@ -463,4 +473,4 @@
<data name="Disk_Speed_Unit_BinaryBytesPerSec" xml:space="preserve">
<value>Binary bytes per second (KiB/s, MiB/s, GiB/s)</value>
</data>
</root>
</root>