Skip to content

Commit

Permalink
feat: IconHelper now uses static BitmapSources for default icons.
Browse files Browse the repository at this point in the history
fix: still a WIP for the bandwidth computation.
refactor: various refactoring
feat: (WIP) changing the way connections are grouped to allow the global bandwidth to be displayed
  • Loading branch information
wokhan committed Apr 30, 2023
1 parent 5df32a2 commit e59348a
Show file tree
Hide file tree
Showing 19 changed files with 463 additions and 300 deletions.
83 changes: 37 additions & 46 deletions Common/IO/Files/IconHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,66 +13,56 @@ namespace Wokhan.WindowsFirewallNotifier.Common.IO.Files;

public static class IconHelper
{
private static ConcurrentDictionary<string, BitmapSource> 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<BitmapSource?> GetIconAsync(string? path = "")
private static ConcurrentDictionary<string, BitmapSource> procIconLst = new() { [""] = UnknownIcon, ["System"] = SystemIcon, ["Unknown"] = ErrorIcon };

public static async Task<BitmapSource?> 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;
}
}
2 changes: 1 addition & 1 deletion Common/Net/IP/AF_INET.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
83 changes: 42 additions & 41 deletions Common/Net/IP/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
}
13 changes: 10 additions & 3 deletions Common/Net/IP/IConnectionOwnerInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
18 changes: 0 additions & 18 deletions Common/Net/IP/IPHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
using Windows.Win32;
using Windows.Win32.NetworkManagement.IpHelper;

using Wokhan.WindowsFirewallNotifier.Common.Logging;


namespace Wokhan.WindowsFirewallNotifier.Common.Net.IP;

Expand Down Expand Up @@ -121,22 +119,6 @@ public static int GetMaxUserPort()
return maxUserPort;
}


internal delegate uint GetOwnerModuleDelegate(IntPtr buffer, ref uint pdwSize);


/// <summary>
/// Returns details about connection of localPort by process identified by pid.
/// </summary>
/// <param name="row"></param>
/// <returns></returns>
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<Connection> GetAllConnections(bool tcpOnly = false)
{
var ret = GetAllTCPConnections<MIB_TCPTABLE_OWNER_MODULE, MIB_TCPROW_OWNER_MODULE>(AF_INET.IP4).Select(tcpConn => new Connection(tcpConn));
Expand Down
10 changes: 0 additions & 10 deletions Common/Net/IP/NativeOverrides/MIB_UDP6ROW_OWNER_MODULE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
12 changes: 0 additions & 12 deletions Common/Net/IP/NativeOverrides/MIB_UDPROW_OWNER_MODULE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
28 changes: 11 additions & 17 deletions Common/Processes/ProcessHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,19 @@ public static void RunElevated(string process)

Process.Start(proc);
}
//public static string[] GetProcessOwnerWMI(int owningPid, ref Dictionary<int, string[]> 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<ManagementObject>().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<ManagementObject>().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<ManagementObject>().FirstOrDefault();

// return previousCache[owningPid];
//}
if (r is null)
{
return null;
}

return ((string)r["Name"], (string)r["ExecutablePath"], (string)r["CommandLine"]);
}

//public static IEnumerable<string>? GetAllServices(uint pid)
//{
Expand Down
Loading

0 comments on commit e59348a

Please sign in to comment.