Skip to content

Commit 879e5db

Browse files
committed
Fix: Based on Copilot feedback
1 parent f3ddd23 commit 879e5db

6 files changed

Lines changed: 50 additions & 81 deletions

File tree

Source/NETworkManager.Models/Export/ExportManager.NeighborInfo.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text;
66
using System.Xml.Linq;
77
using NETworkManager.Models.Network;
8+
using NETworkManager.Utilities;
89
using Newtonsoft.Json;
910

1011
namespace NETworkManager.Models.Export;
@@ -48,7 +49,7 @@ private static void CreateCsv(IEnumerable<NeighborInfo> collection, string fileP
4849

4950
foreach (var info in collection)
5051
stringBuilder.AppendLine(
51-
$"{info.IPAddress},{info.MACAddress},{info.InterfaceAlias},{info.InterfaceIndex},{info.State},{info.AddressFamily},{info.IsMulticast}");
52+
$"{info.IPAddress},{info.MACAddress},{CsvHelper.QuoteString(info.InterfaceAlias)},{info.InterfaceIndex},{info.State},{info.AddressFamily},{info.IsMulticast}");
5253

5354
File.WriteAllText(filePath, stringBuilder.ToString());
5455
}

Source/NETworkManager.Models/Firewall/Firewall.cs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ namespace NETworkManager.Models.Firewall;
1515

