diff --git a/Source/GlobalAssemblyInfo.cs b/Source/GlobalAssemblyInfo.cs index b557cda1cd..90884159c7 100644 --- a/Source/GlobalAssemblyInfo.cs +++ b/Source/GlobalAssemblyInfo.cs @@ -6,5 +6,5 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("2025.1.18.0")] -[assembly: AssemblyFileVersion("2025.1.18.0")] +[assembly: AssemblyVersion("2025.3.16.0")] +[assembly: AssemblyFileVersion("2025.3.16.0")] diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index 783c4482cd..e074ddeb6b 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -771,6 +771,15 @@ public static string ApplicationName_DNSLookup { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die Hosts File Editor ähnelt. + /// + public static string ApplicationName_HostsFileEditor { + get { + return ResourceManager.GetString("ApplicationName_HostsFileEditor", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die HTTP Headers ähnelt. /// @@ -1761,6 +1770,15 @@ public static string CommandLineArguments { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die Comment ähnelt. + /// + public static string Comment { + get { + return ResourceManager.GetString("Comment", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Community ähnelt. /// @@ -4532,6 +4550,24 @@ public static string Hosts { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die Hosts File Editor ähnelt. + /// + public static string HostsFileEditor { + get { + return ResourceManager.GetString("HostsFileEditor", resourceCulture); + } + } + + /// + /// Sucht eine lokalisierte Zeichenfolge, die To edit the hosts file, the application must be started with elevated rights! ähnelt. + /// + public static string HostsFileEditorAdminMessage { + get { + return ResourceManager.GetString("HostsFileEditorAdminMessage", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die HotKeys ähnelt. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index b568f36d54..acd237281f 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3879,4 +3879,16 @@ Right-click for more options. Profile file + + Hosts File Editor + + + Hosts File Editor + + + To edit the hosts file, the application must be started with elevated rights! + + + Comment + \ No newline at end of file diff --git a/Source/NETworkManager.Models/AWS/AWSProfile.cs b/Source/NETworkManager.Models/AWS/AWSProfile.cs index cc4240e64b..0fcdc08377 100644 --- a/Source/NETworkManager.Models/AWS/AWSProfile.cs +++ b/Source/NETworkManager.Models/AWS/AWSProfile.cs @@ -6,10 +6,10 @@ public static class AWSProfile { public static List GetDefaultList() { - return new List - { - new(false, "default", "eu-central-1"), - new(false, "default", "us-east-1") - }; + return + [ + new AWSProfileInfo(false, "default", "eu-central-1"), + new AWSProfileInfo(false, "default", "us-east-1") + ]; } } \ No newline at end of file diff --git a/Source/NETworkManager.Models/AWS/AWSProfileInfo.cs b/Source/NETworkManager.Models/AWS/AWSProfileInfo.cs index c10d98202e..e453f8a70b 100644 --- a/Source/NETworkManager.Models/AWS/AWSProfileInfo.cs +++ b/Source/NETworkManager.Models/AWS/AWSProfileInfo.cs @@ -1,7 +1,7 @@ namespace NETworkManager.Models.AWS; /// -/// Class is used to store informations about an AWS profile. +/// Class is used to store information about an AWS profile. /// public class AWSProfileInfo { @@ -15,7 +15,7 @@ public AWSProfileInfo() /// /// Create an instance of with parameters. /// - /// . + /// . /// . /// . public AWSProfileInfo(bool isEnabled, string profile, string region) diff --git a/Source/NETworkManager.Models/ApplicationManager.cs b/Source/NETworkManager.Models/ApplicationManager.cs index 072dacd875..c179b5d774 100644 --- a/Source/NETworkManager.Models/ApplicationManager.cs +++ b/Source/NETworkManager.Models/ApplicationManager.cs @@ -1,8 +1,8 @@ -using System; +using MahApps.Metro.IconPacks; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Controls; -using MahApps.Metro.IconPacks; namespace NETworkManager.Models; @@ -92,6 +92,9 @@ public static Canvas GetIcon(ApplicationName name) case ApplicationName.SNTPLookup: canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.ClockCheckOutline }); break; + case ApplicationName.HostsFileEditor: + canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.FileEditOutline }); + break; case ApplicationName.DiscoveryProtocol: canvas.Children.Add(new PackIconMaterial { Kind = PackIconMaterialKind.SwapHorizontal }); break; diff --git a/Source/NETworkManager.Models/ApplicationName.cs b/Source/NETworkManager.Models/ApplicationName.cs index 63f0665cbb..29196133fb 100644 --- a/Source/NETworkManager.Models/ApplicationName.cs +++ b/Source/NETworkManager.Models/ApplicationName.cs @@ -90,6 +90,11 @@ public enum ApplicationName /// SNTPLookup, + /// + /// Hosts file editor application. + /// + HostsFileEditor, + /// /// Discovery protocol application. /// diff --git a/Source/NETworkManager.Models/Export/ExportManager.HostsFileEntry.cs b/Source/NETworkManager.Models/Export/ExportManager.HostsFileEntry.cs new file mode 100644 index 0000000000..2eb7ae3e58 --- /dev/null +++ b/Source/NETworkManager.Models/Export/ExportManager.HostsFileEntry.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using NETworkManager.Models.HostsFileEditor; +using Newtonsoft.Json; + +namespace NETworkManager.Models.Export; + +public static partial class ExportManager +{ + /// + /// Method to export objects from type to a file. + /// + /// Path to the export file. + /// Allowed are CSV, XML or JSON. + /// Objects as to export. + public static void Export(string filePath, ExportFileType fileType, IReadOnlyList collection) + { + switch (fileType) + { + case ExportFileType.Csv: + CreateCsv(collection, filePath); + break; + case ExportFileType.Xml: + CreateXml(collection, filePath); + break; + case ExportFileType.Json: + CreateJson(collection, filePath); + break; + case ExportFileType.Txt: + default: + throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null); + } + } + + /// + /// Creates a CSV file from the given collection. + /// + /// Objects as to export. + /// Path to the export file. + private static void CreateCsv(IEnumerable collection, string filePath) + { + var stringBuilder = new StringBuilder(); + + stringBuilder.AppendLine( + $"{nameof(HostsFileEntry.IsEnabled)},{nameof(HostsFileEntry.IPAddress)},{nameof(HostsFileEntry.Hostname)},{nameof(HostsFileEntry.Comment)}"); + + foreach (var info in collection) + stringBuilder.AppendLine($"{info.IsEnabled},{info.IPAddress},{info.Hostname},{info.Comment}"); + + File.WriteAllText(filePath, stringBuilder.ToString()); + } + + /// + /// Creates a XML file from the given collection. + /// + /// Objects as to export. + /// Path to the export file. + private static void CreateXml(IEnumerable collection, string filePath) + { + var document = new XDocument(DefaultXDeclaration, + new XElement(ApplicationName.HostsFileEditor.ToString(), + new XElement(nameof(HostsFileEntry) + "s", + from info in collection + select + new XElement(nameof(HostsFileEntry), + new XElement(nameof(HostsFileEntry.IsEnabled), info.IsEnabled), + new XElement(nameof(HostsFileEntry.IPAddress), info.IPAddress), + new XElement(nameof(HostsFileEntry.Hostname), info.Hostname), + new XElement(nameof(HostsFileEntry.Comment), info.Comment))))); + + document.Save(filePath); + } + + /// + /// Creates a JSON file from the given collection. + /// + /// Objects as to export. + /// Path to the export file. + private static void CreateJson(IReadOnlyList collection, string filePath) + { + var jsonData = new object[collection.Count]; + + for (var i = 0; i < collection.Count; i++) + jsonData[i] = new + { + collection[i].IsEnabled, + collection[i].IPAddress, + collection[i].Hostname, + collection[i].Comment + }; + + File.WriteAllText(filePath, JsonConvert.SerializeObject(jsonData, Formatting.Indented)); + } +} diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs new file mode 100644 index 0000000000..7f487aa89c --- /dev/null +++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using log4net; +using NETworkManager.Utilities; + +namespace NETworkManager.Models.HostsFileEditor; + +public static class HostsFileEditor +{ +#region Events + public static event EventHandler HostsFileChanged; + + private static void OnHostsFileChanged() + { + Log.Debug("OnHostsFileChanged - Hosts file changed."); + HostsFileChanged?.Invoke(null, EventArgs.Empty); + } + #endregion + + #region Variables + private static readonly ILog Log = LogManager.GetLogger(typeof(HostsFileEditor)); + + private static readonly FileSystemWatcher HostsFileWatcher; + + /// + /// Path to the hosts file. + /// + private static string HostsFilePath => Path.Combine(Environment.SystemDirectory, "drivers", "etc", "hosts"); + + /// + /// Example values in the hosts file that should be ignored. + /// + private static readonly HashSet<(string IPAddress, string Hostname)> ExampleValuesToIgnore = + [ + ("102.54.94.97", "rhino.acme.com"), + ("38.25.63.10", "x.acme.com") + ]; + + /// + /// Regex to match a hosts file entry with optional comments, supporting IPv4, IPv6, and hostnames + /// + private static readonly Regex HostsFileEntryRegex = new(RegexHelper.HostsEntryRegex); + + #endregion + + #region Constructor + + static HostsFileEditor() + { + // Create a file system watcher to monitor changes to the hosts file + try + { + Log.Debug("HostsFileEditor - Creating file system watcher for hosts file..."); + + // Create the file system watcher + HostsFileWatcher = new FileSystemWatcher(); + HostsFileWatcher.Path = Path.GetDirectoryName(HostsFilePath) ?? throw new InvalidOperationException("Hosts file path is invalid."); + HostsFileWatcher.Filter = Path.GetFileName(HostsFilePath) ?? throw new InvalidOperationException("Hosts file name is invalid."); + HostsFileWatcher.NotifyFilter = NotifyFilters.LastWrite; + + // Maybe fired twice. This is a known bug/feature. + // See: https://stackoverflow.com/questions/1764809/filesystemwatcher-changed-event-is-raised-twice + HostsFileWatcher.Changed += (_, _) => OnHostsFileChanged(); + + // Enable the file system watcher + HostsFileWatcher.EnableRaisingEvents = true; + + Log.Debug("HostsFileEditor - File system watcher for hosts file created."); + } + catch (Exception ex) + { + Log.Error("Failed to create file system watcher for hosts file.", ex); + } + } + #endregion + + #region Methods + public static Task> GetHostsFileEntriesAsync() + { + return Task.Run(GetHostsFileEntries); + } + + /// + /// + /// + /// + private static IEnumerable GetHostsFileEntries() + { + var hostsFileLines = File.ReadAllLines(HostsFilePath); + + // Parse the hosts file content + var entries = new List(); + + foreach (var line in hostsFileLines) + { + var result = HostsFileEntryRegex.Match(line.Trim()); + + if (result.Success) + { + Log.Debug("GetHostsFileEntries - Line matched: " + line); + + var entry = new HostsFileEntry + { + IsEnabled = !result.Groups[1].Value.Equals("#"), + IPAddress = result.Groups[2].Value, + Hostname = result.Groups[3].Value.Replace(@"\s", "").Trim(), + Comment = result.Groups[4].Value.TrimStart('#',' '), + Line = line + }; + + // Skip example entries + if(!entry.IsEnabled) + { + if (ExampleValuesToIgnore.Contains((entry.IPAddress, entry.Hostname))) + { + Log.Debug("GetHostsFileEntries - Matched example entry. Skipping..."); + continue; + } + } + + entries.Add(entry); + } + else + { + Log.Debug("GetHostsFileEntries - Line not matched: " + line); + } + } + + return entries; + } + #endregion +} \ No newline at end of file diff --git a/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntry.cs b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntry.cs new file mode 100644 index 0000000000..eecb1d7da1 --- /dev/null +++ b/Source/NETworkManager.Models/HostsFileEditor/HostsFileEntry.cs @@ -0,0 +1,69 @@ +namespace NETworkManager.Models.HostsFileEditor; + +/// +/// Class that represents a single entry in the hosts file. +/// +public class HostsFileEntry +{ + /// + /// Indicates whether the entry is enabled or not. + /// + public bool IsEnabled { get; init; } + + /// + /// IP address of the host. + /// + public string IPAddress { get; init; } + + /// + /// Host name(s) of the host. Multiple host names are separated by a + /// space (equal to the hosts file format). + /// + public string Hostname { get; init; } + + /// + /// Comment of the host. + /// + public string Comment { get; init; } + + /// + /// Line of the entry in the hosts file. + /// + public string Line { get; init; } + + /// + /// Creates a new instance of . + /// + public HostsFileEntry() + { + + } + + /// + /// Creates a new instance of with parameters. + /// + /// Indicates whether the entry is enabled or not. + /// IP address of the host. + /// Host name(s) of the host. + /// Comment of the host. + public HostsFileEntry(bool isEnabled, string ipAddress, string hostname, string comment) + { + IsEnabled = isEnabled; + IPAddress = ipAddress; + Hostname = hostname; + Comment = comment; + } + + /// + /// Creates a new instance of with parameters. + /// + /// Indicates whether the entry is enabled or not. + /// IP address of the host. + /// Host name(s) of the host. + /// Comment of the host. + /// Line of the entry in the hosts file. + public HostsFileEntry(bool isEnabled, string ipAddress, string hostname, string comment, string line) : this(isEnabled, ipAddress, hostname, comment) + { + Line = line; + } +} diff --git a/Source/NETworkManager.Models/Lookup/OUILookup.cs b/Source/NETworkManager.Models/Lookup/OUILookup.cs index 3763d4e712..20cc8e4654 100644 --- a/Source/NETworkManager.Models/Lookup/OUILookup.cs +++ b/Source/NETworkManager.Models/Lookup/OUILookup.cs @@ -22,7 +22,7 @@ public static class OUILookup /// static OUILookup() { - OUIInfoList = new List(); + OUIInfoList = []; var document = new XmlDocument(); document.Load(OuiFilePath); diff --git a/Source/NETworkManager.Models/Lookup/PortLookup.cs b/Source/NETworkManager.Models/Lookup/PortLookup.cs index c031a407b4..cef648b1cc 100644 --- a/Source/NETworkManager.Models/Lookup/PortLookup.cs +++ b/Source/NETworkManager.Models/Lookup/PortLookup.cs @@ -19,7 +19,7 @@ public static class PortLookup /// static PortLookup() { - PortList = new List(); + PortList = []; var document = new XmlDocument(); document.Load(PortsFilePath); diff --git a/Source/NETworkManager.Models/Network/Connection.cs b/Source/NETworkManager.Models/Network/Connection.cs index 24e3c270a0..866772d0a1 100644 --- a/Source/NETworkManager.Models/Network/Connection.cs +++ b/Source/NETworkManager.Models/Network/Connection.cs @@ -52,7 +52,7 @@ public enum TcpTableClass } // Cache for remote host names with some default values - private static readonly Dictionary _remoteHostNames = new() + private static readonly Dictionary RemoteHostNames = new() { { IPAddress.Parse("127.0.0.1"), "localhost" }, { IPAddress.Parse("::1"), "localhost" }, @@ -94,9 +94,9 @@ private static List GetActiveTcpConnections() var row = (MibTcpRowOwnerPid)Marshal.PtrToStructure(rowPtr, typeof(MibTcpRowOwnerPid))!; var localAddress = new IPAddress(row.localAddr); - var localPort = BitConverter.ToUInt16(new[] { row.localPort2, row.localPort1 }, 0); + var localPort = BitConverter.ToUInt16([row.localPort2, row.localPort1], 0); var remoteAddress = new IPAddress(row.remoteAddr); - var remotePort = BitConverter.ToUInt16(new[] { row.remotePort2, row.remotePort1 }, 0); + var remotePort = BitConverter.ToUInt16([row.remotePort2, row.remotePort1], 0); var state = (TcpState)row.state; // Get process info by PID @@ -116,14 +116,14 @@ private static List GetActiveTcpConnections() } // Resolve remote host name if not cached - if (!_remoteHostNames.ContainsKey(remoteAddress)) + if (!RemoteHostNames.ContainsKey(remoteAddress)) { var dnsResolverTask = DNSClient.GetInstance().ResolvePtrAsync(remoteAddress); dnsResolverTask.Wait(); // Cache the result - _remoteHostNames.Add(remoteAddress, + RemoteHostNames.Add(remoteAddress, !dnsResolverTask.Result.HasError ? dnsResolverTask.Result.Value : "-/-"); } @@ -133,7 +133,7 @@ private static List GetActiveTcpConnections() localPort, remoteAddress, remotePort, - _remoteHostNames.GetValueOrDefault(remoteAddress, "-/-"), + RemoteHostNames.GetValueOrDefault(remoteAddress, "-/-"), state, processId, processName, diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index c7c07ce381..56fb4704f6 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -19,12 +19,15 @@ public static class GlobalStaticConfiguration { #region Global settings + // Application config + public static int ApplicationUIRefreshInterval => 2500; + // Type to search (average type speed --> 187 chars/min) public static TimeSpan SearchDispatcherTimerTimeSpan => new(0, 0, 0, 0, 750); // Network config public static int NetworkChangeDetectionDelay => 5000; - + // Profile config public static bool Profile_ExpandProfileView => true; public static double Profile_WidthCollapsed => 40; @@ -219,6 +222,9 @@ public static class GlobalStaticConfiguration // Application: SNTP Lookup public static int SNTPLookup_Timeout => 4000; public static ExportFileType SNTPLookup_ExportFileType => ExportFileType.Csv; + + // Application: Hosts File Editor + public static ExportFileType HostsFileEditor_ExportFileType => ExportFileType.Csv; // Application: Discovery Protocol public static DiscoveryProtocol DiscoveryProtocol_Protocol => DiscoveryProtocol.LldpCdp; diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index 4c997c92eb..52db1bcc32 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -3584,7 +3584,7 @@ public ExportFileType SNMP_ExportFileType #region SNTP Lookup - private ObservableCollection _sntpLookup_SNTPServers = new(); + private ObservableCollection _sntpLookup_SNTPServers = []; public ObservableCollection SNTPLookup_SNTPServers { @@ -3660,6 +3660,40 @@ public ExportFileType SNTPLookup_ExportFileType } #endregion + + #region Hosts File Editor + + private string _hostsFileEditor_ExportFilePath; + + public string HostsFileEditor_ExportFilePath + { + get => _hostsFileEditor_ExportFilePath; + set + { + if (value == _hostsFileEditor_ExportFilePath) + return; + + _hostsFileEditor_ExportFilePath = value; + OnPropertyChanged(); + } + } + + private ExportFileType _hostsFileEditor_ExportFileType = GlobalStaticConfiguration.HostsFileEditor_ExportFileType; + + public ExportFileType HostsFileEditor_ExportFileType + { + get => _hostsFileEditor_ExportFileType; + set + { + if (value == _hostsFileEditor_ExportFileType) + return; + + _hostsFileEditor_ExportFileType = value; + OnPropertyChanged(); + } + } + + #endregion #region Discovery Protocol diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 83056b5df5..9fee3564fc 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.ObjectModel; +using log4net; +using NETworkManager.Models; +using NETworkManager.Models.Network; +using System; using System.IO; using System.Linq; using System.Xml.Serialization; -using log4net; -using NETworkManager.Controls; -using NETworkManager.Models; -using NETworkManager.Models.Network; -using NETworkManager.Models.PowerShell; namespace NETworkManager.Settings; @@ -189,7 +186,7 @@ public static void Upgrade(Version fromVersion, Version toVersion) if (fromVersion < new Version(2023, 11, 28, 0)) UpgradeTo_2023_11_28_0(); - + // 2024.11.11.0 if (fromVersion < new Version(2024, 11, 11, 0)) UpgradeTo_2024_11_11_0(); @@ -205,7 +202,7 @@ public static void Upgrade(Version fromVersion, Version toVersion) Log.Info("Settings upgrade finished!"); } - /// + /// /// Method to apply changes for version 2023.3.7.0. /// private static void UpgradeTo_2023_3_7_0() @@ -217,14 +214,14 @@ private static void UpgradeTo_2023_3_7_0() Current.General_ApplicationList.Add(ApplicationManager.GetDefaultList() .First(x => x.Name == ApplicationName.SNTPLookup)); Current.SNTPLookup_SNTPServers = - new ObservableCollection(SNTPServer.GetDefaultList()); + [.. SNTPServer.GetDefaultList()]; // Add IP Scanner custom commands foreach (var customCommand in from customCommand in IPScannerCustomCommand.GetDefaultList() - let customCommandFound = - Current.IPScanner_CustomCommands.FirstOrDefault(x => x.Name == customCommand.Name) - where customCommandFound == null - select customCommand) + let customCommandFound = + Current.IPScanner_CustomCommands.FirstOrDefault(x => x.Name == customCommand.Name) + where customCommandFound == null + select customCommand) { Log.Info($"Add \"{customCommand.Name}\" to \"IPScanner_CustomCommands\"..."); Current.IPScanner_CustomCommands.Add(customCommand); @@ -251,7 +248,7 @@ private static void UpgradeTo_2023_3_7_0() // Add new DNS lookup profiles Log.Info("Init \"DNSLookup_DNSServers_v2\" with default DNS servers..."); Current.DNSLookup_DNSServers = - new ObservableCollection(DNSServer.GetDefaultList()); + [.. DNSServer.GetDefaultList()]; } /// @@ -263,7 +260,7 @@ private static void UpgradeTo_2023_4_26_0() // Add SNMP OID profiles Log.Info("Add SNMP OID profiles..."); - Current.SNMP_OidProfiles = new ObservableCollection(SNMPOIDProfile.GetDefaultList()); + Current.SNMP_OidProfiles = [.. SNMPOIDProfile.GetDefaultList()]; } /// @@ -297,9 +294,9 @@ private static void UpgradeTo_2023_11_28_0() // Add DNS lookup profiles after refactoring Log.Info("Init \"DNSLookup_DNSServers\" with default DNS servers..."); Current.DNSLookup_DNSServers = - new ObservableCollection(DNSServer.GetDefaultList()); + [.. DNSServer.GetDefaultList()]; } - + /// /// Method to apply changes for version 2024.11.11.0. /// @@ -309,7 +306,7 @@ private static void UpgradeTo_2024_11_11_0() Log.Info("Reset ApplicationList to default..."); Current.General_ApplicationList = - new ObservableSetCollection(ApplicationManager.GetDefaultList()); + [.. ApplicationManager.GetDefaultList()]; } /// @@ -319,6 +316,13 @@ private static void UpgradeTo_2024_11_11_0() private static void UpgradeToLatest(Version version) { Log.Info($"Apply upgrade to {version}..."); + + // Add Hosts editor application + Log.Info("Add new app \"Hosts File Editor\"..."); + + Current.General_ApplicationList.Insert( + ApplicationManager.GetDefaultList().ToList().FindIndex(x => x.Name == ApplicationName.HostsFileEditor), + ApplicationManager.GetDefaultList().First(x => x.Name == ApplicationName.HostsFileEditor)); } #endregion diff --git a/Source/NETworkManager.Utilities/RegexHelper.cs b/Source/NETworkManager.Utilities/RegexHelper.cs index 60a4d50e63..127d2f98f1 100644 --- a/Source/NETworkManager.Utilities/RegexHelper.cs +++ b/Source/NETworkManager.Utilities/RegexHelper.cs @@ -111,4 +111,20 @@ public static class RegexHelper // Match an SNMP OID (like 1.3.6.1 or .1.3.6.2) public const string SnmpOidRegex = @"^\.?[012]\.(?:[0-9]|[1-3][0-9])(\.\d+)*$"; + + // Match a hosts file entry with optional comments, supporting IPv4, IPv6, and hostnames + // ^* : Matches the beginning of the line + // (#)? : Optionally matches a comment (#) at the start of the line + // \s* : Matches any whitespace after the comment (or before the IP) + // ((?:(?:\d{1,3}\.){3}\d{1,3}) : Matches an IPv4 address (e.g., 192.168.1.1) + // | : OR (alternation between IPv4 and IPv6) + // (?:(?:[A-Fa-f0-9:]+:+)+[A-Fa-f0-9]+) : Matches an IPv6 address (e.g., 2001:db8::1) + // \s+ : Matches one or more spaces between the IP and the hostnames + // ([\w.-]+(?:\s+[\w.-]+)*) : Matches one or more hostnames, separated by spaces + // \s* : Matches optional whitespace after hostnames + // (#.*)? : Optionally matches a comment after hostnames + // $ : Anchors the match to the end of the line + public static string HostsEntryRegex => + @"^(#)?\s*((?:(?:\d{1,3}\.){3}\d{1,3})|(?:(?:[A-Fa-f0-9:]+:+)+[A-Fa-f0-9]+))\s+([\w.-]+(?:\s+[\w.-]+)*)\s*(#.*)?$"; + } \ No newline at end of file diff --git a/Source/NETworkManager/MainWindow.xaml.cs b/Source/NETworkManager/MainWindow.xaml.cs index bb012051d6..96e54a37ca 100644 --- a/Source/NETworkManager/MainWindow.xaml.cs +++ b/Source/NETworkManager/MainWindow.xaml.cs @@ -674,6 +674,7 @@ private void LoadApplicationList() private WebConsoleHostView _webConsoleHostView; private SNMPHostView _snmpHostView; private SNTPLookupHostView _sntpLookupHostView; + private HostsFileEditorView _hostsFileEditorView; private DiscoveryProtocolView _discoveryProtocolView; private WakeOnLANView _wakeOnLanView; private SubnetCalculatorHostView _subnetCalculatorHostView; @@ -824,6 +825,14 @@ private void OnApplicationViewVisible(ApplicationName name, bool fromSettings = ContentControlApplication.Content = _sntpLookupHostView; break; + case ApplicationName.HostsFileEditor: + if(_hostsFileEditorView == null) + _hostsFileEditorView = new HostsFileEditorView(); + else + _hostsFileEditorView.OnViewVisible(); + + ContentControlApplication.Content = _hostsFileEditorView; + break; case ApplicationName.DiscoveryProtocol: if (_discoveryProtocolView == null) _discoveryProtocolView = new DiscoveryProtocolView(); @@ -904,6 +913,10 @@ private void OnApplicationViewVisible(ApplicationName name, bool fromSettings = ContentControlApplication.Content = _arpTableView; break; + + default: + Log.Error("Cannot show unknown application view: " + name); + break; } } @@ -959,6 +972,9 @@ private void OnApplicationViewHide(ApplicationName name) case ApplicationName.SNTPLookup: _sntpLookupHostView?.OnViewHide(); break; + case ApplicationName.HostsFileEditor: + _hostsFileEditorView?.OnViewHide(); + break; case ApplicationName.DiscoveryProtocol: _discoveryProtocolView?.OnViewHide(); break; @@ -989,6 +1005,9 @@ private void OnApplicationViewHide(ApplicationName name) case ApplicationName.ARPTable: _arpTableView?.OnViewHide(); break; + default: + Log.Error("Cannot hide unknown application view: " + name); + break; } } diff --git a/Source/NETworkManager/NETworkManager.csproj b/Source/NETworkManager/NETworkManager.csproj index c479f18a79..a64d71b66d 100644 --- a/Source/NETworkManager/NETworkManager.csproj +++ b/Source/NETworkManager/NETworkManager.csproj @@ -136,6 +136,11 @@ Wpf Designer + + MSBuild:Compile + Wpf + Designer + diff --git a/Source/NETworkManager/ViewModels/ARPTableViewModel.cs b/Source/NETworkManager/ViewModels/ARPTableViewModel.cs index f2944390c2..dd03b2b545 100644 --- a/Source/NETworkManager/ViewModels/ARPTableViewModel.cs +++ b/Source/NETworkManager/ViewModels/ARPTableViewModel.cs @@ -38,11 +38,11 @@ public ARPTableViewModel(IDialogCoordinator instance) ResultsView.Filter = o => { - if (o is not ARPInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ARPInfo info) + return false; // Search by IPAddress and MACAddress return info.IPAddress.ToString().IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || @@ -95,7 +95,7 @@ public string Search } } - private ObservableCollection _results = new(); + private ObservableCollection _results = []; public ObservableCollection Results { @@ -429,8 +429,8 @@ private async Task Refresh() IsRefreshing = true; Results.Clear(); - - (await ARP.GetTableAsync()).ForEach(x => Results.Add(x)); + + (await ARP.GetTableAsync()).ForEach(Results.Add); IsRefreshing = false; } diff --git a/Source/NETworkManager/ViewModels/AWSSessionManagerHostViewModel.cs b/Source/NETworkManager/ViewModels/AWSSessionManagerHostViewModel.cs index 7b77c82db8..5bc0474e38 100644 --- a/Source/NETworkManager/ViewModels/AWSSessionManagerHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/AWSSessionManagerHostViewModel.cs @@ -650,7 +650,7 @@ private async Task SyncAllInstanceIDsFromAWS() } // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(2000); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); Log.Info("All Instance IDs synced from AWS!"); @@ -679,7 +679,7 @@ private async Task SyncGroupInstanceIDsFromAWS(string group) } // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(2000); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); Log.Info("Group synced!"); @@ -998,12 +998,12 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { + if (string.IsNullOrEmpty(Search)) + return true ; + if (o is not ProfileInfo info) return false; - if (string.IsNullOrEmpty(Search)) - return true; - var search = Search.Trim(); // Search by: Tag=xxx (exact match, ignore case) diff --git a/Source/NETworkManager/ViewModels/AboutViewModel.cs b/Source/NETworkManager/ViewModels/AboutViewModel.cs index c4b1913122..4642b17f5d 100644 --- a/Source/NETworkManager/ViewModels/AboutViewModel.cs +++ b/Source/NETworkManager/ViewModels/AboutViewModel.cs @@ -42,7 +42,7 @@ private async Task CheckForUpdatesAsync() IsUpdateCheckRunning = true; // Show a loading animation for the user - await Task.Delay(1000); + await Task.Delay(1250); var updater = new Updater(); diff --git a/Source/NETworkManager/ViewModels/ConnectionsViewModel.cs b/Source/NETworkManager/ViewModels/ConnectionsViewModel.cs index 420296ebb9..c12096632b 100644 --- a/Source/NETworkManager/ViewModels/ConnectionsViewModel.cs +++ b/Source/NETworkManager/ViewModels/ConnectionsViewModel.cs @@ -39,12 +39,12 @@ public ConnectionsViewModel(IDialogCoordinator instance) IPAddressHelper.CompareIPAddresses(x.LocalIPAddress, y.LocalIPAddress)); ResultsView.Filter = o => - { - if (o is not ConnectionInfo info) - return false; - + { if (string.IsNullOrEmpty(Search)) - return true; + return true; + + if (o is not ConnectionInfo info) + return false; // Search by local/remote IP Address, local/remote Port, Protocol and State return info.LocalIPAddress.ToString().IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || @@ -346,8 +346,8 @@ private async Task Refresh() IsRefreshing = true; Results.Clear(); - - (await Connection.GetActiveTcpConnectionsAsync()).ForEach(x => Results.Add(x)); + + (await Connection.GetActiveTcpConnectionsAsync()).ForEach(Results.Add); IsRefreshing = false; } diff --git a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs index 14d5b7aba1..8ef01da9d7 100644 --- a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs @@ -372,11 +372,13 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { + if (string.IsNullOrEmpty(Search)) + return true; + if (o is not ProfileInfo info) return false; - if (string.IsNullOrEmpty(Search)) - return true; + var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs new file mode 100644 index 0000000000..cb6fd94c40 --- /dev/null +++ b/Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using System.Timers; +using System.Windows; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Threading; +using log4net; +using MahApps.Metro.Controls; +using MahApps.Metro.Controls.Dialogs; +using NETworkManager.Localization.Resources; +using NETworkManager.Models.Export; +using NETworkManager.Models.HostsFileEditor; +using NETworkManager.Models.Network; +using NETworkManager.Settings; +using NETworkManager.Utilities; +using NETworkManager.Views; + +namespace NETworkManager.ViewModels; + +public class HostsFileEditorViewModel : ViewModelBase +{ + #region Variables + private static readonly ILog Log = LogManager.GetLogger(typeof(HostsFileEditorViewModel)); + + private readonly IDialogCoordinator _dialogCoordinator; + + private readonly bool _isLoading; + + private string _search; + public string Search + { + get => _search; + set + { + if (value == _search) + return; + + _search = value; + + ResultsView.Refresh(); + + OnPropertyChanged(); + } + } + + private ObservableCollection _results = []; + + public ObservableCollection Results + { + get => _results; + set + { + if (value == _results) + return; + + _results = value; + OnPropertyChanged(); + } + } + + public ICollectionView ResultsView { get; } + + private HostsFileEntry _selectedResult; + + public HostsFileEntry SelectedResult + { + get => _selectedResult; + set + { + if (value == _selectedResult) + return; + + _selectedResult = value; + OnPropertyChanged(); + } + } + + private IList _selectedResults = new ArrayList(); + + public IList SelectedResults + { + get => _selectedResults; + set + { + if (Equals(value, _selectedResults)) + return; + + _selectedResults = value; + OnPropertyChanged(); + } + } + + private bool _isRefreshing; + + public bool IsRefreshing + { + get => _isRefreshing; + set + { + if (value == _isRefreshing) + return; + + _isRefreshing = value; + OnPropertyChanged(); + } + } + + private bool _isStatusMessageDisplayed; + + public bool IsStatusMessageDisplayed + { + get => _isStatusMessageDisplayed; + set + { + if (value == _isStatusMessageDisplayed) + return; + + _isStatusMessageDisplayed = value; + OnPropertyChanged(); + } + } + + private string _statusMessage; + + public string StatusMessage + { + get => _statusMessage; + private set + { + if (value == _statusMessage) + return; + + _statusMessage = value; + OnPropertyChanged(); + } + } + + #endregion + + #region Constructor, LoadSettings + + public HostsFileEditorViewModel(IDialogCoordinator instance) + { + _isLoading = true; + _dialogCoordinator = instance; + + // Result view + search + ResultsView = CollectionViewSource.GetDefaultView(Results); + ResultsView.Filter = o => + { + if (string.IsNullOrEmpty(Search)) + return true; + + if (o is not HostsFileEntry entry) + return false; + + return entry.IPAddress.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || + entry.Hostname.IndexOf(Search, StringComparison.OrdinalIgnoreCase)> -1 || + entry.Comment.IndexOf(Search, StringComparison.OrdinalIgnoreCase)> -1; + }; + + // Get hosts file entries + Refresh(true).ConfigureAwait(false); + + // Watch hosts file for changes + HostsFileEditor.HostsFileChanged += (_, _) => + { + Application.Current.Dispatcher.Invoke(() => + { + Refresh().ConfigureAwait(false); + }); + }; + + _isLoading = false; + } + + private void LoadSettings() + { + + } + + #endregion + + #region ICommands & Actions + public ICommand RefreshCommand => new RelayCommand(_ => RefreshAction().ConfigureAwait(false), Refresh_CanExecute); + + private bool Refresh_CanExecute(object parameter) + { + return Application.Current.MainWindow != null && + !((MetroWindow)Application.Current.MainWindow).IsAnyDialogOpen && + !ConfigurationManager.Current.IsChildWindowOpen; + } + + private async Task RefreshAction() + { + await Refresh(); + } + + public ICommand ExportCommand => new RelayCommand(_ => ExportAction().ConfigureAwait(false)); + + private async Task ExportAction() + { + var customDialog = new CustomDialog + { + Title = Strings.Export + }; + + var exportViewModel = new ExportViewModel(async instance => + { + await _dialogCoordinator.HideMetroDialogAsync(this, customDialog); + + try + { + ExportManager.Export(instance.FilePath, instance.FileType, + instance.ExportAll + ? Results + : new ObservableCollection(SelectedResults.Cast().ToArray())); + } + catch (Exception ex) + { + Log.Error("Error while exporting data as " + instance.FileType, ex); + + var settings = AppearanceManager.MetroDialog; + settings.AffirmativeButtonText = Strings.OK; + + await _dialogCoordinator.ShowMessageAsync(this, Strings.Error, + Strings.AnErrorOccurredWhileExportingTheData + Environment.NewLine + + Environment.NewLine + ex.Message, MessageDialogStyle.Affirmative, settings); + } + + SettingsManager.Current.HostsFileEditor_ExportFileType = instance.FileType; + SettingsManager.Current.HostsFileEditor_ExportFilePath = instance.FilePath; + }, _ => { _dialogCoordinator.HideMetroDialogAsync(this, customDialog); }, [ + ExportFileType.Csv, ExportFileType.Xml, ExportFileType.Json + ], true, SettingsManager.Current.HostsFileEditor_ExportFileType, SettingsManager.Current.HostsFileEditor_ExportFilePath); + + customDialog.Content = new ExportDialog + { + DataContext = exportViewModel + }; + + await _dialogCoordinator.ShowMetroDialogAsync(this, customDialog); + } + + public ICommand RestartAsAdminCommand => new RelayCommand(_ => RestartAsAdminAction().ConfigureAwait(false)); + + private async Task RestartAsAdminAction() + { + try + { + (Application.Current.MainWindow as MainWindow)?.RestartApplication(true); + } + catch (Exception ex) + { + await _dialogCoordinator.ShowMessageAsync(this, Strings.Error, ex.Message, + MessageDialogStyle.Affirmative, AppearanceManager.MetroDialog); + } + } + #endregion + + #region Methods + + private async Task Refresh(bool init = false) + { + if(IsRefreshing) + return; + + IsRefreshing = true; + + // Retry 3 times if the hosts file is locked + for (var i = 1; i < 4; i++) + { + // Wait for 2.5 seconds on refresh + if (init == false || i > 1) + { + StatusMessage = "Refreshing..."; + IsStatusMessageDisplayed = true; + + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); + } + + try + { + var entries = await HostsFileEditor.GetHostsFileEntriesAsync(); + + Results.Clear(); + + entries.ToList().ForEach(Results.Add); + + StatusMessage = "Reloaded at " + DateTime.Now.ToShortTimeString(); + IsStatusMessageDisplayed = true; + + break; + } + catch (Exception ex) + { + Log.Error(ex); + + StatusMessage = "Failed to reload hosts file: " + ex.Message; + + if (i < 3) + StatusMessage += Environment.NewLine + "Retrying in 2.5 seconds..."; + + IsStatusMessageDisplayed = true; + + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); + } + } + + IsRefreshing = false; + } + + public void OnViewVisible() + { + + } + + public void OnViewHide() + { + + } + #endregion +} \ No newline at end of file diff --git a/Source/NETworkManager/ViewModels/IPApiDNSResolverWidgetViewModel.cs b/Source/NETworkManager/ViewModels/IPApiDNSResolverWidgetViewModel.cs index 027847f1d6..fea55efac4 100644 --- a/Source/NETworkManager/ViewModels/IPApiDNSResolverWidgetViewModel.cs +++ b/Source/NETworkManager/ViewModels/IPApiDNSResolverWidgetViewModel.cs @@ -88,7 +88,7 @@ private async Task CheckAsync() Result = null; // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(2000); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); Result = await DNSResolverService.GetInstance().GetDNSResolverAsync(); diff --git a/Source/NETworkManager/ViewModels/IPApiIPGeolocationWidgetViewModel.cs b/Source/NETworkManager/ViewModels/IPApiIPGeolocationWidgetViewModel.cs index 5f68a2c463..144f0ebd79 100644 --- a/Source/NETworkManager/ViewModels/IPApiIPGeolocationWidgetViewModel.cs +++ b/Source/NETworkManager/ViewModels/IPApiIPGeolocationWidgetViewModel.cs @@ -90,7 +90,7 @@ private async Task CheckAsync() Result = null; // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(2000); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); Result = await IPGeolocationService.GetInstance().GetIPGeolocationAsync(); diff --git a/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs b/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs index 11cbed1e30..b0bb7f42eb 100644 --- a/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs @@ -373,11 +373,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) - return true; + return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs b/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs index 54896a8470..a352707c3a 100644 --- a/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs @@ -377,12 +377,12 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { + if (string.IsNullOrEmpty(Search)) + return true; + if (o is not ProfileInfo info) return false; - if (string.IsNullOrEmpty(Search)) - return true; - var search = Search.Trim(); // Search by: Tag=xxx (exact match, ignore case) diff --git a/Source/NETworkManager/ViewModels/ListenersViewModel.cs b/Source/NETworkManager/ViewModels/ListenersViewModel.cs index 2fbb0c11e4..5b7ecb8643 100644 --- a/Source/NETworkManager/ViewModels/ListenersViewModel.cs +++ b/Source/NETworkManager/ViewModels/ListenersViewModel.cs @@ -39,12 +39,12 @@ public ListenersViewModel(IDialogCoordinator instance) ResultsView.Filter = o => { + if (string.IsNullOrEmpty(Search)) + return true; + if (o is not ListenerInfo info) return false; - if (string.IsNullOrEmpty(Search)) - return true; - // Search by IP Address, Port and Protocol return info.IPAddress.ToString().IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || info.Port.ToString().IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || @@ -335,8 +335,8 @@ private async Task Refresh() IsRefreshing = true; Results.Clear(); - - (await Listener.GetAllActiveListenersAsync()).ForEach(x => Results.Add(x)); + + (await Listener.GetAllActiveListenersAsync()).ForEach(Results.Add); IsRefreshing = false; } diff --git a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs index 752a3b008e..f11c4ecf84 100644 --- a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs +++ b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs @@ -938,8 +938,6 @@ private async Task RemoveIPv4AddressAction() private async void ReloadNetworkInterfaces() { - Debug.WriteLine("ReloadNetworkInterfaces............."); - // Avoid multiple reloads if(IsNetworkInterfaceLoading) return; @@ -947,7 +945,7 @@ private async void ReloadNetworkInterfaces() IsNetworkInterfaceLoading = true; // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(2000); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); // Store the last selected id var id = SelectedNetworkInterface?.Id ?? string.Empty; @@ -1337,12 +1335,12 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) - return true; + return true; + if (o is not ProfileInfo info) + return false; + var search = Search.Trim(); // Search by: Tag=xxx (exact match, ignore case) diff --git a/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs index 2cbb4a41ca..413f213ebb 100644 --- a/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs @@ -594,11 +594,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs index ec1f7ac912..bac9aa7a66 100644 --- a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs @@ -378,12 +378,12 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { + if (string.IsNullOrEmpty(Search)) + return true; + if (o is not ProfileInfo info) return false; - if (string.IsNullOrEmpty(Search)) - return true; - var search = Search.Trim(); // Search by: Tag=xxx (exact match, ignore case) diff --git a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs index 15e1b1b0f9..7165e98b05 100644 --- a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs @@ -637,11 +637,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/ProfilesViewModel.cs b/Source/NETworkManager/ViewModels/ProfilesViewModel.cs index 86f21862ab..e71def69ba 100644 --- a/Source/NETworkManager/ViewModels/ProfilesViewModel.cs +++ b/Source/NETworkManager/ViewModels/ProfilesViewModel.cs @@ -264,11 +264,11 @@ private void SetProfilesView(GroupInfo group, ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs b/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs index c40c225d7a..e560490cf1 100644 --- a/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs @@ -714,11 +714,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs b/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs index 06b8e44942..9995bbf22c 100644 --- a/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs @@ -586,11 +586,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs b/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs index d606bb5230..d624478b5b 100644 --- a/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs @@ -393,11 +393,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/SettingsAutostartViewModel.cs b/Source/NETworkManager/ViewModels/SettingsAutostartViewModel.cs index f7274df5db..4b17303ac0 100644 --- a/Source/NETworkManager/ViewModels/SettingsAutostartViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsAutostartViewModel.cs @@ -47,7 +47,7 @@ private async Task EnableDisableAutostart(bool enable) await AutostartManager.DisableAsync(); // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(2000); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); } catch (Exception ex) { diff --git a/Source/NETworkManager/ViewModels/SettingsLanguageViewModel.cs b/Source/NETworkManager/ViewModels/SettingsLanguageViewModel.cs index 98904dc020..ef6213c807 100644 --- a/Source/NETworkManager/ViewModels/SettingsLanguageViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsLanguageViewModel.cs @@ -27,7 +27,7 @@ public SettingsLanguageViewModel() if (string.IsNullOrEmpty(Search)) return true; - if (!(o is LocalizationInfo info)) + if (o is not LocalizationInfo info) return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs b/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs index b93eb53032..9240e006f6 100644 --- a/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs @@ -512,13 +512,12 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.GroupDescriptions.Add(new PropertyGroupDescription(nameof(ProfileInfo.Group))); Profiles.Filter = o => - { + {if (string.IsNullOrEmpty(Search)) + return true; + if (o is not ProfileInfo info) return false; - if (string.IsNullOrEmpty(Search)) - return true; - var search = Search.Trim(); // Search by: Tag=xxx (exact match, ignore case) diff --git a/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs b/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs index c43a0b1760..dee7c15f94 100644 --- a/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs @@ -378,11 +378,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs b/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs index 6ff7c1e246..03175c4d35 100644 --- a/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs +++ b/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs @@ -369,7 +369,7 @@ private async Task WakeUp(WakeOnLANInfo info) WakeOnLAN.Send(info); // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(2000); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); StatusMessage = Strings.MagicPacketSentMessage; IsStatusMessageDisplayed = true; @@ -464,11 +464,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs b/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs index b8a672cd76..59dbf9c47e 100644 --- a/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs @@ -482,11 +482,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs b/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs index 66690ca5ea..68351a70ef 100644 --- a/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs @@ -372,11 +372,11 @@ private void SetProfilesView(ProfileInfo profile = null) Profiles.Filter = o => { - if (o is not ProfileInfo info) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not ProfileInfo info) + return false; var search = Search.Trim(); diff --git a/Source/NETworkManager/ViewModels/WiFiConnectViewModel.cs b/Source/NETworkManager/ViewModels/WiFiConnectViewModel.cs index df40ca1548..90891b99d3 100644 --- a/Source/NETworkManager/ViewModels/WiFiConnectViewModel.cs +++ b/Source/NETworkManager/ViewModels/WiFiConnectViewModel.cs @@ -345,13 +345,13 @@ public async Task CheckWpsAsync() IsWpsChecking = true; // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(1000); + await Task.Delay(1250); IsWpsAvailable = await WiFi.IsWpsAvailable(Options.AdapterInfo.WiFiAdapter, Options.NetworkInfo.AvailableNetwork); // Make the user happy, let him see a reload animation (and he cannot spam the reload command) - await Task.Delay(1000); + await Task.Delay(1250); IsWpsChecking = false; } diff --git a/Source/NETworkManager/ViewModels/WiFiViewModel.cs b/Source/NETworkManager/ViewModels/WiFiViewModel.cs index 8d6c713639..961bc46b32 100644 --- a/Source/NETworkManager/ViewModels/WiFiViewModel.cs +++ b/Source/NETworkManager/ViewModels/WiFiViewModel.cs @@ -471,29 +471,28 @@ public WiFiViewModel(IDialogCoordinator instance) ListSortDirection.Ascending)); NetworksView.Filter = o => { - if (o is not WiFiNetworkInfo info) - return false; - - if (info.Radio == WiFiRadio.GHz2dot4 && !Show2dot4GHzNetworks) - return false; - - if (info.Radio == WiFiRadio.GHz5 && !Show5GHzNetworks) - return false; - - if (info.Radio == WiFiRadio.GHz6 && !Show6GHzNetworks) - return false; - if (string.IsNullOrEmpty(Search)) return true; + + if (o is not WiFiNetworkInfo info) + return false; - // Search by: SSID, Security, Frequency , Channel, BSSID (MAC address), Vendor, Phy kind - return info.AvailableNetwork.Ssid.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || - info.NetworkAuthenticationType.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || - $"{info.ChannelCenterFrequencyInGigahertz}".IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || - $"{info.Channel}".IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || - info.AvailableNetwork.Bssid.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || - info.Vendor.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || - info.PhyKind.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1; + switch (info.Radio) + { + case WiFiRadio.GHz2dot4 when !Show2dot4GHzNetworks: + case WiFiRadio.GHz5 when !Show5GHzNetworks: + case WiFiRadio.GHz6 when !Show6GHzNetworks: + return false; + default: + // Search by: SSID, Security, Frequency , Channel, BSSID (MAC address), Vendor, Phy kind + return info.AvailableNetwork.Ssid.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || + info.NetworkAuthenticationType.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || + $"{info.ChannelCenterFrequencyInGigahertz}".IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || + $"{info.Channel}".IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || + info.AvailableNetwork.Bssid.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || + info.Vendor.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1 || + info.PhyKind.IndexOf(Search, StringComparison.OrdinalIgnoreCase) > -1; + } }; // Load network adapters @@ -599,7 +598,7 @@ private async Task LoadAdaptersAsync(string adapterId = null) IsAdaptersLoading = true; // Show a loading animation for the user - await Task.Delay(2500); + await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval); try { @@ -645,7 +644,7 @@ private async Task LoadAdaptersAsync(string adapterId = null) Log.Debug("LoadAdaptersAsync - Done."); } - private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = false, uint delayInMs = 0) + private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = false, int delayInMs = 0) { Log.Debug($"ScanAsync - Scanning WiFi adapter \"{adapterInfo.NetworkInterfaceInfo.Name}\" with delay of {delayInMs} ms..."); @@ -661,7 +660,7 @@ private async Task ScanAsync(WiFiAdapterInfo adapterInfo, bool refreshing = fals } if (delayInMs != 0) - await Task.Delay((int)delayInMs); + await Task.Delay(delayInMs); var statusMessage = string.Empty; @@ -922,7 +921,7 @@ private async void Disconnect() } // Refresh - await ScanAsync(SelectedAdapter, true, 2500); + await ScanAsync(SelectedAdapter, true, GlobalStaticConfiguration.ApplicationUIRefreshInterval); } private async Task Export() diff --git a/Source/NETworkManager/Views/ARPTableView.xaml b/Source/NETworkManager/Views/ARPTableView.xaml index fb5b342a0d..26b2a1aff0 100644 --- a/Source/NETworkManager/Views/ARPTableView.xaml +++ b/Source/NETworkManager/Views/ARPTableView.xaml @@ -38,8 +38,8 @@ + Width="250" Text="{Binding Path=Search, UpdateSourceTrigger=PropertyChanged}" + Style="{StaticResource ResourceKey=SearchTextBox}" /> + + + + + + + + + + - + - - - - - - - - - - + @@ -124,11 +125,11 @@ MinWidth="100" /> - - + @@ -141,7 +142,7 @@ - + diff --git a/Source/NETworkManager/Views/HostsFileEditorView.xaml b/Source/NETworkManager/Views/HostsFileEditorView.xaml new file mode 100644 index 0000000000..9faf7fec04 --- /dev/null +++ b/Source/NETworkManager/Views/HostsFileEditorView.xaml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/NETworkManager/Views/HostsFileEditorView.xaml.cs b/Source/NETworkManager/Views/HostsFileEditorView.xaml.cs new file mode 100644 index 0000000000..ef1a2a8c63 --- /dev/null +++ b/Source/NETworkManager/Views/HostsFileEditorView.xaml.cs @@ -0,0 +1,33 @@ +using System.Windows; +using System.Windows.Controls; +using MahApps.Metro.Controls.Dialogs; +using NETworkManager.ViewModels; + +namespace NETworkManager.Views; + +public partial class HostsFileEditorView +{ + private readonly HostsFileEditorViewModel _viewModel = new(DialogCoordinator.Instance); + + public HostsFileEditorView() + { + InitializeComponent(); + DataContext = _viewModel; + } + + public void OnViewHide() + { + _viewModel.OnViewHide(); + } + + public void OnViewVisible() + { + _viewModel.OnViewVisible(); + } + + private void ContextMenu_Opened(object sender, RoutedEventArgs e) + { + if (sender is ContextMenu menu) + menu.DataContext = _viewModel; + } +} diff --git a/Source/NETworkManager/Views/WiFiView.xaml b/Source/NETworkManager/Views/WiFiView.xaml index 8e7a6cdb36..a28ecd7dce 100644 --- a/Source/NETworkManager/Views/WiFiView.xaml +++ b/Source/NETworkManager/Views/WiFiView.xaml @@ -774,7 +774,6 @@ Style="{DynamicResource ResourceKey=LoadingIndicatorPulseStyle}" Visibility="{Binding Path=IsBackgroundSearchRunning, Converter={StaticResource ResourceKey=BooleanToVisibilityCollapsedConverter}}" Width="24" Height="24" - VerticalAlignment="Center" SpeedRatio="1" Margin="0,0,10,0" /> - + +