Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 int _hostsUp;
private int _hostsDown;

/// <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 => Volatile.Read(ref _hostsUp);

/// <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 => Volatile.Read(ref _hostsDown);

#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)
Interlocked.Increment(ref _hostsUp);
else
Interlocked.Increment(ref _hostsDown);

// DNS & ARP
if (isReachable || options.ShowAllResults)
{
Expand Down
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 int _portsOpen;
private int _portsClosed;

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 => Volatile.Read(ref _portsOpen);

/// <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 => Volatile.Read(ref _portsClosed);

#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)
Interlocked.Increment(ref _portsOpen);
else
Interlocked.Increment(ref _portsClosed);

if (_options.ShowAllResults || portState == PortState.Open)
OnPortScanned(new PortScannerPortScannedArgs(
new PortScannerPortInfo(host.ipAddress, hostname, port,
Expand Down
53 changes: 50 additions & 3 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 @@ -485,7 +519,11 @@ private async Task Start()

HostsToScan = hosts.hosts.Count;
HostsScanned = 0;
HostsUp = 0;
HostsDown = 0;
Volatile.Write(ref _latestHostsScanned, 0);
Volatile.Write(ref _latestHostsUp, 0);
Volatile.Write(ref _latestHostsDown, 0);

PreparingScan = false;

Expand Down Expand Up @@ -768,20 +806,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using NETworkManager.Views;

namespace NETworkManager.ViewModels;

/// <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 <see cref="PingMonitorHostViewModel.HostsChangeVersion"/> as the second. The second
/// value isn't used directly, it only forces re-evaluation whenever a host's
/// <see cref="PingMonitorViewModel.IsReachable"/>/<see cref="PingMonitorViewModel.IsRunning"/>
/// 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 hosts = group.Items.OfType<PingMonitorView>().Select(host => host.ViewModel).ToList();

switch (parameter as string)
{
case "Up":
return $"{hosts.Count(host => host.IsRunning && host.IsReachable)} {values[2]}";
case "Down":
return $"{hosts.Count(host => host.IsRunning && !host.IsReachable)} {values[2]}";
case "Paused":
return $"{hosts.Count(host => !host.IsRunning)} {values[2]}";
case "PausedVisibility":
return hosts.Any(host => !host.IsRunning) ? Visibility.Visible : Visibility.Collapsed;
default:
return string.Empty;
}
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
49 changes: 49 additions & 0 deletions Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -136,6 +137,25 @@ public ObservableCollection<PingMonitorView> Hosts
/// </summary>
public ICollectionView HostsView { get; }

/// <summary>
/// Bumped whenever a host is added/removed, or a host's reachability/running state
/// changes. Used only as a change-notification trigger for the per-group up/down summary
/// (see <see cref="PingMonitorGroupSummaryConverter"/>) - the value itself carries no
/// meaning.
/// </summary>
public int HostsChangeVersion
{
get;
private set
{
if (value == field)
return;

field = value;
OnPropertyChanged();
}
}

#endregion

#region Constructor, load settings
Expand All @@ -152,6 +172,7 @@ public PingMonitorHostViewModel()
HostsView = CollectionViewSource.GetDefaultView(Hosts);
HostsView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(PingMonitorView.Group)));
HostsView.SortDescriptions.Add(new SortDescription(nameof(PingMonitorView.Group), ListSortDirection.Ascending));
Hosts.CollectionChanged += Hosts_CollectionChanged;

InitializeProfileHost();
}
Expand Down Expand Up @@ -372,5 +393,33 @@ private void UserHasCanceled()
IsRunning = false;
}

/// <summary>
/// Keeps each host's <see cref="PingMonitorViewModel.PropertyChanged"/> subscription in
/// sync with <see cref="Hosts"/>, and bumps <see cref="HostsChangeVersion"/> so the
/// per-group up/down summary (<see cref="PingMonitorGroupSummaryConverter"/>) re-evaluates.
/// </summary>
private void Hosts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (PingMonitorView host in e.NewItems)
host.ViewModel.PropertyChanged += HostViewModel_PropertyChanged;

if (e.OldItems != null)
foreach (PingMonitorView host in e.OldItems)
host.ViewModel.PropertyChanged -= HostViewModel_PropertyChanged;

HostsChangeVersion++;
}

/// <summary>
/// Bumps <see cref="HostsChangeVersion"/> whenever a host's reachability or running state
/// changes, so the per-group up/down summary re-evaluates.
/// </summary>
private void HostViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(PingMonitorViewModel.IsReachable) or nameof(PingMonitorViewModel.IsRunning))
HostsChangeVersion++;
}

#endregion
}
Loading
Loading