diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests/NetworkStatsTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests/NetworkStatsTests.cs new file mode 100644 index 0000000000..ab6cd215d1 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests/NetworkStatsTests.cs @@ -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 GetSnapshots() + { + throw new InvalidOperationException("This provider is not used by these tests."); + } + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests/SettingsManagerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests/SettingsManagerTests.cs new file mode 100644 index 0000000000..2f7508847c --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.PerformanceMonitor.UnitTests/SettingsManagerTests.cs @@ -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); + } + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs index 00e4b1e529..d39f49681c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/NetworkStats.cs @@ -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> _networkCounters = new(); - private bool _networkCounterReadFailureLogged; - - private Dictionary NetworkUsages { get; set; } = new(); - - private Dictionary> 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 _previousSamples = new(); + private readonly Dictionary> _chartValues = new(); + private readonly List _allAdaptersChartValues = new(); + private readonly List _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 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 { bytesSent, bytesReceived, currentBandwidth }; - _networkCounters.Add(instanceName, instanceCounters); - NetChartValues.Add(instanceName, new List()); - 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 snapshots, double elapsedSeconds) + { + var currentInterfaceIds = new HashSet(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(); + _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 chartValues, float usage) + { + lock (chartValues) + { + ChartHelper.AddNextChartValue(usage * 100, chartValues); + } + } + + private void RemoveMissingInterfaces(HashSet 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); + } } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/PhysicalNetworkInterfaceSnapshotProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/PhysicalNetworkInterfaceSnapshotProvider.cs new file mode 100644 index 0000000000..93b157834e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/DevHome/Helpers/PhysicalNetworkInterfaceSnapshotProvider.cs @@ -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 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(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 GetSnapshots(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs index 56fb01896c..ed3ceb5448 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Icons.cs @@ -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 } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt index b3ef2d2bbe..f9c333e30b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/NativeMethods.txt @@ -4,3 +4,5 @@ CreateDXGIFactory1 IDXGIFactory1 IDXGIAdapter1 DXGI_ADAPTER_DESC1 +GetIfTable2 +FreeMibTable diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs index e612c3d1eb..7a6879e1e1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/PerformanceWidgetsPage.cs @@ -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 diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/SettingsManager.cs index 8adf7120c4..98cf6dacd7 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/SettingsManager.cs @@ -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(); + 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); + } + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw index 0ed930d5e4..5f9b1c5824 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Strings/en-US/Resources.resw @@ -191,6 +191,16 @@ Next network + + Set as default + + + Default network set to {0} + {0} is the network adapter name. + + + All physical network adapters + Ethernet @@ -463,4 +473,4 @@ Binary bytes per second (KiB/s, MiB/s, GiB/s) - \ No newline at end of file +