1616
/// <summary>
1717
/// Provides static methods to read and modify Windows Firewall rules via PowerShell.
18-
/// All operations share a single <see cref="Runspace"/> that is initialized once with
19-
/// the required execution policy and the NetSecurity module, reducing per-call overhead.
20-
/// A <see cref="SemaphoreSlim"/> serializes access so the runspace is never used concurrently.
18+
/// All operations share a single <see cref="Runspace"/> that is lazily initialized on
19+
/// first use with the required execution policy and the <c>NetSecurity</c> module imported,
20+
/// reducing per-call overhead. A <see cref="SemaphoreSlim"/> serializes access so the
21+
/// runspace is never used concurrently.
2122
/// </summary>
2223
public class Firewall
2324
{
@@ -40,26 +41,26 @@ public class Firewall
4041
private static readonly SemaphoreSlim Lock = new(1, 1);
4142

4243
/// <summary>
43-
/// Shared PowerShell runspace, initialized once in the static constructor with
44-
/// <c>Set-ExecutionPolicy Bypass</c> and <c>Import-Module NetSecurity</c>.
44+
/// Lazily initialized PowerShell runspace. Created and configured on first access so that
45+
/// simply navigating to the Firewall view does not start a PowerShell process unless a
46+
/// modifying operation is actually performed.
4547
/// </summary>
46-
private static readonly Runspace SharedRunspace;
47-
48-
/// <summary>
49-
/// Opens <see cref="SharedRunspace"/> and runs the one-time initialization script
50-
/// so that subsequent operations do not need to repeat the module import.
51-
/// </summary>
52-
static Firewall()
48+
private static readonly Lazy<Runspace> _sharedRunspace = new(() =>
5349
{
54-
SharedRunspace = RunspaceFactory.CreateRunspace();
55-
SharedRunspace.Open();
50+
var runspace = RunspaceFactory.CreateRunspace();
51+
runspace.Open();
5652

5753
using var ps = SMA.PowerShell.Create();
58-
ps.Runspace = SharedRunspace;
54+
ps.Runspace = runspace;
5955
ps.AddScript(@"
6056
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
6157
Import-Module NetSecurity -ErrorAction Stop").Invoke();
62-
}
58+
59+
return runspace;
60+
});
61+
62+
/// <summary>Returns the shared runspace, initializing it on first access.</summary>
63+
private static Runspace SharedRunspace => _sharedRunspace.Value;
6364

6465
#endregion
6566

Source/NETworkManager.Models/Network/NeighborTable.cs

Lines changed: 25 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ namespace NETworkManager.Models.Network;
1919
/// Provides static methods to read and modify the Windows IP neighbor table
2020
/// (IPv4 ARP and IPv6 NDP). Read access uses the <c>GetIpNetTable2</c> Win32 API.
2121
/// Modifying operations (add/delete entries, clear table) run via PowerShell in a
22-
/// shared <see cref="Runspace"/> that is initialized once with the required execution
23-
/// policy and the <c>NetTCPIP</c> module imported. A <see cref="SemaphoreSlim"/>
24-
/// serializes access so the runspace is never used concurrently. Modifying operations
25-
/// require the application to run with elevated rights.
22+
/// shared <see cref="Runspace"/> that is lazily initialized on first use with the
23+
/// required execution policy and the <c>NetTCPIP</c> module imported. A
24+
/// <see cref="SemaphoreSlim"/> serializes access so the runspace is never used
25+
/// concurrently. Modifying operations require the application to run with elevated rights.
2626
/// </summary>
2727
public class NeighborTable
2828
{
@@ -75,11 +75,7 @@ private struct MIB_IPNET_ROW2
7575
[DllImport("Iphlpapi.dll")]
7676
private static extern void FreeMibTable(IntPtr memory);
7777

78-
/// <summary>Looks up a single neighbor cache entry by address. Returns 0 on success.</summary>
79-
[DllImport("Iphlpapi.dll")]
80-
private static extern uint GetIpNetEntry2(ref MIB_IPNET_ROW2 row);
81-
82-
/// <summary>
78+
/// <summary>
8379
/// Ensures that only one PowerShell pipeline runs on <see cref="SharedRunspace"/> at a time.
8480
/// </summary>
8581
private static readonly SemaphoreSlim Lock = new(1, 1);
@@ -97,26 +93,26 @@ private struct MIB_IPNET_ROW2
9793
private static readonly TimeSpan InterfaceAliasCacheDuration = TimeSpan.FromMinutes(5);
9894

9995
/// <summary>
100-
/// Shared PowerShell runspace, initialized once in the static constructor with
101-
/// <c>Set-ExecutionPolicy Bypass</c> and <c>Import-Module NetTCPIP</c> so that
102-
/// subsequent operations do not need to repeat the module import.
96+
/// Lazily initialized PowerShell runspace. Created and configured on first access so that
97+
/// read-only paths (e.g. MAC address lookup in IP Scanner) do not start a PowerShell
98+
/// process unless a modifying operation is actually performed.
10399
/// </summary>
104-
private static readonly Runspace SharedRunspace;
105-
106-
/// <summary>
107-
/// Opens <see cref="SharedRunspace"/> and runs the one-time initialization script.
108-
/// </summary>
109-
static NeighborTable()
100+
private static readonly Lazy<Runspace> _sharedRunspace = new(() =>
110101
{
111-
SharedRunspace = RunspaceFactory.CreateRunspace();
112-
SharedRunspace.Open();
102+
var runspace = RunspaceFactory.CreateRunspace();
103+
runspace.Open();
113104

114105
using var ps = SMA.PowerShell.Create();
115-
ps.Runspace = SharedRunspace;
106+
ps.Runspace = runspace;
116107
ps.AddScript(@"
117108
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
118109
Import-Module NetTCPIP -ErrorAction Stop").Invoke();
119-
}
110+
111+
return runspace;
112+
});
113+
114+
/// <summary>Returns the shared runspace, initializing it on first access.</summary>
115+
private static Runspace SharedRunspace => _sharedRunspace.Value;
120116

121117
#endregion
122118

@@ -226,7 +222,9 @@ private static List<NeighborInfo> GetTable()
226222
list.Add(new NeighborInfo(
227223
ipAddress,
228224
macAddress,
229-
ipAddress.IsIPv6Multicast || IPv4Address.IsMulticast(ipAddress),
225+
addressFamily == AddressFamily.InterNetworkV6
226+
? ipAddress.IsIPv6Multicast
227+
: IPv4Address.IsMulticast(ipAddress),
230228
(int)row.InterfaceIndex,
231229
alias ?? string.Empty,
232230
(NeighborState)row.State,
@@ -289,47 +287,13 @@ private static Dictionary<int, string> BuildInterfaceAliasMap()
289287
}
290288

291289
/// <summary>
292-
/// Returns the MAC address for <paramref name="ipAddress"/> via <c>GetIpNetEntry2</c>,
293-
/// or <see langword="null"/> when no cache entry exists. Supports both IPv4 and IPv6.
290+
/// Returns the MAC address for <paramref name="ipAddress"/> by scanning the neighbor
291+
/// cache, or <see langword="null"/> when no entry exists. Supports both IPv4 and IPv6.
294292
/// </summary>
295293
public static string GetMACAddress(IPAddress ipAddress)
296294
{
297-
var addressFamily = ipAddress.AddressFamily;
298-
299-
if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6)
300-
return null;
301-
302-
var row = new MIB_IPNET_ROW2
303-
{
304-
Address = new byte[SOCKADDR_INET_SIZE],
305-
PhysicalAddress = new byte[IF_MAX_PHYS_ADDRESS_LENGTH]
306-
};
307-
308-
if (addressFamily == AddressFamily.InterNetwork)
309-
{
310-
// SOCKADDR_IN: family(2) at offset 0, addr(4) at offset 4
311-
BitConverter.GetBytes(AF_INET).CopyTo(row.Address, 0);
312-
ipAddress.GetAddressBytes().CopyTo(row.Address, 4);
313-
}
314-
else
315-
{
316-
// SOCKADDR_IN6: family(2) at offset 0, addr(16) at offset 8, scope_id(4) at offset 24
317-
BitConverter.GetBytes(AF_INET6).CopyTo(row.Address, 0);
318-
ipAddress.GetAddressBytes().CopyTo(row.Address, 8);
319-
BitConverter.GetBytes((uint)ipAddress.ScopeId).CopyTo(row.Address, 24);
320-
}
321-
322-
if (GetIpNetEntry2(ref row) != 0)
323-
return null;
324-
325-
var macLen = (int)row.PhysicalAddressLength;
326-
if (macLen is <= 0 or > IF_MAX_PHYS_ADDRESS_LENGTH)
327-
return null;
328-
329-
var macBytes = new byte[macLen];
330-
Buffer.BlockCopy(row.PhysicalAddress, 0, macBytes, 0, macLen);
331-
332-
return new PhysicalAddress(macBytes).ToString();
295+
var entry = GetTable().FirstOrDefault(x => x.IPAddress.Equals(ipAddress));
296+
return entry?.MACAddress.ToString();
333297
}
334298

335299
/// <summary>

Source/NETworkManager/ViewModels/NeighborTableViewModel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,10 @@ private async void AutoRefreshTimer_Tick(object sender, EventArgs e)
489489
{
490490
_autoRefreshTimer.Stop();
491491

492-
await Refresh();
492+
// Skip refresh while a modify operation (add/delete) is in progress to avoid
493+
// clearing the table while the user is interacting with it.
494+
if (!IsModifying)
495+
await Refresh();
493496

494497
_autoRefreshTimer.Start();
495498
}

Website/docs/application/neighbor-table.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Both protocols are susceptible to spoofing/poisoning attacks that can manipulate
1818

1919
:::
2020

21-
::::::note
21+
:::note
2222

2323
Adding and deleting neighbor entries requires administrator privileges. If the application is not running as administrator, the view is in read-only mode. Use the **Restart as administrator** button to relaunch the application with elevated rights.
2424

Website/docs/changelog/next-release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Release date: **xx.xx.2025**
2727

2828
## Breaking Changes
2929

30-
- **ARP Table** has been renamed to **[Neighbor Table](../application/neighbor-table.md)**. Existing settings are automatically migrated on first launch after the update. [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
30+
- **ARP Table** has been renamed to **[Neighbor Table](../application/neighbor-table.md)**. The application list entry is automatically migrated on first launch. Other view settings (auto-refresh interval, export file type/path) reset to their defaults. [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
3131
- **IP Scanner** export: The `ARPMACAddress` and `ARPVendor` columns have been removed from CSV, XML and JSON exports. Use `MACAddress` and `Vendor` instead, which contain the same value (ARP/NDP preferred, NetBIOS as fallback). [#3403](https://github.com/BornToBeRoot/NETworkManager/pull/3403)
3232

3333
## What's new?

0 commit comments

Comments
 (0)