Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using NETworkManager.Models.Network;

namespace NETworkManager.Converters;

/// <summary>
/// Formats the count of hosts up (reachable), down (unreachable) or paused (not running) within
/// a Ping Monitor group, selected via <c>ConverterParameter</c> ("Up", "Down", "Paused" - formats
/// "{count} {label}" using the label bound as the third value - or "PausedVisibility", which
/// instead returns a <see cref="Visibility"/> so the paused count can be hidden while zero).
/// </summary>
/// <remarks>
/// Bound as a <see cref="MultiBinding"/> with the <see cref="CollectionViewGroup"/> as the first
/// value and a per-group change-notification trigger as the second. The second value isn't used
/// directly, it only forces re-evaluation whenever a host's <see cref="IPingMonitorHostStatus"/>
/// changes, since <see cref="CollectionViewGroup"/> itself only raises change notifications for
/// item add/remove, not for property changes on its items.
/// </remarks>
public sealed class PingMonitorGroupSummaryConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 0 || values[0] is not CollectionViewGroup group)
return parameter as string == "PausedVisibility" ? Visibility.Collapsed : string.Empty;

var up = 0;
var down = 0;
var paused = 0;

foreach (var host in group.Items.OfType<IPingMonitorHostStatus>())
{
if (!host.IsRunning)
paused++;
else if (host.IsReachable)
up++;
else
down++;
}

return parameter as string switch
{
"Up" => $"{up} {values[2]}",
"Down" => $"{down} {values[2]}",
"Paused" => $"{paused} {values[2]}",
"PausedVisibility" => paused > 0 ? Visibility.Visible : Visibility.Collapsed,
_ => string.Empty
};
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
35 changes: 31 additions & 4 deletions Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Source/NETworkManager.Localization/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@
<data name="DontFragment" xml:space="preserve">
<value>Don't fragment</value>
</data>
<data name="Down" xml:space="preserve">
<value>Down</value>
</data>
<data name="EditCredentials" xml:space="preserve">
<value>Edit credentials</value>
</data>
Expand Down Expand Up @@ -1252,6 +1255,9 @@ Profile files are not affected!</value>
<data name="UntrayBringWindowToForeground" xml:space="preserve">
<value>Untray / Bring window to foreground</value>
</data>
<data name="Up" xml:space="preserve">
<value>Up</value>
</data>
<data name="URL" xml:space="preserve">
<value>URL</value>
</data>
Expand Down Expand Up @@ -2442,6 +2448,9 @@ is disabled!</value>
<data name="Pause" xml:space="preserve">
<value>Pause</value>
</data>
<data name="Paused" xml:space="preserve">
<value>Paused</value>
</data>
<data name="Resume" xml:space="preserve">
<value>Resume</value>
</data>
Expand Down Expand Up @@ -2836,7 +2845,7 @@ is disabled!</value>
<value>Received</value>
</data>
<data name="StatusChange" xml:space="preserve">
<value>Status change</value>
<value>Last status change</value>
</data>
<data name="UpdateAvailable" xml:space="preserve">
<value>Update available!</value>
Expand Down
21 changes: 21 additions & 0 deletions Source/NETworkManager.Models/Network/IPScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ public sealed class IPScanner(IPScannerOptions options)
#region Variables

private int _progressValue;
private readonly InterlockedCounter _hostsUp = new();
private readonly InterlockedCounter _hostsDown = new();

/// <summary>
/// Gets the number of hosts found to be reachable so far. Thread-safe; may be read from
/// any thread while the scan is running.
/// </summary>
public int HostsUp => _hostsUp.Value;

/// <summary>
/// Gets the number of hosts found to be unreachable so far. Thread-safe; may be read from
/// any thread while the scan is running.
/// </summary>
public int HostsDown => _hostsDown.Value;

#endregion

