diff --git a/Common/IO/Files/IconHelper.cs b/Common/IO/Files/IconHelper.cs index ffd6d7a..5b2b7fe 100644 --- a/Common/IO/Files/IconHelper.cs +++ b/Common/IO/Files/IconHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Drawing; +using System.IO; using System.Threading.Tasks; using System.Windows; using System.Windows.Interop; @@ -12,66 +13,56 @@ namespace Wokhan.WindowsFirewallNotifier.Common.IO.Files; public static class IconHelper { - private static ConcurrentDictionary procIconLst = new(); + public static BitmapSource SystemIcon { get; } = CreateImage(SystemIcons.WinLogo); + public static BitmapSource ErrorIcon { get; } = CreateImage(SystemIcons.Error); + public static BitmapSource WarningIcon { get; } = CreateImage(SystemIcons.Warning); + public static BitmapSource ApplicationIcon { get; } = CreateImage(SystemIcons.Application); + public static BitmapSource UnknownIcon { get; } = CreateImage(SystemIcons.Question); - public static async Task GetIconAsync(string? path = "") + private static ConcurrentDictionary procIconLst = new() { [""] = UnknownIcon, ["System"] = SystemIcon, ["Unknown"] = ErrorIcon }; + + public static async Task GetIconAsync(string? path) { + path ??= String.Empty; return await Task.Run(() => procIconLst.GetOrAdd(path ?? String.Empty, RetrieveIcon)).ConfigureAwait(false); } private static BitmapSource RetrieveIcon(string path) { - BitmapSource? bitmap; - Icon? ic = null; + if (!path.Contains('\\', StringComparison.Ordinal)) + { + LogHelper.Debug($"Skipped extract icon: '{path}' because path has no directory info."); + } + try { - switch (path) + using var icon = Icon.ExtractAssociatedIcon(path); + if (icon is not null) { - case "System": - case "-": - ic = SystemIcons.WinLogo; - break; - - case "": - case "?error": //FIXME: Use something else? - case "Unknown": - ic = SystemIcons.Error; - break; + return CreateImage(icon); + } + } + catch (ArgumentException) + { + LogHelper.Debug("Unable to extract icon: " + path); + return ErrorIcon; + } + catch (FileNotFoundException) //Undocumented exception + { + LogHelper.Debug("Unable to extract icon: " + path); + return WarningIcon; + } - default: - if (!path.Contains('\\', StringComparison.Ordinal)) - { - LogHelper.Debug($"Skipped extract icon: '{path}' because path has no directory info."); - break; - } + return ApplicationIcon; + } - try - { - ic = Icon.ExtractAssociatedIcon(path); - } - catch (ArgumentException) - { - LogHelper.Debug("Unable to extract icon: " + path); - } - catch (System.IO.FileNotFoundException) //Undocumented exception - { - LogHelper.Debug("Unable to extract icon: " + path); - ic = SystemIcons.Warning; - } - break; - } - ic ??= SystemIcons.Application; + private static BitmapSource CreateImage(Icon source) + { + var bitmap = Imaging.CreateBitmapSourceFromHIcon(source.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); - //FIXME: Resize the icon to save some memory? - bitmap = Imaging.CreateBitmapSourceFromHIcon(ic.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); - bitmap.Freeze(); + bitmap.Freeze(); - return bitmap; - } - finally - { - ic?.Dispose(); - } + return bitmap; } } \ No newline at end of file diff --git a/Common/Net/IP/AF_INET.cs b/Common/Net/IP/AF_INET.cs index e9c9769..c02204c 100644 --- a/Common/Net/IP/AF_INET.cs +++ b/Common/Net/IP/AF_INET.cs @@ -2,7 +2,7 @@ public abstract partial class IPHelper { - internal class AF_INET + internal static class AF_INET { internal const uint IP4 = 2; internal const uint IP6 = 23; diff --git a/Common/Net/IP/Connection.cs b/Common/Net/IP/Connection.cs index be64239..d2ec176 100644 --- a/Common/Net/IP/Connection.cs +++ b/Common/Net/IP/Connection.cs @@ -2,15 +2,13 @@ using System.ComponentModel; using System.Net; using System.Runtime.InteropServices; - -using Windows.Win32; using Windows.Win32.NetworkManagement.IpHelper; using Wokhan.WindowsFirewallNotifier.Common.Logging; namespace Wokhan.WindowsFirewallNotifier.Common.Net.IP; -public class Connection +public record Connection { private const uint NO_ERROR = 0; private const uint ERROR_INSUFFICIENT_BUFFER = 122; @@ -36,14 +34,12 @@ public class Connection public bool IsLoopback { get; private set; } - private MIB_TCP6ROW tcp6MIBRow; + private MIB_TCP6ROW? tcp6MIBRow; + public bool IsMonitored { get; private set; } + private IConnectionOwnerInfo sourceRow; - unsafe delegate uint GetOwnerModuleDelegate(object ROW, TCPIP_OWNER_MODULE_INFO_CLASS infoClass, void* buffer, ref int buffSize); - - GetOwnerModuleDelegate getOwnerModule; - internal Connection(MIB_TCPROW_OWNER_MODULE tcpRow) { sourceRow = tcpRow; @@ -106,46 +102,30 @@ internal Connection(MIB_UDP6ROW_OWNER_MODULE udp6Row) CreationTime = udp6Row.liCreateTimestamp == 0 ? null : DateTime.FromFileTime(udp6Row.liCreateTimestamp); } - private bool EnsureStats(ref bool isAccessDenied) + public bool TryEnableStats() { - if (Protocol != "TCP") - { - throw new InvalidOperationException("Statistics are not available for non-TCP connections. Please check first the connection's protocol."); - } - - if (isAccessDenied || State != ConnectionStatus.ESTABLISHED || IPAddress.IsLoopback(RemoteAddress)) + if (Protocol != "TCP" || State == ConnectionStatus.LISTENING || IPAddress.IsLoopback(RemoteAddress)) { return false; } - var result = new TCP_ESTATS_BANDWIDTH_RW_v0() { EnableCollectionInbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled, EnableCollectionOutbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled }; - var r = sourceRow.SetPerTcpConnectionEStats(ref result, tcp6MIBRow); - if (r != 0) - { - throw new Win32Exception((int)r); - } - - if (result.EnableCollectionInbound != TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled || result.EnableCollectionOutbound != TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled) - { - isAccessDenied = true; - return false; - } + var setting = new TCP_ESTATS_BANDWIDTH_RW_v0() { EnableCollectionInbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled, EnableCollectionOutbound = TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled }; + var r = sourceRow.SetPerTcpConnectionEStats(ref setting, tcp6MIBRow); + + IsMonitored = (r == NO_ERROR && setting.EnableCollectionInbound == TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled && setting.EnableCollectionOutbound == TCP_BOOLEAN_OPTIONAL.TcpBoolOptEnabled); - return true; + return IsMonitored; } + bool _firstPassDone; ulong _lastInboundReadValue; ulong _lastOutboundReadValue; - //TODO: not fond of those ref params, but using an interface prevents me to use local private fields - and using a property with a proper setter would result in a backing field creatino, breaking the initial struct layout. - public (ulong InboundBandwidth, ulong OutboundBandwidth) GetEstimatedBandwidth(ref bool isAccessDenied) + public (ulong InboundBandwidth, ulong OutboundBandwidth, bool IsMonitored) GetEstimatedBandwidth() { - if (!EnsureStats(ref isAccessDenied)) + if (!IsMonitored) { - _lastInboundReadValue = 0; - _lastOutboundReadValue = 0; - - return (0, 0); + return (0, 0, false); } try @@ -154,28 +134,39 @@ private bool EnsureStats(ref bool isAccessDenied) if (rodObjectNullable is null) { - isAccessDenied = true; - return (0, 0); + IsMonitored = false; + return (0, 0, false); } var rodObject = rodObjectNullable.Value; // Fix according to https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-setpertcpconnectionestats // One must subtract the previously read value to get the right one (as reenabling statistics doesn't work as before starting from Win 10 1709) - var inbound = rodObject.InboundBandwidth >= _lastInboundReadValue ? rodObject.InboundBandwidth - _lastInboundReadValue : rodObject.InboundBandwidth; - var outbound = rodObject.OutboundBandwidth >= _lastOutboundReadValue ? rodObject.OutboundBandwidth - _lastOutboundReadValue : rodObject.OutboundBandwidth; + ulong inbound = 0; + ulong outbound = 0; + + // Ignore first pass as data will be wrong (as observed during testing) + if (_firstPassDone) + { + inbound = rodObject.InboundBandwidth >= _lastInboundReadValue ? rodObject.InboundBandwidth - _lastInboundReadValue : rodObject.InboundBandwidth; + outbound = rodObject.OutboundBandwidth >= _lastOutboundReadValue ? rodObject.OutboundBandwidth - _lastOutboundReadValue : rodObject.OutboundBandwidth; + } + + _firstPassDone = true; _lastInboundReadValue = rodObject.InboundBandwidth; _lastOutboundReadValue = rodObject.OutboundBandwidth; - return (inbound, outbound); + return (inbound, outbound, true); } catch (Win32Exception we) when (we.NativeErrorCode == IPHelper.ERROR_NOT_FOUND) { + IsMonitored = false; + _lastInboundReadValue = 0; _lastOutboundReadValue = 0; - return (0, 0); + return (0, 0, false); } } @@ -238,4 +229,14 @@ private bool EnsureStats(ref bool isAccessDenied) } } } + + public void UpdateWith(Connection rawConnection) + { + this.State = rawConnection.State; + this.RemoteAddress = rawConnection.RemoteAddress; + this.RemotePort = rawConnection.RemotePort; + this.IsLoopback = rawConnection.IsLoopback; + + this.tcp6MIBRow = rawConnection.tcp6MIBRow; + } } diff --git a/Common/Net/IP/IConnectionOwnerInfo.cs b/Common/Net/IP/IConnectionOwnerInfo.cs index 7378183..2d60620 100644 --- a/Common/Net/IP/IConnectionOwnerInfo.cs +++ b/Common/Net/IP/IConnectionOwnerInfo.cs @@ -6,8 +6,15 @@ namespace Wokhan.WindowsFirewallNotifier.Common.Net.IP; public interface IConnectionOwnerInfo { - internal uint SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? tcp6Row); - internal TCP_ESTATS_BANDWIDTH_ROD_v0? GetPerTcpConnectionEState(MIB_TCP6ROW? tcp6Row); - internal uint GetOwnerModule(IntPtr buffer, ref uint buffSize); + internal uint SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? tcp6Row) + { + throw new NotImplementedException(); + } + + internal TCP_ESTATS_BANDWIDTH_ROD_v0? GetPerTcpConnectionEState(MIB_TCP6ROW? tcp6Row) + { + throw new NotImplementedException(); + } + internal uint GetOwnerModule(IntPtr buffer, ref uint buffSize); } \ No newline at end of file diff --git a/Common/Net/IP/IPHelper.cs b/Common/Net/IP/IPHelper.cs index 5116a9e..420a9fb 100644 --- a/Common/Net/IP/IPHelper.cs +++ b/Common/Net/IP/IPHelper.cs @@ -16,8 +16,6 @@ using Windows.Win32; using Windows.Win32.NetworkManagement.IpHelper; -using Wokhan.WindowsFirewallNotifier.Common.Logging; - namespace Wokhan.WindowsFirewallNotifier.Common.Net.IP; @@ -121,22 +119,6 @@ public static int GetMaxUserPort() return maxUserPort; } - - internal delegate uint GetOwnerModuleDelegate(IntPtr buffer, ref uint pdwSize); - - - /// - /// Returns details about connection of localPort by process identified by pid. - /// - /// - /// - public static Owner? GetOwner(uint pid, int localPort) - { - var allConn = GetAllConnections(); - var ret = allConn.FirstOrDefault(r => r.LocalPort == localPort && r.OwningPid == pid); - return ret?.OwnerModule; - } - public static IEnumerable GetAllConnections(bool tcpOnly = false) { var ret = GetAllTCPConnections(AF_INET.IP4).Select(tcpConn => new Connection(tcpConn)); diff --git a/Common/Net/IP/NativeOverrides/MIB_UDP6ROW_OWNER_MODULE.cs b/Common/Net/IP/NativeOverrides/MIB_UDP6ROW_OWNER_MODULE.cs index 0266788..1bffa03 100644 --- a/Common/Net/IP/NativeOverrides/MIB_UDP6ROW_OWNER_MODULE.cs +++ b/Common/Net/IP/NativeOverrides/MIB_UDP6ROW_OWNER_MODULE.cs @@ -14,14 +14,4 @@ unsafe uint IConnectionOwnerInfo.GetOwnerModule(IntPtr buffer, ref uint buffSize { return NativeMethods.GetOwnerModuleFromUdp6Entry(this, TCPIP_OWNER_MODULE_INFO_CLASS.TCPIP_OWNER_MODULE_INFO_BASIC, buffer.ToPointer(), ref buffSize); } - - TCP_ESTATS_BANDWIDTH_ROD_v0? IConnectionOwnerInfo.GetPerTcpConnectionEState(MIB_TCP6ROW? tcp6Row) - { - throw new NotImplementedException(); - } - - uint IConnectionOwnerInfo.SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? tcp6Row) - { - throw new NotImplementedException(); - } } diff --git a/Common/Net/IP/NativeOverrides/MIB_UDPROW_OWNER_MODULE.cs b/Common/Net/IP/NativeOverrides/MIB_UDPROW_OWNER_MODULE.cs index 39843fd..78ecdd1 100644 --- a/Common/Net/IP/NativeOverrides/MIB_UDPROW_OWNER_MODULE.cs +++ b/Common/Net/IP/NativeOverrides/MIB_UDPROW_OWNER_MODULE.cs @@ -14,16 +14,4 @@ unsafe uint IConnectionOwnerInfo.GetOwnerModule(IntPtr buffer, ref uint buffSize { return NativeMethods.GetOwnerModuleFromUdpEntry(this, TCPIP_OWNER_MODULE_INFO_CLASS.TCPIP_OWNER_MODULE_INFO_BASIC, buffer.ToPointer(), ref buffSize); } - - TCP_ESTATS_BANDWIDTH_ROD_v0? IConnectionOwnerInfo.GetPerTcpConnectionEState(MIB_TCP6ROW? tcp6Row) - { - //TODO: Check GetUdpStatisticsEx? - //NativeMethods.GetUdpStatisticsEx(, AF_INET.IP4) - throw new NotImplementedException(); - } - - uint IConnectionOwnerInfo.SetPerTcpConnectionEStats(ref TCP_ESTATS_BANDWIDTH_RW_v0 rw, MIB_TCP6ROW? tcp6Row) - { - return 0; - } } diff --git a/Common/Processes/ProcessHelper.cs b/Common/Processes/ProcessHelper.cs index 9731102..449b73f 100644 --- a/Common/Processes/ProcessHelper.cs +++ b/Common/Processes/ProcessHelper.cs @@ -31,25 +31,19 @@ public static void RunElevated(string process) Process.Start(proc); } - //public static string[] GetProcessOwnerWMI(int owningPid, ref Dictionary previousCache) - //{ - // if (previousCache is null) - // { - // using var searcher = new ManagementObjectSearcher("SELECT ProcessId, Name, ExecutablePath, CommandLine FROM Win32_Process"); - // using var results = searcher.Get(); - // // Looks like the first cast to uint is required for this to work (pretty weird if you ask me). - // previousCache = results.Cast().ToDictionary(r => (int)(uint)r["ProcessId"], r => new[] { (string)r["Name"], (string)r["ExecutablePath"], (string)r["CommandLine"] }); - // } - // if (!previousCache.ContainsKey(owningPid)) - // { - // using var searcher = new ManagementObjectSearcher($"SELECT ProcessId, Name, ExecutablePath, CommandLine FROM Win32_Process WHERE ProcessId = {owningPid}"); - // using var r = searcher.Get().Cast().FirstOrDefault(); - // previousCache.Add(owningPid, new[] { (string)r["Name"], (string)r["ExecutablePath"], (string)r["CommandLine"] }); - // } + public static (string Name, string Path, string CommandLine)? GetProcessOwnerWMI(uint owningPid) + { + using var searcher = new ManagementObjectSearcher($"SELECT ProcessId, Name, ExecutablePath, CommandLine FROM Win32_Process WHERE ProcessId = {owningPid}"); + using var r = searcher.Get().Cast().FirstOrDefault(); - // return previousCache[owningPid]; - //} + if (r is null) + { + return null; + } + + return ((string)r["Name"], (string)r["ExecutablePath"], (string)r["CommandLine"]); + } //public static IEnumerable? GetAllServices(uint pid) //{ diff --git a/Common/UI/ViewModels/ConnectionBaseInfo.cs b/Common/UI/ViewModels/ConnectionBaseInfo.cs index 0549107..8cf1999 100644 --- a/Common/UI/ViewModels/ConnectionBaseInfo.cs +++ b/Common/UI/ViewModels/ConnectionBaseInfo.cs @@ -18,12 +18,10 @@ public abstract partial class ConnectionBaseInfo : ObservableObject public uint Pid { get; protected set; } - public string? IconPath { get; protected set; } - protected BitmapSource? _icon; public BitmapSource? Icon { - get => this.GetOrSetValueAsync(() => IconHelper.GetIconAsync(IconPath ?? Path), ref _icon, OnPropertyChanged); + get => this.GetOrSetValueAsync(() => IconHelper.GetIconAsync(Path), ref _icon, OnPropertyChanged); set => this.SetValue(ref _icon, value, OnPropertyChanged); } @@ -41,18 +39,25 @@ public string? TargetHostName public string? Company { get; protected set; } public string? ServiceName { get; protected set; } public string? ServiceDisplayName { get; protected set; } - public string? SourceIP { get; protected set; } - public string? SourcePort { get; set; } + public string SourceIP { get; protected set; } + public string SourcePort { get; set; } [ObservableProperty] private string? _targetIP; + partial void OnTargetIPChanged(string? value) + { + // Resets the TargetHostName property so that it gets asynchronously resolved next time it's needed. + // We don't trigger the resolution here manually since we cannot be sure "something" is interested by its value. + TargetHostName = null; + } + [ObservableProperty] private string? _targetPort; public int RawProtocol { get; protected set; } - public string? Protocol { get; protected set; } + public string Protocol { get; protected set; } public string? Direction { get; protected set; } protected void SetProductInfo() @@ -68,8 +73,8 @@ protected void SetProductInfo() } else if (Path == "System") { - Description = "System"; - ProductName = "System"; + Description = "Windows system process"; + ProductName = Environment.OSVersion.ToString(); Company = String.Empty; } // TODO: To check if stil applies => File.Exists returns false when accessing system32 files from a x86 application; solution would be to target AnyCPU diff --git a/Console/Console.csproj b/Console/Console.csproj index a5742e3..6530bfd 100644 --- a/Console/Console.csproj +++ b/Console/Console.csproj @@ -133,9 +133,4 @@ $([System.String]::Copy(%(Filename)).Replace(".Dummy",".cs")) - - - MonitoredConnection.cs - - \ No newline at end of file diff --git a/Console/Helpers/GeoLocationHelper.cs b/Console/Helpers/GeoLocationHelper.cs index c4e6bc7..02dfe24 100644 --- a/Console/Helpers/GeoLocationHelper.cs +++ b/Console/Helpers/GeoLocationHelper.cs @@ -132,7 +132,7 @@ private static void GeoWatcher_PositionChanged(object? sender, GeoPositionChange public static async Task> ComputeRoute(string target) { - if (target is "127.0.0.1" or "::1" || CurrentCoordinates is null || !IPAddress.TryParse(target, out var _)) + if (CurrentCoordinates is null || !IPAddress.TryParse(target, out var targetIp) || IPAddress.IsLoopback(targetIp)) { return new List(); } diff --git a/Console/UI/Pages/Connections.xaml b/Console/UI/Pages/Connections.xaml index c602eb5..d134b64 100644 --- a/Console/UI/Pages/Connections.xaml +++ b/Console/UI/Pages/Connections.xaml @@ -1,30 +1,27 @@ - - - - - - + + - + @@ -36,7 +33,7 @@ - + @@ -53,7 +50,7 @@ - + @@ -63,20 +60,52 @@ - + - + + - - - - + + + + + + + + + + + + + + + + + + + + + @@ -149,14 +178,21 @@ - + + + + - - + + - - + + - + - - + + - - + + diff --git a/Console/UI/Pages/Connections.xaml.cs b/Console/UI/Pages/Connections.xaml.cs index b63fb44..213b608 100644 --- a/Console/UI/Pages/Connections.xaml.cs +++ b/Console/UI/Pages/Connections.xaml.cs @@ -1,19 +1,27 @@ -using LiveChartsCore.SkiaSharpView; +using CommunityToolkit.Mvvm.ComponentModel; + +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; + using System.Windows.Media; +using Wokhan.Collections; using Wokhan.WindowsFirewallNotifier.Common.Config; using Wokhan.WindowsFirewallNotifier.Common.Net.IP; + using Wokhan.WindowsFirewallNotifier.Common.UI.Themes; using Wokhan.WindowsFirewallNotifier.Console.ViewModels; @@ -33,15 +41,58 @@ public partial class Connections : TimerBasedPage public ObservableCollection AllConnections { get; } = new(); + public GroupedObservableCollection GroupedConnections { get; init; } + + [ObservableProperty] + 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])); + BindingOperations.EnableCollectionSynchronization(AllConnections, uisynclocker); 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) @@ -106,22 +157,31 @@ protected override void OnTimerTick(object? state, ElapsedEventArgs e) for (int i = AllConnections.Count - 1; i >= 0; i--) { var item = AllConnections[i]; - var elapsed = DateTime.Now.Subtract(item.LastSeen).TotalMilliseconds; - if (elapsed > ConnectionTimeoutRemove) - { - lock (locker) - AllConnections.Remove(item); - } - else if (elapsed > ConnectionTimeoutDying) - { - item.IsDying = true; - } - else if (DateTime.Now.Subtract(item.CreationTime).TotalMilliseconds > ConnectionTimeoutNew) + + switch (DateTime.Now.Subtract(item.LastSeen).TotalMilliseconds) { - item.IsNew = false; + case > ConnectionTimeoutRemove: + AllConnections.RemoveAt(i); + break; + + case > ConnectionTimeoutDying: + item.IsDying = true; + break; + + default: + if (DateTime.Now.Subtract(item.CreationTime).TotalMilliseconds > ConnectionTimeoutNew) + { + item.IsNew = false; + } + break; } } - + + foreach(var group in GroupedConnections) + { + group.Key.UpdateBandwidth(group); + } + if (graph.IsVisible) graph.UpdateGraph(); //if (map.IsVisible) map.UpdateMap(); } @@ -129,10 +189,7 @@ protected override void OnTimerTick(object? state, ElapsedEventArgs e) private void AddOrUpdateConnection(Connection connectionInfo) { - MonitoredConnection? lvi; - // TEMP: test to avoid enumerating while modifying (might result in a deadlock, to test carefully!) - lock (locker) - lvi = AllConnections.FirstOrDefault(l => l.Pid == connectionInfo.OwningPid && l.Protocol == connectionInfo.Protocol && l.SourcePort == connectionInfo.LocalPort.ToString()); + MonitoredConnection? lvi = AllConnections.FirstOrDefault(mconn => mconn.Matches(connectionInfo)); if (lvi is not null) { @@ -140,8 +197,7 @@ private void AddOrUpdateConnection(Connection connectionInfo) } else { - lock (locker) - AllConnections.Add(new MonitoredConnection(connectionInfo) { Color = Colors[AllConnections.Count % Colors.Count] }); + AllConnections.Add(new MonitoredConnection(connectionInfo)); } } @@ -156,10 +212,49 @@ internal void UpdateConnectionsColors() _ => LiveChartsCore.Themes.ColorPalletes.FluentDesign.Select(c => c.AsSKColor().ToColor()).ToList(), }; - lock (locker) - for (var i = 0; i < AllConnections.Count; i++) + // TODO: check for concurrent access issue when switching themes while updating + // I removed the lock but it could have been useful here... + if (GroupedConnections is not null) + { + for (var i = 0; i < GroupedConnections.Count; i++) + { + GroupedConnections[i].Key.Color = Colors[i % Colors.Count]; + } + } + } + + + private bool _isResetTextFilterPending; + internal async void ResetTextFilter() + { + if (!_isResetTextFilterPending) + { + _isResetTextFilterPending = true; + await Task.Delay(500).ConfigureAwait(true); + if (!string.IsNullOrWhiteSpace(TextFilter)) + { + connectionsView!.Filter -= ConnectionsView_Filter; + connectionsView.Filter += ConnectionsView_Filter; ; + } + else { - AllConnections[i].Color = Colors[i % Colors.Count]; + connectionsView!.Filter -= ConnectionsView_Filter; } + _isResetTextFilterPending = false; + } + } + + private void ConnectionsView_Filter(object sender, FilterEventArgs e) + { + e.Accepted = true; + //var connection = (ObservableGrouping)e.Item; + + // 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)); } } \ No newline at end of file diff --git a/Console/UI/Pages/EventsLog.xaml b/Console/UI/Pages/EventsLog.xaml index b34f9b2..cfb7dba 100644 --- a/Console/UI/Pages/EventsLog.xaml +++ b/Console/UI/Pages/EventsLog.xaml @@ -22,7 +22,7 @@ - + All TCP Only diff --git a/Console/UI/Pages/EventsLog.xaml.cs b/Console/UI/Pages/EventsLog.xaml.cs index 3c63b00..2b9c59b 100644 --- a/Console/UI/Pages/EventsLog.xaml.cs +++ b/Console/UI/Pages/EventsLog.xaml.cs @@ -135,9 +135,9 @@ private bool FilterTextPredicate(object entryAsObject) var le = (LoggedConnection)entryAsObject; // Note: do not use Remote Host, because this will trigger dns resolution over all entries - return (le.TargetIP is not null && le.TargetIP.StartsWith(TextFilter, StringComparison.Ordinal)) - || (le.ServiceName is not null && le.ServiceName.Contains(TextFilter, StringComparison.OrdinalIgnoreCase)) - || (le.FileName is not null && le.FileName.Contains(TextFilter, StringComparison.OrdinalIgnoreCase)); + return (le.TargetIP?.StartsWith(TextFilter, StringComparison.Ordinal) == true) + || (le.FileName?.Contains(TextFilter, StringComparison.OrdinalIgnoreCase) == true) + || (le.ServiceName?.Contains(TextFilter, StringComparison.OrdinalIgnoreCase) == true); } internal void ResetTcpFilter() diff --git a/Console/UI/Pages/TimerBasedPage.cs b/Console/UI/Pages/TimerBasedPage.cs index 69683dc..0eae62a 100644 --- a/Console/UI/Pages/TimerBasedPage.cs +++ b/Console/UI/Pages/TimerBasedPage.cs @@ -1,14 +1,15 @@ -using System; +using CommunityToolkit.Mvvm.ComponentModel; + +using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Runtime.CompilerServices; using System.Timers; using System.Windows; using System.Windows.Controls; namespace Wokhan.WindowsFirewallNotifier.Console.UI.Pages; -public class TimerBasedPage : Page, INotifyPropertyChanged +[ObservableObject] +public partial class TimerBasedPage : Page { private readonly Timer timer; @@ -22,14 +23,12 @@ public virtual bool IsTrackingEnabled if (timer.Enabled != value) { timer.Enabled = value; - NotifyPropertyChanged(); + OnPropertyChanged(); } } } - public event PropertyChangedEventHandler? PropertyChanged; - private bool? wasRunningWhenUnloaded; private bool isCurrentlyRunning; @@ -59,11 +58,6 @@ private void Timer_Tick(object? sender, ElapsedEventArgs e) } } - protected void NotifyPropertyChanged([CallerMemberName] string? caller = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller)); - } - private void Page_Loaded(object sender, RoutedEventArgs e) { IsTrackingEnabled = wasRunningWhenUnloaded ?? true; diff --git a/Console/ViewModels/GroupedCollectionGroupDescription.cs b/Console/ViewModels/GroupedCollectionGroupDescription.cs new file mode 100644 index 0000000..45a27b0 --- /dev/null +++ b/Console/ViewModels/GroupedCollectionGroupDescription.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +using Wokhan.Collections; + +namespace Wokhan.WindowsFirewallNotifier.Console.ViewModels; + +public class GroupedCollectionGroupDescription : GroupDescription +{ + public override object GroupNameFromItem(object item, int level, CultureInfo culture) + { + if (item is not ObservableGrouping connectionsGroup) + { + throw new ArgumentException($"Unexpected parameter passed: required type of {typeof(ObservableGrouping)}, but parameter 'item' was of type {item?.GetType()}"); + } + + return connectionsGroup.Key!; + } +} + diff --git a/Console/ViewModels/GroupedMonitoredConnections.cs b/Console/ViewModels/GroupedMonitoredConnections.cs new file mode 100644 index 0000000..c6c6f8c --- /dev/null +++ b/Console/ViewModels/GroupedMonitoredConnections.cs @@ -0,0 +1,47 @@ +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 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; } + public Color Color { get; set; } + + [ObservableProperty] + private long _inboundBandwidth; + + [ObservableProperty] + private long _outboundBandwidth; + + public void UpdateBandwidth(ObservableGrouping group) + { + InboundBandwidth = group.Sum(connection => (long)connection.InboundBandwidth); + OutboundBandwidth = group.Sum(connection => (long)connection.OutboundBandwidth); + } + + public override int GetHashCode() + { + return Path.GetHashCode(); + } + + public override bool Equals(object? obj) + { + return Path.Equals(((GroupedMonitoredConnections)obj).Path); + } +} + diff --git a/Console/ViewModels/MonitoredConnection.cs b/Console/ViewModels/MonitoredConnection.cs index 77def08..1b41434 100644 --- a/Console/ViewModels/MonitoredConnection.cs +++ b/Console/ViewModels/MonitoredConnection.cs @@ -2,14 +2,18 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; + using System.Windows.Media; using Wokhan.ComponentModel.Extensions; +using Wokhan.WindowsFirewallNotifier.Common.IO.Files; using Wokhan.WindowsFirewallNotifier.Common.Net.GeoLocation; using Wokhan.WindowsFirewallNotifier.Common.Net.IP; +using Wokhan.WindowsFirewallNotifier.Common.Processes; using Wokhan.WindowsFirewallNotifier.Common.UI.ViewModels; using Wokhan.WindowsFirewallNotifier.Console.Helpers; @@ -26,7 +30,7 @@ public partial class MonitoredConnection : ConnectionBaseInfo [ObservableProperty] private bool _isNew; - + [ObservableProperty] private bool _isDying; @@ -37,7 +41,15 @@ public partial class MonitoredConnection : ConnectionBaseInfo private string? _lastError; [ObservableProperty] - private string? _state; + private string _state; + + partial void OnStateChanged(string value) + { + if (!IsMonitored) + { + IsMonitored = _rawConnection.TryEnableStats(); + } + } [ObservableProperty] private Color _color = Colors.Black; @@ -48,11 +60,12 @@ public partial class MonitoredConnection : ConnectionBaseInfo [ObservableProperty] private ulong _outboundBandwidth; + [ObservableProperty] - private bool _isAccessDenied; + private bool _isMonitored; - private readonly Connection rawConnection; + private Connection _rawConnection; #region Geolocation @@ -80,7 +93,7 @@ private void OnCoordinatesPropertyChanged(string propertyName) private IEnumerable ComputeStraightRoute() { - if (rawConnection.IsLoopback || Protocol == "UDP" && State != "ESTABLISHED" || Owner is null || Coordinates is null || GeoLocationHelper.CurrentCoordinates is null) + if (_rawConnection.IsLoopback || Protocol == "UDP" && State != "ESTABLISHED" || Owner is null || Coordinates is null || GeoLocationHelper.CurrentCoordinates is null) { return NoLocation; } @@ -113,83 +126,69 @@ internal void UpdateStartingPoint() #endregion - public MonitoredConnection(Connection ownerMod) + public MonitoredConnection(Connection rawconnection) { - rawConnection = ownerMod; + _rawConnection = rawconnection; IsNew = true; - - Pid = ownerMod.OwningPid; - SourceIP = ownerMod.LocalAddress.ToString(); - SourcePort = ownerMod.LocalPort.ToString(); - CreationTime = ownerMod.CreationTime ?? DateTime.Now; - Protocol = ownerMod.Protocol; - TargetIP = ownerMod.RemoteAddress.ToString(); - TargetPort = (ownerMod.RemotePort == -1 ? String.Empty : ownerMod.RemotePort.ToString()); + State = rawconnection.State.ToString(); + LastSeen = DateTime.Now; - - _isAccessDenied = Protocol != "TCP"; - //this._state = Enum.GetName(typeof(ConnectionStatus), ownerMod.State); - - if (Pid is 0 or 4) + Pid = rawconnection.OwningPid; + SourceIP = rawconnection.LocalAddress.ToString(); + SourcePort = rawconnection.LocalPort.ToString(); + CreationTime = rawconnection.CreationTime ?? DateTime.Now; + Protocol = rawconnection.Protocol; + TargetIP = rawconnection.RemoteAddress.ToString(); + TargetPort = (rawconnection.RemotePort == -1 ? String.Empty : rawconnection.RemotePort.ToString()); + + if (rawconnection.OwnerModule == Common.Net.IP.Owner.System) { FileName = Properties.Resources.Connection_ProcessFile_System; - Path = Properties.Resources.Connection_ProcessFile_System; - Owner = Properties.Resources.Connection_ProcessOwner_System; + Path = Common.Net.IP.Owner.System.ModulePath; + Owner = Common.Net.IP.Owner.System.ModuleName!; + Icon = IconHelper.SystemIcon; } else { - try - { - //TODO: check if this is solely to retrieve the owner's executable path as we already have the exe in Connection.cs through GetOwningModule - var module = Process.GetProcessById((int)ownerMod.OwningPid)?.MainModule; - if (module is not null) - { - Path = module.FileName ?? Properties.Resources.Connection_ProcessPath_Unknown; - FileName = module.ModuleName ?? Properties.Resources.Connection_ProcessFile_Unknown; - } - } - catch - { - FileName = Properties.Resources.Connection_ProcessFile_UnknownOrClosed; - Path = Properties.Resources.Connection_ProcessPath_Unresolved; - } + Owner = rawconnection.OwnerModule?.ModuleName ?? "Unknown"; - if (ownerMod.OwnerModule is null) + try { - Owner = Properties.Resources.Connection_ProcessOwner_Unknown; - //Path = Path ?? Properties.Resources.Connection_ProcessPath_Unresolved; + var module = Process.GetProcessById((int)rawconnection.OwningPid)?.MainModule; + Path = module?.FileName ?? "Unknown"; + FileName = module?.ModuleName ?? Properties.Resources.Connection_ProcessFile_Unknown; } - else + catch (Win32Exception we) when (we.NativeErrorCode == 5) { - Owner = ownerMod.OwnerModule.ModuleName; - IconPath = ownerMod.OwnerModule.ModulePath; + var r = ProcessHelper.GetProcessOwnerWMI(rawconnection.OwningPid); + Path = r?.Path ?? "Unknown"; + FileName = r?.Name ?? Properties.Resources.Connection_ProcessFile_Unknown; } } SetProductInfo(); } - internal void UpdateValues(Connection b) + internal void UpdateValues(Connection updatedConnection) { - //lvi.LocalAddress = b.LocalAddress; - //lvi.Protocol = b.Protocol; - var remoteIP = b.RemoteAddress.ToString(); - if (this.TargetIP != remoteIP) - { - TargetIP = remoteIP; - // Force reset the target host name by setting it to null (it will be recomputed next) - TargetHostName = null; - } + LastSeen = DateTime.Now; + + // Update the underlying Connection object (remote address, and so on). + _rawConnection.UpdateWith(updatedConnection); - TargetPort = (b.RemotePort == -1 ? String.Empty : b.RemotePort.ToString()); - State = Enum.GetName(typeof(ConnectionStatus), b.State); - if (!_isAccessDenied) + TargetIP = _rawConnection.RemoteAddress.ToString(); + TargetPort = (_rawConnection.RemotePort == -1 ? String.Empty : _rawConnection.RemotePort.ToString()); + State = _rawConnection.State.ToString(); + + if (IsMonitored) { - // TODO: Should use an object here (embedding all parameters as fields) - (InboundBandwidth, OutboundBandwidth) = rawConnection.GetEstimatedBandwidth(ref _isAccessDenied); + (InboundBandwidth, OutboundBandwidth, IsMonitored) = _rawConnection.GetEstimatedBandwidth(); } + } - LastSeen = DateTime.Now; + public bool Matches(Connection connectionInfo) + { + return Pid == connectionInfo.OwningPid && Protocol == connectionInfo.Protocol && SourcePort == connectionInfo.LocalPort.ToString(); } }