diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 796d6de..72c9a47 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -36,11 +36,11 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.100 + dotnet-version: 8.0.300 - name: Build and publish run: dotnet publish ${{ matrix.release }} -c Release ${{ matrix.self-contained }} - name: Zip published files - run: compress-archive -path ./bin/net7.0-windows/${{ matrix.pub-folder }}publish/* -destinationpath ./bin/net7.0-windows/wfn-${{ matrix.release-name }}.zip + run: compress-archive -path ./bin/net8.0-windows/${{ matrix.pub-folder }}publish/* -destinationpath ./bin/net8.0-windows/wfn-${{ matrix.release-name }}.zip - name: Deploy nightly uses: WebFreak001/deploy-nightly@v2.0.0 env: @@ -48,7 +48,7 @@ jobs: with: upload_url: https://uploads.github.com/repos/wokhan/WFN/releases/24573510/assets{?name,label} # find out this value by opening https://api.github.com/repos///releases in your browser and copy the full "upload_url" value including the {?name,label} part release_id: 24573510 # same as above (id can just be taken out the upload_url, it's used to find old releases) - asset_path: ./bin/net7.0-windows/wfn-${{ matrix.release-name }}.zip # path to archive to upload + asset_path: ./bin/net8.0-windows/wfn-${{ matrix.release-name }}.zip # path to archive to upload asset_name: wfn-$$-${{ matrix.release-name }}.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash asset_content_type: application/zip # required by GitHub API max_releases: 5 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b94754f..15ce9fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,11 +24,11 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.100 + dotnet-version: 8.0.300 - name: Build and publish run: dotnet publish ${{ matrix.release }} -c Release ${{ matrix.self-contained }} - name: Zip published files - run: compress-archive -path ./bin/net7.0-windows/${{ matrix.pub-folder }}publish/* -destinationpath ./bin/net7.0-windows/wfn-${{ matrix.release-name }}.zip + run: compress-archive -path ./bin/net8.0-windows/${{ matrix.pub-folder }}publish/* -destinationpath ./bin/net8.0-windows/wfn-${{ matrix.release-name }}.zip - name: Get release id: get_release uses: bruceadams/get-release@v1.2.2 @@ -41,7 +41,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./bin/net7.0-windows/wfn-${{ matrix.release-name }}.zip + asset_path: ./bin/net8.0-windows/wfn-${{ matrix.release-name }}.zip asset_name: wfn-${{ steps.get_release.outputs.tag_name }}-${{ matrix.release-name }}.zip asset_content_type: application/zip \ No newline at end of file diff --git a/Common.Tests/Common.Tests.csproj b/Common.Tests/Common.Tests.csproj index ed9b987..bb2e32b 100644 --- a/Common.Tests/Common.Tests.csproj +++ b/Common.Tests/Common.Tests.csproj @@ -1,6 +1,6 @@  - net7.0-windows + net8.0-windows Wokhan.WindowsFirewallNotifier.Console.Tests Wokhan.WindowsFirewallNotifier.Console.Tests Windows Firewall Notifier - Common Tests @@ -15,7 +15,7 @@ - 5.1.0 + 5.2.0 - + + + all + runtime; build; native; contentfiles; analyzers + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Common.Tests/Helpers/DnsResolverTest.cs b/Common.Tests/Helpers/DnsResolverTest.cs index f9a6656..6bf2f59 100644 --- a/Common.Tests/Helpers/DnsResolverTest.cs +++ b/Common.Tests/Helpers/DnsResolverTest.cs @@ -31,7 +31,7 @@ public void TestDnsResolverResolveIpAddresses() Task.WaitAll(); LogDictEntries(); - Assert.AreEqual("dns.google", ResolvedIPInformation.CachedIPHostEntryDict["8.8.8.8"].resolvedHost); + Assert.That(ResolvedIPInformation.CachedIPHostEntryDict["8.8.8.8"].resolvedHost, Is.EqualTo("dns.google")); Assert.True(ResolvedIPInformation.CachedIPHostEntryDict.Values.Count == 4); ipList = new List diff --git a/Common.Tests/Helpers/NetshHelperTest.cs b/Common.Tests/Helpers/NetshHelperTest.cs index b249727..b3f5359 100644 --- a/Common.Tests/Helpers/NetshHelperTest.cs +++ b/Common.Tests/Helpers/NetshHelperTest.cs @@ -33,9 +33,9 @@ public void TestFindMatchingFilterInfo() Assert.NotNull(result); Assert.False(result.HasErrors); WriteDebugOutput($"name ={result.Name}, description={result.Description}"); - Assert.AreEqual("Default Outbound", result.Name); + Assert.That(result.Name, Is.EqualTo("Default Outbound")); Assert.True(result.Description is not null); - Assert.AreEqual(FiltersContextEnum.FILTERS, result.FoundIn); + Assert.That(result.FoundIn, Is.EqualTo(FiltersContextEnum.FILTERS)); } //TODO: Commented out by @wokhan since test result depends on OS language and will fail on non-english ones @@ -78,9 +78,9 @@ public void TestFindMatchingFilterInfo2Boot3() Assert.NotNull(result); Assert.False(result.HasErrors); WriteDebugOutput($"name={result.Name}, description={result.Description}"); - Assert.AreEqual("Boot Time Filter", result.Name); + Assert.That(result.Name, Is.EqualTo("Boot Time Filter")); Assert.True(result.Description is not null); - Assert.AreEqual(result.FoundIn, FiltersContextEnum.WFPSTATE); + Assert.That(FiltersContextEnum.WFPSTATE, Is.EqualTo(result.FoundIn)); } //TODO: Commented out by @wokhan since test result depends on OS language and will fail on non-english ones @@ -108,7 +108,7 @@ public void TestFindMatchingFilterInfo4() Assert.NotNull(result); Assert.False(result.HasErrors); WriteDebugOutput($"name={result.Name}, description={result.Description}"); - Assert.AreEqual("Port Scanning Prevention Filter", result.Name); + Assert.That(result.Name, Is.EqualTo("Port Scanning Prevention Filter")); Assert.True(result.Description is not null); } [Test, ManualTestCategory] diff --git a/Common/Common.csproj b/Common/Common.csproj index c68a5be..a481664 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -1,6 +1,6 @@  - net7.0-windows + net8.0-windows true Wokhan.WindowsFirewallNotifier.Common Wokhan.WindowsFirewallNotifier.Common @@ -26,21 +26,21 @@ - - + + - - - - + + + + All - - - + + + diff --git a/Common/Net/IP/Connection.cs b/Common/Net/IP/Connection.cs index d2ec176..515103d 100644 --- a/Common/Net/IP/Connection.cs +++ b/Common/Net/IP/Connection.cs @@ -35,6 +35,7 @@ public record Connection public bool IsLoopback { get; private set; } private MIB_TCP6ROW? tcp6MIBRow; + public bool IsMonitored { get; private set; } @@ -47,7 +48,7 @@ internal Connection(MIB_TCPROW_OWNER_MODULE tcpRow) OwningPid = tcpRow.dwOwningPid; LocalAddress = new IPAddress(tcpRow.dwLocalAddr); LocalPort = IPHelper.GetRealPort(tcpRow.dwLocalPort); - if (tcpRow.dwState != (uint)MIB_TCP_STATE.MIB_TCP_STATE_LISTEN) + if (!tcpRow.dwState.Equals(MIB_TCP_STATE.MIB_TCP_STATE_LISTEN)) { RemoteAddress = new IPAddress(tcpRow.dwRemoteAddr); RemotePort = IPHelper.GetRealPort(tcpRow.dwRemotePort); @@ -67,7 +68,7 @@ internal Connection(MIB_TCP6ROW_OWNER_MODULE tcp6Row) OwningPid = tcp6Row.dwOwningPid; LocalAddress = new IPAddress(tcp6Row.ucLocalAddr.AsSpan().ToArray()); LocalPort = IPHelper.GetRealPort(tcp6Row.dwLocalPort); - if (tcp6Row.dwState != (uint)MIB_TCP_STATE.MIB_TCP_STATE_LISTEN) + if (!tcp6Row.dwState.Equals(MIB_TCP_STATE.MIB_TCP_STATE_LISTEN)) { RemoteAddress = new IPAddress(tcp6Row.ucRemoteAddr.AsSpan().ToArray()); RemotePort = IPHelper.GetRealPort(tcp6Row.dwRemotePort); @@ -110,6 +111,7 @@ public bool TryEnableStats() } var setting = new TCP_ESTATS_BANDWIDTH_RW_v0() { EnableCollectionInbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled, EnableCollectionOutbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled }; + // Note: passing tcp6MIBROW as a parameter even for TCP V4 connections, but it will not be used as tcpMIBRow_LH (for V4) is taken directly from sourcerow var r = sourceRow.SetPerTcpConnectionEStats(ref setting, tcp6MIBRow); IsMonitored = (r == NO_ERROR && setting.EnableCollectionInbound == TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled && setting.EnableCollectionOutbound == TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled); @@ -130,6 +132,7 @@ public bool TryEnableStats() try { + // Note: passing tcp6MIBROW as a parameter even for TCP V4 connections, but it will not be used as tcpMIBRow_LH (for V4) is taken directly from sourcerow var rodObjectNullable = sourceRow.GetPerTcpConnectionEState(tcp6MIBRow); if (rodObjectNullable is null) @@ -159,6 +162,15 @@ public bool TryEnableStats() return (inbound, outbound, true); } + catch (InvalidOperationException) + { + IsMonitored = false; + + _lastInboundReadValue = 0; + _lastOutboundReadValue = 0; + + return (0, 0, false); + } catch (Win32Exception we) when (we.NativeErrorCode == IPHelper.ERROR_NOT_FOUND) { IsMonitored = false; @@ -236,7 +248,7 @@ public void UpdateWith(Connection rawConnection) this.RemoteAddress = rawConnection.RemoteAddress; this.RemotePort = rawConnection.RemotePort; this.IsLoopback = rawConnection.IsLoopback; - + this.tcp6MIBRow = rawConnection.tcp6MIBRow; } } diff --git a/Common/Net/IP/NativeOverrides/MIB_TCP6ROW_OWNER_MODULE.cs b/Common/Net/IP/NativeOverrides/MIB_TCP6ROW_OWNER_MODULE.cs index 46d3e40..7099cea 100644 --- a/Common/Net/IP/NativeOverrides/MIB_TCP6ROW_OWNER_MODULE.cs +++ b/Common/Net/IP/NativeOverrides/MIB_TCP6ROW_OWNER_MODULE.cs @@ -1,4 +1,6 @@ using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Windows; using Wokhan.WindowsFirewallNotifier.Common.Net.IP; @@ -19,12 +21,17 @@ unsafe uint IConnectionOwnerInfo.GetOwnerModule(IntPtr buffer, ref uint buffSize unsafe TCP_ESTATS_BANDWIDTH_ROD_v0? IConnectionOwnerInfo.GetPerTcpConnectionEState(MIB_TCP6ROW? tcp6Row) { + if (tcp6Row is null) + { + return null; + } + var rw = new TCP_ESTATS_BANDWIDTH_RW_v0() { EnableCollectionInbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled, EnableCollectionOutbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled }; var rod = new TCP_ESTATS_BANDWIDTH_ROD_v0(); var row = tcp6Row.Value; - var ret = NativeMethods.GetPerTcp6ConnectionEStats(in row, TCP_ESTATS_TYPE.TcpConnectionEstatsBandwidth, (byte*)&rw, 0, MarshalHelper.rwS, null, 0, 0, (byte*)&rod, 0, MarshalHelper.rodS); + var ret = NativeMethods.GetPerTcp6ConnectionEStats(&row, TCP_ESTATS_TYPE.TcpConnectionEstatsBandwidth, (byte*)&rw, 0, MarshalHelper.rwS, null, 0, 0, (byte*)&rod, 0, MarshalHelper.rodS); if (ret == 0 && rw.EnableCollectionInbound == TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled && rw.EnableCollectionOutbound == TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled) { @@ -36,6 +43,11 @@ unsafe uint IConnectionOwnerInfo.GetOwnerModule(IntPtr buffer, ref uint buffSize unsafe uint IConnectionOwnerInfo.SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? tcp6Row) { + if (tcp6Row is null) + { + return 87; // ERROR_INVALID_PARAMETER + } + var row = tcp6Row.Value; fixed (void* rwPtr = &rw) { diff --git a/Common/Net/IP/NativeOverrides/MIB_TCPROW_OWNER_MODULE.cs b/Common/Net/IP/NativeOverrides/MIB_TCPROW_OWNER_MODULE.cs index 1d11203..98320b0 100644 --- a/Common/Net/IP/NativeOverrides/MIB_TCPROW_OWNER_MODULE.cs +++ b/Common/Net/IP/NativeOverrides/MIB_TCPROW_OWNER_MODULE.cs @@ -16,7 +16,7 @@ unsafe uint IConnectionOwnerInfo.GetOwnerModule(IntPtr buffer, ref uint buffSize return NativeMethods.GetOwnerModuleFromTcpEntry(this, TCPIP_OWNER_MODULE_INFO_CLASS.TCPIP_OWNER_MODULE_INFO_BASIC, buffer.ToPointer(), ref buffSize); } - unsafe TCP_ESTATS_BANDWIDTH_ROD_v0? IConnectionOwnerInfo.GetPerTcpConnectionEState(MIB_TCP6ROW? tcp6Row) + unsafe TCP_ESTATS_BANDWIDTH_ROD_v0? IConnectionOwnerInfo.GetPerTcpConnectionEState(MIB_TCP6ROW? _) { var rw = new TCP_ESTATS_BANDWIDTH_RW_v0() { EnableCollectionInbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled, EnableCollectionOutbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled }; var rod = new TCP_ESTATS_BANDWIDTH_ROD_v0(); @@ -36,7 +36,7 @@ unsafe uint IConnectionOwnerInfo.GetOwnerModule(IntPtr buffer, ref uint buffSize return null; } - unsafe uint IConnectionOwnerInfo.SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? tcp6Row) + unsafe uint IConnectionOwnerInfo.SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? _) { uint ret; diff --git a/Common/Processes/ProcessHelper.cs b/Common/Processes/ProcessHelper.cs index 449b73f..d19963d 100644 --- a/Common/Processes/ProcessHelper.cs +++ b/Common/Processes/ProcessHelper.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Management; +using System.Runtime.InteropServices; using System.Windows; using Windows.Win32; @@ -218,7 +219,8 @@ public unsafe static string GetLocalUserOwner(uint pid) return String.Empty; } - if (!NativeMethods.ConvertSidToStringSid(hTokenInformation.User.Sid, out PWSTR SID)) + PWSTR SID = new PWSTR(); + if (!NativeMethods.ConvertSidToStringSid(hTokenInformation.User.Sid, &SID)) { LogHelper.Warning("Unable to retrieve process local user owner: SID cannot be converted!"); return String.Empty; diff --git a/Common/UAP/StorePackageHelper.cs b/Common/UAP/StorePackageHelper.cs index e523856..0ca97cd 100644 --- a/Common/UAP/StorePackageHelper.cs +++ b/Common/UAP/StorePackageHelper.cs @@ -124,7 +124,8 @@ static StorePackageHelper() } finally { - NativeMethods.FreeSid(pSID); + //Already freed since it's a safe handle? + //NativeMethods.FreeSid(pSID); } } finally diff --git a/Console/App.xaml b/Console/App.xaml index 6b4b032..5a35336 100644 --- a/Console/App.xaml +++ b/Console/App.xaml @@ -18,6 +18,7 @@ + @@ -238,6 +239,7 @@ + - + - + + @@ -162,12 +163,13 @@ - + - + + - + + + + + + + + + + + - - - + - - - - - - - - - + + + + + - + - - - - - - - - - + + + + + @@ -295,9 +302,9 @@ - + - + diff --git a/Console/UI/Pages/Connections.xaml.cs b/Console/UI/Pages/Connections.xaml.cs index 213b608..eda7982 100644 --- a/Console/UI/Pages/Connections.xaml.cs +++ b/Console/UI/Pages/Connections.xaml.cs @@ -3,15 +3,8 @@ using LiveChartsCore.SkiaSharpView; using SkiaSharp.Views.WPF; - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.ComponentModel; using System.Data; -using System.Linq; -using System.Threading.Tasks; using System.Timers; using System.Windows; using System.Windows.Data; @@ -39,60 +32,36 @@ public partial class Connections : TimerBasedPage //TODO: let the user pick a color palette for the bandwidth graph & connection private List? Colors; - public ObservableCollection AllConnections { get; } = new(); - public GroupedObservableCollection GroupedConnections { get; init; } + public CollectionView ConnectionsView { get; init; } + [ObservableProperty] - private string? _textFilter = String.Empty; + private string _textFilter = String.Empty; partial void OnTextFilterChanged(string? value) => ResetTextFilter(); - CollectionViewSource connectionsView; - public Connections() { UpdateConnectionsColors(); - AllConnections.CollectionChanged += AllConnections_CollectionChanged; - GroupedConnections = new(connection => new GroupedMonitoredConnections(connection, Colors![GroupedConnections!.Count % Colors.Count])); + Func keyGetter = (MonitoredConnection connection) => GroupedConnections!.Keys.FirstOrDefault(group => group.Path == connection.Path) ?? new GroupedMonitoredConnections(connection, Colors![GroupedConnections!.Count % Colors.Count]); - BindingOperations.EnableCollectionSynchronization(AllConnections, uisynclocker); + GroupedConnections = new(keyGetter); + + //ConnectionsView = new GroupedObservableCollectionsView2(GroupedConnections, keyGetter); + ConnectionsView = new GroupedObservableCollectionView(GroupedConnections, keyGetter); + //GroupedConnections.CollectionChanged += GroupedConnections_CollectionChanged; + //BindingOperations.EnableCollectionSynchronization(GroupedConnections, uisynclocker); + + //ConnectionsViewSource.Source = GroupedConnections; + //ConnectionsViewSource.SortDescriptions.Add(new SortDescription(nameof(GroupedMonitoredConnections.FileName), ListSortDirection.Ascending)); + //ConnectionsViewSource.GroupDescriptions.Add(new PropertyGroupDescription("Key"));//(group => group.Path)); Settings.Default.PropertyChanged += SettingsChanged; InitializeComponent(); - connectionsView = (CollectionViewSource)this.Resources["connectionsView"]; - connectionsView.GroupDescriptions.Add(new GroupedCollectionGroupDescription()); - } - - private void AllConnections_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - Dispatcher.Invoke(() => - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - foreach (MonitoredConnection item in e.NewItems!) - { - GroupedConnections.Add(item); - } - break; - - case NotifyCollectionChangedAction.Remove: - foreach (MonitoredConnection item in e.OldItems!) - { - var group = GroupedConnections.First(group => group.Key.Path == item.Path); - group.Remove(item); - if (group.Count() == 0) - { - GroupedConnections.Remove(group); - } - } - break; - } - }); } private void SettingsChanged(object? sender, PropertyChangedEventArgs? e) @@ -148,48 +117,67 @@ private void Components_VisibilityChanged(object sender, DependencyPropertyChang protected override void OnTimerTick(object? state, ElapsedEventArgs e) { - foreach (var c in IPHelper.GetAllConnections()) + foreach (var connection in IPHelper.GetAllConnections()) { - AddOrUpdateConnection(c); + AddOrUpdateConnection(connection); } - // Start from the end as it's easier to handle removal from a collection when what you removed doesn't impact the actual index / count - for (int i = AllConnections.Count - 1; i >= 0; i--) + DateTime now = DateTime.Now; + + // Start from the end as it's easier to handle removal from a collection when what you removed doesn't impact the actual index / count (also a foreach would fail if we modify the collection) + for (int j = GroupedConnections.Count - 1; j >= 0; j--) { - var item = AllConnections[i]; + var group = GroupedConnections[j]; + var inboundBandwidth = 0UL; + var outboundBandwidth = 0UL; - switch (DateTime.Now.Subtract(item.LastSeen).TotalMilliseconds) + for (int i = group.Count - 1; i >= 0; i--) { - case > ConnectionTimeoutRemove: - AllConnections.RemoveAt(i); - break; - - case > ConnectionTimeoutDying: - item.IsDying = true; - break; + var connection = group[i]; - default: - if (DateTime.Now.Subtract(item.CreationTime).TotalMilliseconds > ConnectionTimeoutNew) + var lastSeenMS = now.Subtract(connection.LastSeen).TotalMilliseconds; + if (lastSeenMS > ConnectionTimeoutRemove) + { + Dispatcher.Invoke(() => { - item.IsNew = false; + if (group.Remove(connection) && group.Count == 0) + { + GroupedConnections.Remove(group); + } + }); + } + else if (lastSeenMS > ConnectionTimeoutDying) + { + connection.IsDying = true; + } + else + { + connection.IsDying = false; + if (connection.IsNew && DateTime.Now.Subtract(connection.CreationTime).TotalMilliseconds > ConnectionTimeoutNew) + { + connection.IsNew = false; } - break; + } + + if (connection.IsMonitored) + { + inboundBandwidth += connection.InboundBandwidth; + outboundBandwidth += connection.OutboundBandwidth; + } } - } - foreach(var group in GroupedConnections) - { - group.Key.UpdateBandwidth(group); + group.Key.InboundBandwidth = inboundBandwidth; + group.Key.OutboundBandwidth = outboundBandwidth; } if (graph.IsVisible) graph.UpdateGraph(); - //if (map.IsVisible) map.UpdateMap(); + if (map.IsVisible) map.UpdateMap(); } private void AddOrUpdateConnection(Connection connectionInfo) { - MonitoredConnection? lvi = AllConnections.FirstOrDefault(mconn => mconn.Matches(connectionInfo)); + MonitoredConnection? lvi = GroupedConnections.Values.FirstOrDefault(mconn => mconn.Matches(connectionInfo)); if (lvi is not null) { @@ -197,7 +185,11 @@ private void AddOrUpdateConnection(Connection connectionInfo) } else { - AllConnections.Add(new MonitoredConnection(connectionInfo)); + Dispatcher.Invoke(() => + { + //using var defer = ConnectionsView.DeferRefresh(); + GroupedConnections.Add(new MonitoredConnection(connectionInfo, ConnectionTimeoutNew)); + }); } } @@ -208,7 +200,7 @@ internal void UpdateConnectionsColors() { ThemeHelper.THEME_LIGHT => LiveChartsCore.Themes.ColorPalletes.FluentDesign.Select(c => c.AsSKColor().ToColor()).ToList(), ThemeHelper.THEME_DARK => LiveChartsCore.Themes.ColorPalletes.FluentDesign.Select(c => c.AsSKColor().ToColor()).ToList(), - ThemeHelper.THEME_SYSTEM => new List { SystemColors.WindowTextColor }, + ThemeHelper.THEME_SYSTEM => [SystemColors.WindowTextColor], _ => LiveChartsCore.Themes.ColorPalletes.FluentDesign.Select(c => c.AsSKColor().ToColor()).ToList(), }; @@ -233,28 +225,27 @@ internal async void ResetTextFilter() await Task.Delay(500).ConfigureAwait(true); if (!string.IsNullOrWhiteSpace(TextFilter)) { - connectionsView!.Filter -= ConnectionsView_Filter; - connectionsView.Filter += ConnectionsView_Filter; ; + ConnectionsView!.Filter = ConnectionsView_Filter; } else { - connectionsView!.Filter -= ConnectionsView_Filter; + ConnectionsView!.Filter = null; } _isResetTextFilterPending = false; } } - private void ConnectionsView_Filter(object sender, FilterEventArgs e) + private bool ConnectionsView_Filter(object obj) { - e.Accepted = true; - //var connection = (ObservableGrouping)e.Item; - + var connection = (MonitoredConnection)obj; + // Note: do not use Remote Host, because this will trigger dns resolution over all entries // TODO: fix since we're now using ObservableGrouping (with already grouped collection) - //e.Accepted = ((connection.FileName?.Contains(TextFilter, StringComparison.OrdinalIgnoreCase) == true) - // || (connection.ServiceName?.Contains(TextFilter, StringComparison.OrdinalIgnoreCase) == true) - // || (connection.TargetIP?.StartsWith(TextFilter, StringComparison.Ordinal) == true) - // || connection.State.StartsWith(TextFilter, StringComparison.Ordinal) - // || connection.Protocol.Contains(TextFilter, StringComparison.OrdinalIgnoreCase)); + return ((connection.FileName?.Contains(TextFilter, StringComparison.OrdinalIgnoreCase) == true) + || (connection.ServiceName?.Contains(TextFilter, StringComparison.OrdinalIgnoreCase) == true) + || (connection.TargetIP?.StartsWith(TextFilter, StringComparison.Ordinal) == true) + || connection.State.StartsWith(TextFilter, StringComparison.Ordinal) + || connection.Protocol.Contains(TextFilter, StringComparison.OrdinalIgnoreCase) + || connection.SourcePort.Contains(TextFilter)); } } \ No newline at end of file diff --git a/Console/UI/Pages/EventsLog.xaml.cs b/Console/UI/Pages/EventsLog.xaml.cs index 2b9c59b..c733ae5 100644 --- a/Console/UI/Pages/EventsLog.xaml.cs +++ b/Console/UI/Pages/EventsLog.xaml.cs @@ -2,9 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using System; using System.ComponentModel; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; diff --git a/Console/UI/Pages/Rules.xaml.cs b/Console/UI/Pages/Rules.xaml.cs index dcfba46..8336899 100644 --- a/Console/UI/Pages/Rules.xaml.cs +++ b/Console/UI/Pages/Rules.xaml.cs @@ -1,10 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Windows; using System.Windows.Controls; diff --git a/Console/UI/Pages/Status.xaml.cs b/Console/UI/Pages/Status.xaml.cs index f27f2e7..e1821e5 100644 --- a/Console/UI/Pages/Status.xaml.cs +++ b/Console/UI/Pages/Status.xaml.cs @@ -1,10 +1,8 @@ using CommunityToolkit.Mvvm.Input; -using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; -using System.Windows; using System.Windows.Controls; using Wokhan.WindowsFirewallNotifier.Common.Config; diff --git a/Console/UI/Pages/TimerBasedPage.cs b/Console/UI/Pages/TimerBasedPage.cs index 0eae62a..dee0908 100644 --- a/Console/UI/Pages/TimerBasedPage.cs +++ b/Console/UI/Pages/TimerBasedPage.cs @@ -1,7 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; -using System; -using System.Collections.Generic; using System.Timers; using System.Windows; using System.Windows.Controls; @@ -11,7 +9,7 @@ namespace Wokhan.WindowsFirewallNotifier.Console.UI.Pages; [ObservableObject] public partial class TimerBasedPage : Page { - private readonly Timer timer; + private readonly System.Timers.Timer timer; public virtual List Intervals { get; } = new List { 0.5, 1, 5, 10 }; diff --git a/Console/UI/Windows/About.xaml.cs b/Console/UI/Windows/About.xaml.cs index 6ab5dbf..a93d9d3 100644 --- a/Console/UI/Windows/About.xaml.cs +++ b/Console/UI/Windows/About.xaml.cs @@ -1,7 +1,5 @@ -using System; -using System.Windows; +using System.Windows; using System.Reflection; -using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Navigation; using Wokhan.WindowsFirewallNotifier.Common.Processes; diff --git a/Console/UI/Windows/MainWindow.xaml.cs b/Console/UI/Windows/MainWindow.xaml.cs index 6b92c33..fc885f4 100644 --- a/Console/UI/Windows/MainWindow.xaml.cs +++ b/Console/UI/Windows/MainWindow.xaml.cs @@ -1,12 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.Win32; - -using System; using System.ComponentModel; -using System.Linq; using System.Reflection; -using System.Threading; using System.Windows; using System.Windows.Threading; @@ -46,8 +41,8 @@ public MainWindow() timer = new Timer(Timer_Tick, null, 1000, 1000); - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - Application.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException; + //AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + //Application.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException; } private void Timer_Tick(object? _) diff --git a/Console/UI/Windows/Options.xaml.cs b/Console/UI/Windows/Options.xaml.cs index a422958..f6f3f3c 100644 --- a/Console/UI/Windows/Options.xaml.cs +++ b/Console/UI/Windows/Options.xaml.cs @@ -1,9 +1,6 @@ using CommunityToolkit.Mvvm.Input; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Reflection; using System.Windows; using System.Windows.Controls.Primitives; diff --git a/Console/ViewModels/ExposedInterfaceView.cs b/Console/ViewModels/ExposedInterfaceView.cs index 435fa23..71d42ae 100644 --- a/Console/ViewModels/ExposedInterfaceView.cs +++ b/Console/ViewModels/ExposedInterfaceView.cs @@ -1,6 +1,4 @@ -using System; -using System.ComponentModel; -using System.Linq; +using System.ComponentModel; using System.Net.NetworkInformation; namespace Wokhan.WindowsFirewallNotifier.Console.ViewModels; diff --git a/Console/ViewModels/FirewallStatusWrapper.cs b/Console/ViewModels/FirewallStatusWrapper.cs index 5e24a65..f6e4197 100644 --- a/Console/ViewModels/FirewallStatusWrapper.cs +++ b/Console/ViewModels/FirewallStatusWrapper.cs @@ -1,7 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; -using System; using System.Diagnostics.CodeAnalysis; + using Wokhan.WindowsFirewallNotifier.Common.Net.WFP; using Wokhan.WindowsFirewallNotifier.Console.Helpers; diff --git a/Console/ViewModels/GroupedCollectionGroupDescription.cs b/Console/ViewModels/GroupedCollectionGroupDescription.cs index 45a27b0..6b2a69b 100644 --- a/Console/ViewModels/GroupedCollectionGroupDescription.cs +++ b/Console/ViewModels/GroupedCollectionGroupDescription.cs @@ -1,21 +1,34 @@ -using System; -using System.ComponentModel; +using System.ComponentModel; using System.Globalization; - -using Wokhan.Collections; +using System.Runtime.CompilerServices; namespace Wokhan.WindowsFirewallNotifier.Console.ViewModels; -public class GroupedCollectionGroupDescription : GroupDescription +/// +/// Creates a GroupDescription to use along with a CollectionView to use an already grouped collection with a WPF Binding. +/// Internally uses a to attach the key to the object itself and "cache" it (kind of). +/// +/// +/// +public class GroupedCollectionGroupDescription : GroupDescription where TKey : class where TItem : class { + private readonly Func keyGetter; + + private ConditionalWeakTable cwt = []; + + public GroupedCollectionGroupDescription(Func keyGetter) : base() + { + this.keyGetter = keyGetter; + } + public override object GroupNameFromItem(object item, int level, CultureInfo culture) { - if (item is not ObservableGrouping connectionsGroup) + if (item is not TItem itm) { - throw new ArgumentException($"Unexpected parameter passed: required type of {typeof(ObservableGrouping)}, but parameter 'item' was of type {item?.GetType()}"); + throw new ArgumentException($"Unexpected parameter passed: required type of {typeof(TItem)}, but parameter 'item' was of type {item?.GetType()}"); } - return connectionsGroup.Key!; + return cwt.GetValue(itm, item => keyGetter(itm)!); } } diff --git a/Console/ViewModels/GroupedMonitoredConnections.cs b/Console/ViewModels/GroupedMonitoredConnections.cs index c6c6f8c..e281bdb 100644 --- a/Console/ViewModels/GroupedMonitoredConnections.cs +++ b/Console/ViewModels/GroupedMonitoredConnections.cs @@ -1,47 +1,59 @@ using CommunityToolkit.Mvvm.ComponentModel; - -using System.Linq; using System.Windows.Media; -using Wokhan.Collections; - namespace Wokhan.WindowsFirewallNotifier.Console.ViewModels; -public partial class GroupedMonitoredConnections : ObservableObject +public partial class GroupedMonitoredConnections : ObservableObject, IComparable { - public GroupedMonitoredConnections(MonitoredConnection connection, Color color) - { - Path = connection.Path!; - FileName = connection.FileName!; - ProductName = connection.ProductName; - Color = color; - } - public string FileName { get; init; } public string Path { get; init; } public string? ProductName { get; init; } + + private SolidColorBrush? _brush; + public SolidColorBrush Brush => _brush ??= new SolidColorBrush(Color); + public Color Color { get; set; } [ObservableProperty] - private long _inboundBandwidth; + private ulong _inboundBandwidth; [ObservableProperty] - private long _outboundBandwidth; + private ulong _outboundBandwidth; - public void UpdateBandwidth(ObservableGrouping group) + public GroupedMonitoredConnections(MonitoredConnection connection, Color color) { - InboundBandwidth = group.Sum(connection => (long)connection.InboundBandwidth); - OutboundBandwidth = group.Sum(connection => (long)connection.OutboundBandwidth); + + Path = connection.Path!; + FileName = connection.FileName!; + ProductName = connection.ProductName; + Color = color; + + connection.Color = color; } - public override int GetHashCode() + public int CompareTo(object? obj) { - return Path.GetHashCode(); + return Path.CompareTo((obj as GroupedMonitoredConnections)?.Path); } public override bool Equals(object? obj) { - return Path.Equals(((GroupedMonitoredConnections)obj).Path); + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (ReferenceEquals(obj, null)) + { + return false; + } + + return Path.Equals((obj as GroupedMonitoredConnections)?.Path); + } + + public override int GetHashCode() + { + return Path.GetHashCode(); } } diff --git a/Console/ViewModels/GroupedObservableCollectionView.cs b/Console/ViewModels/GroupedObservableCollectionView.cs new file mode 100644 index 0000000..5380591 --- /dev/null +++ b/Console/ViewModels/GroupedObservableCollectionView.cs @@ -0,0 +1,82 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Windows.Data; + +using Wokhan.Collections; + +namespace Wokhan.WindowsFirewallNotifier.Console.ViewModels; + +/// +/// Allows to create a from a , hence having a view from already grouped data (hence mimicking the IsGroupSourced from UWP/WinUI, which isn't available in WPF). +/// Note: this would be useless if had an property exposing all Values, which we would then use as a source for a standard . +/// +/// Type of the key the collection is grouped by +/// Type of each item in the collection groups +public class GroupedObservableCollectionView : ListCollectionView where TKey : class + where TItem : class +{ + private readonly ObservableCollection typedSourceCollection; + + /// + /// Creates a new from an already grouped collection, grouping by the property specified by . + /// + /// Source collection to get the view for. This collection must already be grouped by the key retrieved by keyGetter."/> + /// Property to group on (must be of type) + public GroupedObservableCollectionView(GroupedObservableCollection collection, string groupByPropertyName) : base(new ObservableCollection(collection.SelectMany(item => item))) + { + ArgumentNullException.ThrowIfNull(groupByPropertyName); + + typedSourceCollection = (ObservableCollection)SourceCollection; + + this.GroupDescriptions.Add(new PropertyGroupDescription(groupByPropertyName)); + + collection.CollectionChanged += SourceCollection_CollectionChanged; + } + + /// + /// Builds a new from an already grouped collection, grouping using the specifier to access either a composite property or a new object as a key. + /// + /// Source collection to get the view for. This collection must already be grouped by the key retrieved by keyGetter."/> + /// Method to retrieve the key to group values on + public GroupedObservableCollectionView(GroupedObservableCollection collection, Func keyGetter) : base(new ObservableCollection(collection.SelectMany(item => item))) + { + ArgumentNullException.ThrowIfNull(keyGetter); + + typedSourceCollection = (ObservableCollection)SourceCollection; + + this.GroupDescriptions.Add(new GroupedCollectionGroupDescription(keyGetter)); + + collection.CollectionChanged += SourceCollection_CollectionChanged; + } + + private void SourceCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (var item in e.NewItems!.Cast>()) + { + item.CollectionChanged += Item_CollectionChanged; + } + } + } + + private void Item_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + ArgumentNullException.ThrowIfNull(sender); + + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (var it in e.NewItems!) + { + typedSourceCollection.Add((TItem)it); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (var it in e.OldItems!) + { + typedSourceCollection.Remove((TItem)it); + } + } + } +} \ No newline at end of file diff --git a/Console/ViewModels/MonitoredConnection.Dummy.cs b/Console/ViewModels/MonitoredConnection.Dummy.cs index d6a9a78..dced0bc 100644 --- a/Console/ViewModels/MonitoredConnection.Dummy.cs +++ b/Console/ViewModels/MonitoredConnection.Dummy.cs @@ -1,7 +1,4 @@ - -using Windows.Win32.NetworkManagement.IpHelper; - -using Wokhan.WindowsFirewallNotifier.Common.Net.IP; +using Wokhan.WindowsFirewallNotifier.Common.Net.IP; namespace Wokhan.WindowsFirewallNotifier.Console.ViewModels; @@ -11,7 +8,7 @@ public class ConnectionDummy : MonitoredConnection //public ConnectionDummy() { }//: base(new MIB_TCPROW_OWNER_MODULE()) { } - public ConnectionDummy(Connection ownerMod) : base(ownerMod) + public ConnectionDummy(Connection ownerMod) : base(ownerMod, null) { } } diff --git a/Console/ViewModels/MonitoredConnection.cs b/Console/ViewModels/MonitoredConnection.cs index 1b41434..9725865 100644 --- a/Console/ViewModels/MonitoredConnection.cs +++ b/Console/ViewModels/MonitoredConnection.cs @@ -1,13 +1,9 @@ using CommunityToolkit.Mvvm.ComponentModel; -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; - using System.Windows.Media; +using System.Windows.Shapes; using Wokhan.ComponentModel.Extensions; using Wokhan.WindowsFirewallNotifier.Common.IO.Files; @@ -23,13 +19,15 @@ public partial class MonitoredConnection : ConnectionBaseInfo { public string Owner { get; private set; } + public string? WindowTitle { get; private set; } + public DateTime LastSeen { get; private set; } [ObservableProperty] private bool _isSelected; [ObservableProperty] - private bool _isNew; + private bool _isNew = true; [ObservableProperty] private bool _isDying; @@ -41,8 +39,11 @@ public partial class MonitoredConnection : ConnectionBaseInfo private string? _lastError; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(StateImage))] private string _state; + public Path StateImage => (Path)((App)App.Current).TryFindResource("CONN_STATE_" + _state); + partial void OnStateChanged(string value) { if (!IsMonitored) @@ -52,8 +53,12 @@ partial void OnStateChanged(string value) } [ObservableProperty] + [NotifyPropertyChangedFor(nameof(Brush))] private Color _color = Colors.Black; + private Brush? _brush; + public Brush Brush => _brush ??= new SolidColorBrush(_color); + [ObservableProperty] private ulong _inboundBandwidth; @@ -98,7 +103,7 @@ private IEnumerable ComputeStraightRoute() return NoLocation; } - return new[] { GeoLocationHelper.CurrentCoordinates, Coordinates }; + return [GeoLocationHelper.CurrentCoordinates, Coordinates]; } @@ -126,13 +131,16 @@ internal void UpdateStartingPoint() #endregion - public MonitoredConnection(Connection rawconnection) + public MonitoredConnection(Connection rawconnection, double? connectionTimeoutNew) { _rawConnection = rawconnection; - IsNew = true; State = rawconnection.State.ToString(); - + + if (connectionTimeoutNew is not null && rawconnection.CreationTime is not null) + { + IsNew = DateTime.Now.Subtract(rawconnection.CreationTime.Value).TotalMilliseconds <= connectionTimeoutNew; + } LastSeen = DateTime.Now; Pid = rawconnection.OwningPid; SourceIP = rawconnection.LocalAddress.ToString(); @@ -155,7 +163,12 @@ public MonitoredConnection(Connection rawconnection) try { - var module = Process.GetProcessById((int)rawconnection.OwningPid)?.MainModule; + var process = Process.GetProcessById((int)rawconnection.OwningPid); + + // Not working yet (for Firefox at least, it returns the wrong title everytime, taking the active tab) + // WindowTitle = process?.MainWindowTitle; + + var module = process?.MainModule; Path = module?.FileName ?? "Unknown"; FileName = module?.ModuleName ?? Properties.Resources.Connection_ProcessFile_Unknown; } @@ -180,7 +193,7 @@ internal void UpdateValues(Connection updatedConnection) TargetIP = _rawConnection.RemoteAddress.ToString(); TargetPort = (_rawConnection.RemotePort == -1 ? String.Empty : _rawConnection.RemotePort.ToString()); State = _rawConnection.State.ToString(); - + if (IsMonitored) { (InboundBandwidth, OutboundBandwidth, IsMonitored) = _rawConnection.GetEstimatedBandwidth(); diff --git a/Notifier/Notifier.csproj b/Notifier/Notifier.csproj index cedd8c4..420a870 100644 --- a/Notifier/Notifier.csproj +++ b/Notifier/Notifier.csproj @@ -1,6 +1,6 @@  - net7.0-windows + net8.0-windows WinExe true true