Expand Down Expand Up @@ -129,6 +143,13 @@ await Parallel.ForEachAsync(hosts, hostParallelOptions, async (host, ct) =>
isAnyPortOpen || // Any port is open
netBIOSInfo.IsReachable; // NetBIOS response

// Count reachable/unreachable hosts unconditionally, since ShowAllResults
// (below) may prevent unreachable hosts from ever reaching HostScanned
if (isReachable)
_hostsUp.Increment();
else
_hostsDown.Increment();

// DNS & ARP
if (isReachable || options.ShowAllResults)
{
Expand Down
19 changes: 19 additions & 0 deletions Source/NETworkManager.Models/Network/IPingMonitorHostStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace NETworkManager.Models.Network;

/// <summary>
/// Minimal reachability/running status of a Ping Monitor host, exposed so lower-level
/// projects (e.g. converters) can read a host's status without depending on the concrete
/// View/ViewModel types that implement it.
/// </summary>
public interface IPingMonitorHostStatus
{
/// <summary>
/// Gets a value indicating whether the host is reachable (responds to ping).
/// </summary>
bool IsReachable { get; }

/// <summary>
/// Gets a value indicating whether the ping monitoring is currently running.
/// </summary>
bool IsRunning { get; }
}
21 changes: 21 additions & 0 deletions Source/NETworkManager.Models/Network/PortScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,23 @@ public PortScanner(PortScannerOptions options)
#region Variables

private int _progressValue;
private readonly InterlockedCounter _portsOpen = new();
private readonly InterlockedCounter _portsClosed = new();

private readonly PortScannerOptions _options;

/// <summary>
/// Gets the number of ports found to be open so far. Thread-safe; may be read from any
/// thread while the scan is running.
/// </summary>
public int PortsOpen => _portsOpen.Value;

/// <summary>
/// Gets the number of ports found to be closed (or timed out) so far. Thread-safe; may be
/// read from any thread while the scan is running.
/// </summary>
public int PortsClosed => _portsClosed.Value;

#endregion

#region Events
Expand Down Expand Up @@ -102,6 +116,13 @@ await Parallel.ForEachAsync(ports, portParallelOptions, async (port, portCt) =>
var portState = await PortProbe.ProbeAsync(host.ipAddress, port, _options.Timeout, portCt)
.ConfigureAwait(false);

// Count open/closed ports unconditionally, since ShowAllResults (below)
// may prevent closed ports from ever reaching PortScanned
if (portState == PortState.Open)
_portsOpen.Increment();
else
_portsClosed.Increment();

if (_options.ShowAllResults || portState == PortState.Open)
OnPortScanned(new PortScannerPortScannedArgs(
new PortScannerPortInfo(host.ipAddress, hostname, port,
Expand Down
25 changes: 25 additions & 0 deletions Source/NETworkManager.Utilities/InterlockedCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Threading;

namespace NETworkManager.Utilities;

/// <summary>
/// A simple counter that can be incremented from any thread and read from any other thread
/// without locking.
/// </summary>
public sealed class InterlockedCounter
{
private int _value;

/// <summary>
/// Gets the current value.
/// </summary>
public int Value => Volatile.Read(ref _value);

/// <summary>
/// Increments the value by one.
/// </summary>
public void Increment()
{
Interlocked.Increment(ref _value);
}
}
62 changes: 57 additions & 5 deletions Source/NETworkManager/ViewModels/IPScannerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public class IPScannerViewModel : ViewModelBase, IProfileManagerMinimal
// (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;
private int _latestHostsUp;
private int _latestHostsDown;

/// <summary>
/// Gets or sets the host or IP range to scan.
Expand Down Expand Up @@ -209,6 +211,38 @@ public int HostsScanned
}
}

/// <summary>
/// Gets or sets the number of hosts found to be reachable so far.
/// </summary>
public int HostsUp
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
}

/// <summary>
/// Gets or sets the number of hosts found to be unreachable so far.
/// </summary>
public int HostsDown
{
get;
set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
}

/// <summary>
/// Gets or sets a value indicating whether the scan is being prepared.
/// </summary>
Expand Down Expand Up @@ -456,6 +490,17 @@ private async Task Start()

Results.Clear();

// Reset before hostname resolution too (not just after), so a cancellation during
// resolution can't flush the previous scan's stale totals - HostsToScan = 0 also hides
// the up/down summary until the new scan's host count is known.
HostsToScan = 0;
HostsScanned = 0;
HostsUp = 0;
HostsDown = 0;
Volatile.Write(ref _latestHostsScanned, 0);
Volatile.Write(ref _latestHostsUp, 0);
Volatile.Write(ref _latestHostsDown, 0);

DragablzTabItem.SetTabHeader(_tabId, Host);

_cancellationTokenSource?.Dispose();
Expand Down Expand Up @@ -484,8 +529,6 @@ private async Task Start()
}

HostsToScan = hosts.hosts.Count;
HostsScanned = 0;
Volatile.Write(ref _latestHostsScanned, 0);

PreparingScan = false;

Expand Down Expand Up @@ -768,20 +811,29 @@ private void FlushResultsBuffer()
/// pick up on the next timer tick, instead of updating the bound property directly from a
/// background thread on every single host.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="sender">The <see cref="IPScanner"/> instance raising the event.</param>
/// <param name="e">The <see cref="ProgressChangedArgs"/> instance containing the event data.</param>
private void ProgressChanged(object sender, ProgressChangedArgs e)
{
Volatile.Write(ref _latestHostsScanned, e.Value);

if (sender is IPScanner ipScanner)
{
Volatile.Write(ref _latestHostsUp, ipScanner.HostsUp);
Volatile.Write(ref _latestHostsDown, ipScanner.HostsDown);
}
}

/// <summary>
/// Pushes the latest buffered progress value into <see cref="HostsScanned"/>. Always called
/// on the UI thread - same calling contexts as <see cref="FlushResultsBuffer"/>.
/// Pushes the latest buffered progress value into <see cref="HostsScanned"/>,
/// <see cref="HostsUp"/> and <see cref="HostsDown"/>. Always called on the UI thread - same
/// calling contexts as <see cref="FlushResultsBuffer"/>.
/// </summary>
private void FlushProgress()
{
HostsScanned = Volatile.Read(ref _latestHostsScanned);
HostsUp = Volatile.Read(ref _latestHostsUp);
HostsDown = Volatile.Read(ref _latestHostsDown);
}

/// <summary>
Expand Down
Loading
Loading