diff --git a/Source/GlobalAssemblyInfo.cs b/Source/GlobalAssemblyInfo.cs index 3e9b4307d7..23d3656a91 100644 --- a/Source/GlobalAssemblyInfo.cs +++ b/Source/GlobalAssemblyInfo.cs @@ -6,5 +6,5 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("2026.7.12.0")] -[assembly: AssemblyFileVersion("2026.7.12.0")] +[assembly: AssemblyVersion("2026.8.5.0")] +[assembly: AssemblyFileVersion("2026.8.5.0")] diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index d0d46edfd2..b1362fdc88 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -5283,21 +5283,6 @@ public static string HelpMessage_Tags { } } - /// - /// Looks up a localized string similar to This setting specifies the minimum number of threads that will be created from the application's ThreadPool on demand. This can improve the performance for example of the IP scanner or port scanner. - /// - ///The value is added to the default min. threads (number of CPU threads). The value 0 leaves the default settings. If the value is higher than the default max. threads of the ThreadPool, this value is used. - /// - ///If the value is too high, performance problems may occur. - /// - ///Changes to this value will take effect a [rest of string was truncated]";. - /// - public static string HelpMessage_ThreadPoolAdditionalMinThreads { - get { - return ResourceManager.GetString("HelpMessage_ThreadPoolAdditionalMinThreads", resourceCulture); - } - } - /// /// Looks up a localized string similar to Use custom themes to personalize the appearance of the application. You can edit or add theme in the "Program Folder > Themes" directory. For more details, refer to the documentation.. /// @@ -6891,16 +6876,6 @@ public static string MaxPortThreads { return ResourceManager.GetString("MaxPortThreads", resourceCulture); } } - - /// - /// Looks up a localized string similar to These settings only change the maximum number of concurrently executed threads per host/port scan. Go to Settings > General > General to adjust the (min) threads of the application.. - /// - public static string MaxThreadsOnlyGoToSettingsGeneralGeneral { - get { - return ResourceManager.GetString("MaxThreadsOnlyGoToSettingsGeneralGeneral", resourceCulture); - } - } - /// /// Looks up a localized string similar to Measured time. /// @@ -11944,15 +11919,6 @@ public static string ThisWillResetAllSettings { } } - /// - /// Looks up a localized string similar to ThreadPool additional min. threads. - /// - public static string ThreadPoolAdditionalMinThreads { - get { - return ResourceManager.GetString("ThreadPoolAdditionalMinThreads", resourceCulture); - } - } - /// /// Looks up a localized string similar to Threads. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 1157005191..0c2584cd75 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3353,21 +3353,6 @@ If the option is disabled again, the values are no longer modified. However, the Ping status - - ThreadPool additional min. threads - - - This setting specifies the minimum number of threads that will be created from the application's ThreadPool on demand. This can improve the performance for example of the IP scanner or port scanner. - -The value is added to the default min. threads (number of CPU threads). The value 0 leaves the default settings. If the value is higher than the default max. threads of the ThreadPool, this value is used. - -If the value is too high, performance problems may occur. - -Changes to this value will take effect after the application is restarted. Whether the value was set successfully can be seen in the log file under %LocalAppData%\NETworkManager\NETworkManager.log - - - These settings only change the maximum number of concurrently executed threads per host/port scan. Go to Settings > General > General to adjust the (min) threads of the application. - Port status diff --git a/Source/NETworkManager.Models/Network/HostRangeHelper.cs b/Source/NETworkManager.Models/Network/HostRangeHelper.cs index 43ccdc505b..f7b21cadef 100644 --- a/Source/NETworkManager.Models/Network/HostRangeHelper.cs +++ b/Source/NETworkManager.Models/Network/HostRangeHelper.cs @@ -29,19 +29,14 @@ public static IEnumerable CreateListFromInput(string hosts) .ToArray(); } - public static Task<(List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved)> + public static async Task<(List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved)> ResolveAsync(IEnumerable hosts, bool dnsResolveHostnamePreferIPv4, CancellationToken cancellationToken) - { - return Task.Run(() => Resolve(hosts, dnsResolveHostnamePreferIPv4, cancellationToken), cancellationToken); - } - - private static (List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved) Resolve( - IEnumerable hosts, bool dnsResolveHostnamePreferIPv4, CancellationToken cancellationToken) { var hostsBag = new ConcurrentBag<(IPAddress ipAddress, string hostname)>(); var hostnamesNotResovledBag = new ConcurrentBag(); - Parallel.ForEach(hosts, new ParallelOptions { CancellationToken = cancellationToken }, host => + await Parallel.ForEachAsync(hosts, new ParallelOptions { CancellationToken = cancellationToken }, + async (host, ct) => { switch (host) { @@ -62,7 +57,7 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List Parallel.For(IPv4Address.ToInt32(network.Network), IPv4Address.ToInt32(network.Broadcast) + 1, (i, state) => { - if (cancellationToken.IsCancellationRequested) + if (ct.IsCancellationRequested) state.Break(); hostsBag.Add((IPv4Address.FromInt32(i), string.Empty)); @@ -77,7 +72,7 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List Parallel.For(IPv4Address.ToInt32(IPAddress.Parse(range[0])), IPv4Address.ToInt32(IPAddress.Parse(range[1])) + 1, (i, state) => { - if (cancellationToken.IsCancellationRequested) + if (ct.IsCancellationRequested) state.Break(); hostsBag.Add((IPv4Address.FromInt32(i), string.Empty)); @@ -107,7 +102,7 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List Parallel.For(int.Parse(rangeNumbers[0]), int.Parse(rangeNumbers[1]) + 1, (i, state) => { - if (cancellationToken.IsCancellationRequested) + if (ct.IsCancellationRequested) state.Break(); innerList.Add(i); @@ -124,18 +119,18 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List } // Build the new ipv4 - Parallel.ForEach(list[0], new ParallelOptions { CancellationToken = cancellationToken }, + Parallel.ForEach(list[0], new ParallelOptions { CancellationToken = ct }, i => { - Parallel.ForEach(list[1], new ParallelOptions { CancellationToken = cancellationToken }, + Parallel.ForEach(list[1], new ParallelOptions { CancellationToken = ct }, j => { Parallel.ForEach(list[2], - new ParallelOptions { CancellationToken = cancellationToken }, + new ParallelOptions { CancellationToken = ct }, k => { Parallel.ForEach(list[3], - new ParallelOptions { CancellationToken = cancellationToken }, + new ParallelOptions { CancellationToken = ct }, h => { hostsBag.Add((IPAddress.Parse($"{i}.{j}.{k}.{h}"), string.Empty)); @@ -148,17 +143,13 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List // example.com case var _ when RegexHelper.HostnameOrDomainRegex().IsMatch(host): - using (var dnsResolverTask = - DNSClientHelper.ResolveAorAaaaAsync(host, dnsResolveHostnamePreferIPv4)) - { - // Wait for task inside a Parallel.Foreach - dnsResolverTask.Wait(cancellationToken); + var dnsResult = await DNSClientHelper.ResolveAorAaaaAsync(host, dnsResolveHostnamePreferIPv4) + .WaitAsync(ct).ConfigureAwait(false); - if (!dnsResolverTask.Result.HasError) - hostsBag.Add((IPAddress.Parse($"{dnsResolverTask.Result.Value}"), host)); - else - hostnamesNotResovledBag.Add(host); - } + if (!dnsResult.HasError) + hostsBag.Add((IPAddress.Parse($"{dnsResult.Value}"), host)); + else + hostnamesNotResovledBag.Add(host); break; @@ -168,42 +159,39 @@ private static (List<(IPAddress ipAddress, string hostname)> hosts, List var hostAndSubnet = host.Split('/'); // Only support IPv4 - using (var dnsResolverTask = DNSClientHelper.ResolveAorAaaaAsync(hostAndSubnet[0], true)) - { - // Wait for task inside a Parallel.Foreach - dnsResolverTask.Wait(cancellationToken); + var dnsResultWithSubnet = await DNSClientHelper.ResolveAorAaaaAsync(hostAndSubnet[0], true) + .WaitAsync(ct).ConfigureAwait(false); - if (!dnsResolverTask.Result.HasError) + if (!dnsResultWithSubnet.HasError) + { + // Only support IPv4 for ranges for now + if (dnsResultWithSubnet.Value.AddressFamily == AddressFamily.InterNetwork) { - // Only support IPv4 for ranges for now - if (dnsResolverTask.Result.Value.AddressFamily == AddressFamily.InterNetwork) - { - network = IPNetwork2.Parse( - $"{dnsResolverTask.Result.Value}/{hostAndSubnet[1]}"); - - Parallel.For(IPv4Address.ToInt32(network.Network), - IPv4Address.ToInt32(network.Broadcast) + 1, (i, state) => - { - if (cancellationToken.IsCancellationRequested) - state.Break(); - - hostsBag.Add((IPv4Address.FromInt32(i), string.Empty)); - }); - } - else - { - hostnamesNotResovledBag.Add(hostAndSubnet[0]); - } + network = IPNetwork2.Parse( + $"{dnsResultWithSubnet.Value}/{hostAndSubnet[1]}"); + + Parallel.For(IPv4Address.ToInt32(network.Network), + IPv4Address.ToInt32(network.Broadcast) + 1, (i, state) => + { + if (ct.IsCancellationRequested) + state.Break(); + + hostsBag.Add((IPv4Address.FromInt32(i), string.Empty)); + }); } else { hostnamesNotResovledBag.Add(hostAndSubnet[0]); } } + else + { + hostnamesNotResovledBag.Add(hostAndSubnet[0]); + } break; } - }); + }).ConfigureAwait(false); // Sort list and return IPAddressComparer comparer = new(); diff --git a/Source/NETworkManager.Models/Network/IPScanner.cs b/Source/NETworkManager.Models/Network/IPScanner.cs index 7fb4e8b698..039fc66482 100644 --- a/Source/NETworkManager.Models/Network/IPScanner.cs +++ b/Source/NETworkManager.Models/Network/IPScanner.cs @@ -79,7 +79,7 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts, CancellationToken cancellationToken) { // Start the scan in a separate task - Task.Run(() => + Task.Run(async () => { _progressValue = 0; @@ -101,40 +101,32 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts, }; // Start scan - Parallel.ForEach(hosts, hostParallelOptions, host => + await Parallel.ForEachAsync(hosts, hostParallelOptions, async (host, ct) => { - // Start ping async - var pingTask = PingAsync(host.ipAddress, cancellationToken); + // Start ping, port scan and netbios lookup concurrently - none of these block a thread anymore + var pingTask = PingAsync(host.ipAddress, ct); - // Start port scan async (if enabled) var portScanTask = options.PortScanEnabled - ? PortScanAsync(host.ipAddress, portScanParallelOptions, cancellationToken) - : Task.FromResult(Enumerable.Empty()); + ? PortScanAsync(host.ipAddress, portScanParallelOptions, ct) + : Task.FromResult(new List()); - // Start netbios lookup async (if enabled) var netbiosTask = options.NetBIOSEnabled - ? NetBIOSResolver.ResolveAsync(host.ipAddress, options.NetBIOSTimeout, cancellationToken) + ? NetBIOSResolver.ResolveAsync(host.ipAddress, options.NetBIOSTimeout, ct) : Task.FromResult(new NetBIOSInfo(host.ipAddress)); - // Get ping result - pingTask.Wait(cancellationToken); - var pingInfo = pingTask.Result; - - // Get port scan result - portScanTask.Wait(cancellationToken); - var portScanResults = portScanTask.Result.ToList(); + await Task.WhenAll(pingTask, portScanTask, netbiosTask).ConfigureAwait(false); - // Get netbios result - netbiosTask.Wait(cancellationToken); + var pingInfo = pingTask.Result; + var portScanResults = portScanTask.Result; var netBIOSInfo = netbiosTask.Result; // Cancel if the user has canceled - cancellationToken.ThrowIfCancellationRequested(); + ct.ThrowIfCancellationRequested(); // Check if host is up var isAnyPortOpen = portScanResults.Any(x => x.State == PortState.Open); var isReachable = pingInfo.Status == IPStatus.Success || // ICMP response - isAnyPortOpen || // Any port is open + isAnyPortOpen || // Any port is open netBIOSInfo.IsReachable; // NetBIOS response // DNS & ARP @@ -145,14 +137,11 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts, if (options.ResolveHostname) { - // Don't use await in Parallel.ForEach, this will break - var dnsResolverTask = DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress); - - // Wait for task inside a Parallel.Foreach - dnsResolverTask.Wait(cancellationToken); + var dnsResult = await DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress) + .WaitAsync(ct).ConfigureAwait(false); - if (!dnsResolverTask.Result.HasError) - dnsHostname = dnsResolverTask.Result.Value; + if (!dnsResult.HasError) + dnsHostname = dnsResult.Value; } // ARP @@ -212,7 +201,7 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts, } IncreaseProgress(); - }); + }).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -225,101 +214,79 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts, }, cancellationToken); } - private Task PingAsync(IPAddress ipAddress, CancellationToken cancellationToken) + private async Task PingAsync(IPAddress ipAddress, CancellationToken cancellationToken) { - return Task.Run(() => + using var ping = new System.Net.NetworkInformation.Ping(); + + for (var i = 0; i < options.ICMPAttempts; i++) { - using var ping = new System.Net.NetworkInformation.Ping(); + // Get timestamp + var timestamp = DateTime.Now; - for (var i = 0; i < options.ICMPAttempts; i++) + try { - // Get timestamp - var timestamp = DateTime.Now; + // Note: the CancellationToken-accepting overload requires a TimeSpan timeout, + // unlike the legacy int-based overloads used elsewhere in .NET's Ping API. + var pingReply = await ping.SendPingAsync(ipAddress, TimeSpan.FromMilliseconds(options.ICMPTimeout), + options.ICMPBuffer, cancellationToken: cancellationToken).ConfigureAwait(false); - try + // Success + if (pingReply is { Status: IPStatus.Success }) { - var pingReply = ping.Send(ipAddress, options.ICMPTimeout, options.ICMPBuffer); - - // Success - if (pingReply is { Status: IPStatus.Success }) + switch (ipAddress.AddressFamily) { - switch (ipAddress.AddressFamily) - { - case AddressFamily.InterNetwork: - return new PingInfo( - timestamp, - pingReply.Address, - pingReply.Buffer.Length, - pingReply.RoundtripTime, - pingReply.Options!.Ttl, - pingReply.Status); - case AddressFamily.InterNetworkV6: - return new PingInfo( + case AddressFamily.InterNetwork: + return new PingInfo( timestamp, pingReply.Address, pingReply.Buffer.Length, pingReply.RoundtripTime, + pingReply.Options!.Ttl, pingReply.Status); - } + case AddressFamily.InterNetworkV6: + return new PingInfo( + timestamp, + pingReply.Address, + pingReply.Buffer.Length, + pingReply.RoundtripTime, + pingReply.Status); } - - // Failed - if (pingReply != null) - return new PingInfo(timestamp, ipAddress, pingReply.Status); - } - catch (PingException) - { - // Ping failed with unknown status - return new PingInfo(timestamp, ipAddress, IPStatus.Unknown); } - // Don't scan again, if the user has canceled (when more than 1 attempt) - if (cancellationToken.IsCancellationRequested) - break; + // Failed + if (pingReply != null) + return new PingInfo(timestamp, ipAddress, pingReply.Status); + } + catch (PingException) + { + // Ping failed with unknown status + return new PingInfo(timestamp, ipAddress, IPStatus.Unknown); } - // Fall back to unknown status - return new PingInfo(DateTime.Now, ipAddress, IPStatus.Unknown); - }, cancellationToken); + // Don't scan again, if the user has canceled (when more than 1 attempt) + if (cancellationToken.IsCancellationRequested) + break; + } + + // Fall back to unknown status + return new PingInfo(DateTime.Now, ipAddress, IPStatus.Unknown); } - private Task> PortScanAsync(IPAddress ipAddress, ParallelOptions parallelOptions, + private async Task> PortScanAsync(IPAddress ipAddress, ParallelOptions parallelOptions, CancellationToken cancellationToken) { ConcurrentBag results = []; - Parallel.ForEach(options.PortScanPorts, parallelOptions, port => + await Parallel.ForEachAsync(options.PortScanPorts, parallelOptions, async (port, ct) => { - // Test if port is open - using var tcpClient = new TcpClient(ipAddress.AddressFamily); + var portState = await PortProbe.ProbeAsync(ipAddress, port, options.PortScanTimeout, ct) + .ConfigureAwait(false); - var portState = PortState.None; - - try - { - // ReSharper disable once MethodSupportsCancellation - Wait for timeout - var task = tcpClient.ConnectAsync(ipAddress, port); - - if (task.Wait(options.PortScanTimeout, cancellationToken)) - portState = tcpClient.Connected ? PortState.Open : PortState.Closed; - else - portState = PortState.TimedOut; - } - catch - { - portState = PortState.Closed; - } - finally - { - tcpClient.Close(); - - if (portState == PortState.Open || options.ShowAllResults) - results.Add( - new PortInfo(port, PortLookup.LookupByPortAndProtocol(port), portState)); - } - }); + if (portState == PortState.Open || options.ShowAllResults) + results.Add(new PortInfo(port, PortLookup.LookupByPortAndProtocol(port), portState)); + }).ConfigureAwait(false); - return Task.FromResult(results.AsEnumerable()); + return results.ToList(); } private void IncreaseProgress() diff --git a/Source/NETworkManager.Models/Network/NetBIOSResolver.cs b/Source/NETworkManager.Models/Network/NetBIOSResolver.cs index 342de111fe..ca44faf100 100644 --- a/Source/NETworkManager.Models/Network/NetBIOSResolver.cs +++ b/Source/NETworkManager.Models/Network/NetBIOSResolver.cs @@ -47,21 +47,20 @@ public static async Task ResolveAsync(IPAddress ipAddress, int time CancellationToken cancellationToken) { var udpClient = new UdpClient(); - udpClient.Client.ReceiveTimeout = timeout; var remoteEndPoint = new IPEndPoint(ipAddress, NetBIOSUdpPort); + // Linked so the receive is genuinely canceled (not just abandoned) on either timeout or + // caller cancellation - avoids leaving a pending receive that later faults, unobserved, + // once the socket is closed in the finally block below. + using var timeoutCts = new CancellationTokenSource(timeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + try { await udpClient.SendAsync(RequestData, RequestData.Length, remoteEndPoint); - // ReSharper disable once MethodSupportsCancellation - cancellation is handled below by Task.WhenAny - var receiveTask = udpClient.ReceiveAsync(); - - if (!receiveTask.Wait(timeout, cancellationToken)) - return new NetBIOSInfo(ipAddress); - - var response = receiveTask.Result; + var response = await udpClient.ReceiveAsync(linkedCts.Token).ConfigureAwait(false); if (response.Buffer.Length < ResponseBaseLen || response.Buffer[ResponseTypePos] != ResponseTypeNbstat) return new NetBIOSInfo(ipAddress); // response was too short diff --git a/Source/NETworkManager.Models/Network/PortProbe.cs b/Source/NETworkManager.Models/Network/PortProbe.cs new file mode 100644 index 0000000000..6e2096abba --- /dev/null +++ b/Source/NETworkManager.Models/Network/PortProbe.cs @@ -0,0 +1,53 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace NETworkManager.Models.Network; + +/// +/// Provides a shared, fully asynchronous TCP connect probe used by and +/// . +/// +internal static class PortProbe +{ + /// + /// Attempts a TCP connect to the given and and + /// classifies the result. Never throws for expected connect failures. + /// + /// IP address to connect to. + /// Port to connect to. + /// Timeout in milliseconds after which the port is considered timed out. + /// Token to monitor for cancellation requests. + /// The of the probed port. + public static async Task ProbeAsync(IPAddress ipAddress, int port, int timeoutMs, + CancellationToken cancellationToken) + { + using var tcpClient = new TcpClient(ipAddress.AddressFamily); + using var timeoutCts = new CancellationTokenSource(timeoutMs); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + try + { + await tcpClient.ConnectAsync(ipAddress, port, linkedCts.Token).ConfigureAwait(false); + + return tcpClient.Connected ? PortState.Open : PortState.Closed; + } + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && + !cancellationToken.IsCancellationRequested) + { + // Only our own timeout fired, not the caller's cancellation -> timed out + return PortState.TimedOut; + } + catch (Exception) when (!cancellationToken.IsCancellationRequested) + { + // Connection refused, host unreachable, etc. + return PortState.Closed; + } + finally + { + tcpClient.Close(); + } + } +} diff --git a/Source/NETworkManager.Models/Network/PortProfile.cs b/Source/NETworkManager.Models/Network/PortProfile.cs index 606efa67d3..f124078cef 100644 --- a/Source/NETworkManager.Models/Network/PortProfile.cs +++ b/Source/NETworkManager.Models/Network/PortProfile.cs @@ -18,7 +18,8 @@ public static List GetDefaultList() new("Database", "1433-1434; 1521; 1830; 3306; 5432"), new("SMB", "139; 445"), new("LDAP", "389; 636"), - new("HTTP proxy", "3128") + new("HTTP proxy", "3128"), + new("Well-known ports", "1-1024") }; } } \ No newline at end of file diff --git a/Source/NETworkManager.Models/Network/PortScanner.cs b/Source/NETworkManager.Models/Network/PortScanner.cs index 372cd2d861..fff1c51681 100644 --- a/Source/NETworkManager.Models/Network/PortScanner.cs +++ b/Source/NETworkManager.Models/Network/PortScanner.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Net; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using NETworkManager.Models.Lookup; @@ -67,7 +66,7 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts, { _progressValue = 0; - Task.Run(() => + Task.Run(async () => { try { @@ -83,58 +82,34 @@ public void ScanAsync(IEnumerable<(IPAddress ipAddress, string hostname)> hosts, MaxDegreeOfParallelism = _options.MaxPortThreads }; - Parallel.ForEach(hosts, hostParallelOptions, host => + await Parallel.ForEachAsync(hosts, hostParallelOptions, async (host, hostCt) => { // Resolve Hostname (PTR) var hostname = string.Empty; if (_options.ResolveHostname) { - // Don't use await in Parallel.ForEach, this will break - var dnsResolverTask = DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress); + var dnsResult = await DNSClient.GetInstance().ResolvePtrAsync(host.ipAddress) + .WaitAsync(hostCt).ConfigureAwait(false); - // Wait for task inside a Parallel.Foreach - dnsResolverTask.Wait(cancellationToken); - - if (!dnsResolverTask.Result.HasError) - hostname = dnsResolverTask.Result.Value; + if (!dnsResult.HasError) + hostname = dnsResult.Value; } // Check each port - Parallel.ForEach(ports, portParallelOptions, port => + await Parallel.ForEachAsync(ports, portParallelOptions, async (port, portCt) => { - // Test if port is open - using (var tcpClient = new TcpClient(host.ipAddress.AddressFamily)) - { - var portState = PortState.None; - - try - { - var task = tcpClient.ConnectAsync(host.ipAddress, port); - - if (task.Wait(_options.Timeout)) - portState = tcpClient.Connected ? PortState.Open : PortState.Closed; - else - portState = PortState.TimedOut; - } - catch - { - portState = PortState.Closed; - } - finally - { - tcpClient.Close(); - - if (_options.ShowAllResults || portState == PortState.Open) - OnPortScanned(new PortScannerPortScannedArgs( - new PortScannerPortInfo(host.ipAddress, hostname, port, - PortLookup.LookupByPortAndProtocol(port), portState))); - } - } + var portState = await PortProbe.ProbeAsync(host.ipAddress, port, _options.Timeout, portCt) + .ConfigureAwait(false); + + if (_options.ShowAllResults || portState == PortState.Open) + OnPortScanned(new PortScannerPortScannedArgs( + new PortScannerPortInfo(host.ipAddress, hostname, port, + PortLookup.LookupByPortAndProtocol(port), portState))); IncreaseProgress(); - }); - }); + }).ConfigureAwait(false); + }).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index d71cb6aab6..c7b9c5adaf 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -61,7 +61,6 @@ public static class GlobalStaticConfiguration // Settings: General public static int General_BackgroundJobInterval => 5; - public static int General_ThreadPoolAdditionalMinThreads => 512; public static int General_HistoryListEntries => 10; // Settings: Window @@ -132,17 +131,17 @@ public static class GlobalStaticConfiguration public static int IPScanner_ICMPBuffer => 32; public static bool IPScanner_ResolveHostname => true; public static bool IPScanner_PortScanEnabled => true; - public static string IPScanner_PortScanPorts => "22; 53; 80; 139; 389; 636; 443; 445; 3389"; + public static string IPScanner_PortScanPorts => "22; 53; 80; 135; 139; 389; 636; 443; 445; 3389; 9100"; public static int IPScanner_PortScanTimeout => 4000; - public static int IPScanner_MaxHostThreads => 256; - public static int IPScanner_MaxPortThreads => 5; + public static int IPScanner_MaxHostThreads => 64; + public static int IPScanner_MaxPortThreads => 4; public static bool IPScanner_NetBIOSEnabled => true; public static int IPScanner_NetBIOSTimeout => 4000; public static ExportFileType IPScanner_ExportFileType => ExportFileType.Csv; // Application: Port Scanner - public static int PortScanner_MaxHostThreads => 5; - public static int PortScanner_MaxPortThreads => 256; + public static int PortScanner_MaxHostThreads => 4; + public static int PortScanner_MaxPortThreads => 64; public static int PortScanner_Timeout => 4000; public static ExportFileType PortScanner_ExportFileType => ExportFileType.Csv; diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs index eb33ec907c..ae78719a25 100644 --- a/Source/NETworkManager.Settings/SettingsInfo.cs +++ b/Source/NETworkManager.Settings/SettingsInfo.cs @@ -152,20 +152,6 @@ public int General_BackgroundJobInterval } } = GlobalStaticConfiguration.General_BackgroundJobInterval; - - public int General_ThreadPoolAdditionalMinThreads - { - get; - set - { - if (value == field) - return; - - field = value; - OnPropertyChanged(); - } - } = GlobalStaticConfiguration.General_ThreadPoolAdditionalMinThreads; - public int General_HistoryListEntries { get; diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 93da62186f..519a0f7777 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -741,6 +741,47 @@ private static void UpgradeTo_2026_7_7_0() private static void UpgradeToLatest(Version version) { Log.Info($"Apply upgrade to {version}..."); + + // IP Scanner / Port Scanner - lower the default concurrency (see changelog for why). + // Only applied if still at the old default, so a deliberately customized value is left alone. + Log.Info("Lower IP Scanner / Port Scanner default concurrency, if still unchanged from the old default..."); + + if (Current.IPScanner_MaxHostThreads == 256) + { + Log.Info("Update \"IPScanner_MaxHostThreads\" from 256 to 64..."); + Current.IPScanner_MaxHostThreads = 64; + } + + if (Current.IPScanner_MaxPortThreads == 5) + { + Log.Info("Update \"IPScanner_MaxPortThreads\" from 5 to 4..."); + Current.IPScanner_MaxPortThreads = 4; + } + + if (Current.PortScanner_MaxHostThreads == 5) + { + Log.Info("Update \"PortScanner_MaxHostThreads\" from 5 to 4..."); + Current.PortScanner_MaxHostThreads = 4; + } + + if (Current.PortScanner_MaxPortThreads == 256) + { + Log.Info("Update \"PortScanner_MaxPortThreads\" from 256 to 64..."); + Current.PortScanner_MaxPortThreads = 64; + } + + // Add new Port Scanner port profiles + foreach (var portProfile in PortProfile.GetDefaultList()) + { + var portProfileFound = + Current.PortScanner_PortProfiles.FirstOrDefault(x => x.Name == portProfile.Name); + + if (portProfileFound != null) + continue; + + Log.Info($"Add \"{portProfile.Name}\" to \"PortScanner_PortProfiles\"..."); + Current.PortScanner_PortProfiles.Add(portProfile); + } } #endregion } diff --git a/Source/NETworkManager/App.xaml.cs b/Source/NETworkManager/App.xaml.cs index 74ac6812a3..d041c1c105 100644 --- a/Source/NETworkManager/App.xaml.cs +++ b/Source/NETworkManager/App.xaml.cs @@ -165,27 +165,6 @@ by BornToBeRoot Log.Info("Background job is disabled."); } - // Setup ThreadPool for the application - ThreadPool.GetMaxThreads(out var workerThreadsMax, out var completionPortThreadsMax); - ThreadPool.GetMinThreads(out var workerThreadsMin, out var completionPortThreadsMin); - - var workerThreadsMinNew = workerThreadsMin + SettingsManager.Current.General_ThreadPoolAdditionalMinThreads; - var completionPortThreadsMinNew = completionPortThreadsMin + - SettingsManager.Current.General_ThreadPoolAdditionalMinThreads; - - if (workerThreadsMinNew > workerThreadsMax) - workerThreadsMinNew = workerThreadsMax; - - if (completionPortThreadsMinNew > completionPortThreadsMax) - completionPortThreadsMinNew = completionPortThreadsMax; - - if (ThreadPool.SetMinThreads(workerThreadsMinNew, completionPortThreadsMinNew)) - Log.Info( - $"ThreadPool min threads set to: workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}"); - else - Log.Warn( - $"ThreadPool min threads could not be set to workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}"); - // Show splash screen if (SettingsManager.Current.SplashScreen_Enabled) { diff --git a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs index c5321a0d50..388ceb5a64 100644 --- a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs +++ b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs @@ -43,6 +43,18 @@ public class IPScannerViewModel : ViewModelBase, IProfileManagerMinimal private bool _firstLoad = true; private bool _closed; + // Background HostScanned events append here instead of hopping to the UI thread per item - + // a DispatcherTimer periodically flushes this into the Results collection instead, so a large + // scan doesn't flood the dispatcher queue with one BeginInvoke per host. + private readonly List _resultsBuffer = []; + private readonly Lock _resultsBufferLock = new(); + private DispatcherTimer _resultsFlushTimer; + + // Same reasoning as the results buffer above - ProgressChanged fires once per host + // (unconditionally, unlike HostScanned), so it's flushed to the bound property on the same + // timer instead of updating it directly from the background thread on every event. + private int _latestHostsScanned; + /// /// Gets or sets the host or IP range to scan. /// @@ -435,6 +447,13 @@ private async Task Start() IsRunning = true; PreparingScan = true; + _resultsFlushTimer?.Stop(); + + lock (_resultsBufferLock) + { + _resultsBuffer.Clear(); + } + Results.Clear(); DragablzTabItem.SetTabHeader(_tabId, Host); @@ -466,6 +485,7 @@ private async Task Start() HostsToScan = hosts.hosts.Count; HostsScanned = 0; + Volatile.Write(ref _latestHostsScanned, 0); PreparingScan = false; @@ -493,6 +513,17 @@ await PortRangeHelper.ConvertPortRangeToIntArrayAsync(SettingsManager.Current.IP ipScanner.ProgressChanged += ProgressChanged; ipScanner.UserHasCanceled += UserHasCanceled; + _resultsFlushTimer = new DispatcherTimer(DispatcherPriority.Background) + { + Interval = TimeSpan.FromMilliseconds(150) + }; + _resultsFlushTimer.Tick += (_, _) => + { + FlushResultsBuffer(); + FlushProgress(); + }; + _resultsFlushTimer.Start(); + ipScanner.ScanAsync(hosts.hosts, _cancellationTokenSource.Token); } @@ -704,21 +735,53 @@ public void OnClose() /// The instance containing the event data. private void HostScanned(object sender, IPScannerHostScannedArgs e) { - Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, - new Action(delegate - { - Results.Add(e.Args); - })); + lock (_resultsBufferLock) + { + _resultsBuffer.Add(e.Args); + } } /// - /// Handles the ProgressChanged event. Updates the progress. + /// Moves buffered scan results into . Always called on the UI thread - + /// either from the tick, or from within a Dispatcher.Invoke + /// call in / . + /// + private void FlushResultsBuffer() + { + List itemsToAdd; + + lock (_resultsBufferLock) + { + if (_resultsBuffer.Count == 0) + return; + + itemsToAdd = [.. _resultsBuffer]; + _resultsBuffer.Clear(); + } + + foreach (var item in itemsToAdd) + Results.Add(item); + } + + /// + /// Handles the ProgressChanged event. Stores the value for to + /// pick up on the next timer tick, instead of updating the bound property directly from a + /// background thread on every single host. /// /// The source of the event. /// The instance containing the event data. private void ProgressChanged(object sender, ProgressChangedArgs e) { - HostsScanned = e.Value; + Volatile.Write(ref _latestHostsScanned, e.Value); + } + + /// + /// Pushes the latest buffered progress value into . Always called + /// on the UI thread - same calling contexts as . + /// + private void FlushProgress() + { + HostsScanned = Volatile.Read(ref _latestHostsScanned); } /// @@ -732,6 +795,10 @@ private void ScanComplete(object sender, EventArgs e) // to ensure all results are added first #3285 Application.Current.Dispatcher.Invoke(() => { + _resultsFlushTimer?.Stop(); + FlushResultsBuffer(); + FlushProgress(); + if (Results.Count == 0) { StatusMessage = Strings.NoReachableHostsFound; @@ -750,11 +817,18 @@ private void ScanComplete(object sender, EventArgs e) /// The instance containing the event data. private void UserHasCanceled(object sender, EventArgs e) { - StatusMessage = Strings.CanceledByUserMessage; - IsStatusMessageDisplayed = true; + Application.Current.Dispatcher.Invoke(() => + { + _resultsFlushTimer?.Stop(); + FlushResultsBuffer(); + FlushProgress(); - IsCanceling = false; - IsRunning = false; + StatusMessage = Strings.CanceledByUserMessage; + IsStatusMessageDisplayed = true; + + IsCanceling = false; + IsRunning = false; + }); } #endregion diff --git a/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs b/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs index 1ea72bf4c1..06a21f8033 100644 --- a/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortProfilesViewModel.cs @@ -26,8 +26,17 @@ public class PortProfilesViewModel : ViewModelBase /// The action to execute when the Cancel command is invoked. public PortProfilesViewModel(Action okCommand, Action cancelHandler) { - OKCommand = new RelayCommand(_ => okCommand(this)); - CancelCommand = new RelayCommand(_ => cancelHandler(this)); + // Clear the filter on close so it doesn't leak into other views sharing this collection's default view. + OKCommand = new RelayCommand(_ => + { + PortProfiles.Filter = null; + okCommand(this); + }); + CancelCommand = new RelayCommand(_ => + { + PortProfiles.Filter = null; + cancelHandler(this); + }); PortProfiles = CollectionViewSource.GetDefaultView(SettingsManager.Current.PortScanner_PortProfiles); PortProfiles.SortDescriptions.Add( diff --git a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs index f683de1179..44f9f26e8e 100644 --- a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs +++ b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs @@ -38,6 +38,18 @@ public class PortScannerViewModel : ViewModelBase private bool _firstLoad = true; private bool _closed; + // Background PortScanned events append here instead of hopping to the UI thread per item - + // a DispatcherTimer periodically flushes this into the Results collection instead, so a large + // scan doesn't flood the dispatcher queue with one BeginInvoke per port. + private readonly List _resultsBuffer = []; + private readonly Lock _resultsBufferLock = new(); + private DispatcherTimer _resultsFlushTimer; + + // Same reasoning as the results buffer above - ProgressChanged fires once per port + // (unconditionally, unlike PortScanned), so it's flushed to the bound property on the same + // timer instead of updating it directly from the background thread on every event. + private int _latestPortsScanned; + /// /// Gets or sets the host to scan. /// @@ -386,6 +398,13 @@ private async Task Start() IsRunning = true; PreparingScan = true; + _resultsFlushTimer?.Stop(); + + lock (_resultsBufferLock) + { + _resultsBuffer.Clear(); + } + Results.Clear(); DragablzTabItem.SetTabHeader(_tabId, Host); @@ -420,6 +439,7 @@ private async Task Start() PortsToScan = ports.Length * hosts.hosts.Count; PortsScanned = 0; + Volatile.Write(ref _latestPortsScanned, 0); PreparingScan = false; @@ -440,6 +460,17 @@ private async Task Start() portScanner.ProgressChanged += ProgressChanged; portScanner.UserHasCanceled += UserHasCanceled; + _resultsFlushTimer = new DispatcherTimer(DispatcherPriority.Background) + { + Interval = TimeSpan.FromMilliseconds(150) + }; + _resultsFlushTimer.Tick += (_, _) => + { + FlushResultsBuffer(); + FlushProgress(); + }; + _resultsFlushTimer.Start(); + portScanner.ScanAsync(hosts.hosts, ports, _cancellationTokenSource.Token); } @@ -531,13 +562,46 @@ private void AddPortToHistory(string port) private void PortScanned(object sender, PortScannerPortScannedArgs e) { - Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, - new Action(delegate { Results.Add(e.Args); })); + lock (_resultsBufferLock) + { + _resultsBuffer.Add(e.Args); + } + } + + /// + /// Moves buffered scan results into . Always called on the UI thread - + /// either from the tick, or from within a Dispatcher.Invoke + /// call in / . + /// + private void FlushResultsBuffer() + { + List itemsToAdd; + + lock (_resultsBufferLock) + { + if (_resultsBuffer.Count == 0) + return; + + itemsToAdd = [.. _resultsBuffer]; + _resultsBuffer.Clear(); + } + + foreach (var item in itemsToAdd) + Results.Add(item); } private void ProgressChanged(object sender, ProgressChangedArgs e) { - PortsScanned = e.Value; + Volatile.Write(ref _latestPortsScanned, e.Value); + } + + /// + /// Pushes the latest buffered progress value into . Always called + /// on the UI thread - same calling contexts as . + /// + private void FlushProgress() + { + PortsScanned = Volatile.Read(ref _latestPortsScanned); } private void ScanComplete(object sender, EventArgs e) @@ -546,6 +610,10 @@ private void ScanComplete(object sender, EventArgs e) // to ensure all results are added first #3285 Application.Current.Dispatcher.Invoke(() => { + _resultsFlushTimer?.Stop(); + FlushResultsBuffer(); + FlushProgress(); + if (Results.Count == 0) { StatusMessage = Strings.NoOpenPortsFound; @@ -559,11 +627,18 @@ private void ScanComplete(object sender, EventArgs e) private void UserHasCanceled(object sender, EventArgs e) { - StatusMessage = Strings.CanceledByUserMessage; - IsStatusMessageDisplayed = true; + Application.Current.Dispatcher.Invoke(() => + { + _resultsFlushTimer?.Stop(); + FlushResultsBuffer(); + FlushProgress(); + + StatusMessage = Strings.CanceledByUserMessage; + IsStatusMessageDisplayed = true; - IsCanceling = false; - IsRunning = false; + IsCanceling = false; + IsRunning = false; + }); } #endregion diff --git a/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs b/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs index 300ef818ee..ddad5752de 100644 --- a/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsGeneralViewModel.cs @@ -45,22 +45,6 @@ public int BackgroundJobInterval } } - public int ThreadPoolAdditionalMinThreads - { - get; - set - { - if (value == field) - return; - - if (!_isLoading) - SettingsManager.Current.General_ThreadPoolAdditionalMinThreads = value; - - field = value; - OnPropertyChanged(); - } - } - public int HistoryListEntries { get; @@ -100,7 +84,6 @@ private void LoadSettings() }.View; BackgroundJobInterval = SettingsManager.Current.General_BackgroundJobInterval; - ThreadPoolAdditionalMinThreads = SettingsManager.Current.General_ThreadPoolAdditionalMinThreads; HistoryListEntries = SettingsManager.Current.General_HistoryListEntries; } diff --git a/Source/NETworkManager/Views/IPScannerSettingsView.xaml b/Source/NETworkManager/Views/IPScannerSettingsView.xaml index 3d3cc946a3..29b8164223 100644 --- a/Source/NETworkManager/Views/IPScannerSettingsView.xaml +++ b/Source/NETworkManager/Views/IPScannerSettingsView.xaml @@ -138,16 +138,5 @@ - - - - - - - - - \ No newline at end of file diff --git a/Source/NETworkManager/Views/PortScannerSettingsView.xaml b/Source/NETworkManager/Views/PortScannerSettingsView.xaml index 27077b6ac8..aaf35b0e34 100644 --- a/Source/NETworkManager/Views/PortScannerSettingsView.xaml +++ b/Source/NETworkManager/Views/PortScannerSettingsView.xaml @@ -103,16 +103,5 @@ - - - - - - - - - \ No newline at end of file diff --git a/Source/NETworkManager/Views/SettingsGeneralView.xaml b/Source/NETworkManager/Views/SettingsGeneralView.xaml index 6eec80259f..4215a2dc17 100644 --- a/Source/NETworkManager/Views/SettingsGeneralView.xaml +++ b/Source/NETworkManager/Views/SettingsGeneralView.xaml @@ -186,19 +186,6 @@ - - - - - - -