1- // Contains code from: https://stackoverflow.com/a/1148861/4986782
1+ // Contains code from: https://stackoverflow.com/a/1148861/4986782
22// Modified by BornToBeRoot
33
44using System ;
55using System . Collections . Generic ;
66using System . ComponentModel ;
77using System . Linq ;
8+ using System . Management . Automation . Runspaces ;
89using System . Net ;
910using System . Net . NetworkInformation ;
1011using System . Runtime . InteropServices ;
12+ using System . Threading ;
1113using System . Threading . Tasks ;
1214using NETworkManager . Utilities ;
15+ using SMA = System . Management . Automation ;
16+ using log4net ;
1317
1418namespace NETworkManager . Models . Network ;
1519
20+ /// <summary>
21+ /// Provides static methods to read and modify the Windows ARP table.
22+ /// Read access uses the <c>IpHlpApi</c> Win32 API. Modifying operations
23+ /// (add/delete entries, clear table) run via PowerShell in a shared
24+ /// <see cref="Runspace"/> that is initialized once with the required
25+ /// execution policy. A <see cref="SemaphoreSlim"/> serializes access so
26+ /// the runspace is never used concurrently. Modifying operations require
27+ /// the application to run with elevated rights.
28+ /// </summary>
1629public class ARP
1730{
1831 #region Variables
1932
33+ /// <summary>
34+ /// The logger for this class.
35+ /// </summary>
36+ private static readonly ILog Log = LogManager . GetLogger ( typeof ( ARP ) ) ;
37+
2038 // The max number of physical addresses.
2139 private const int MAXLEN_PHYSADDR = 8 ;
2240
@@ -50,15 +68,29 @@ private static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(Unmanaged
5068 // The insufficient buffer error.
5169 private const int ERROR_INSUFFICIENT_BUFFER = 122 ;
5270
53- #endregion
54-
55- #region Events
56-
57- public event EventHandler UserHasCanceled ;
58-
59- protected virtual void OnUserHasCanceled ( )
71+ /// <summary>
72+ /// Ensures that only one PowerShell pipeline runs on <see cref="SharedRunspace"/> at a time.
73+ /// </summary>
74+ private static readonly SemaphoreSlim Lock = new ( 1 , 1 ) ;
75+
76+ /// <summary>
77+ /// Shared PowerShell runspace, initialized once in the static constructor with
78+ /// <c>Set-ExecutionPolicy Bypass</c> so subsequent operations can run without
79+ /// repeating the policy change.
80+ /// </summary>
81+ private static readonly Runspace SharedRunspace ;
82+
83+ /// <summary>
84+ /// Opens <see cref="SharedRunspace"/> and runs the one-time initialization script.
85+ /// </summary>
86+ static ARP ( )
6087 {
61- UserHasCanceled ? . Invoke ( this , EventArgs . Empty ) ;
88+ SharedRunspace = RunspaceFactory . CreateRunspace ( ) ;
89+ SharedRunspace . Open ( ) ;
90+
91+ using var ps = SMA . PowerShell . Create ( ) ;
92+ ps . Runspace = SharedRunspace ;
93+ ps . AddScript ( "Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process" ) . Invoke ( ) ;
6294 }
6395
6496 #endregion
@@ -154,61 +186,92 @@ public static string GetMACAddress(IPAddress ipAddress)
154186 return arpInfo ? . MACAddress . ToString ( ) ;
155187 }
156188
157- private void RunPowerShellCommand ( string command )
189+ /// <summary>
190+ /// Adds a static ARP entry by running <c>arp -s</c> through the shared PowerShell
191+ /// runspace. Requires the application to run with elevated rights.
192+ /// </summary>
193+ /// <param name="ipAddress">The IP address of the entry.</param>
194+ /// <param name="macAddress">The MAC address of the entry, separated with <c>-</c>.</param>
195+ /// <exception cref="Exception">
196+ /// Thrown when the PowerShell pipeline reports one or more errors.
197+ /// </exception>
198+ public static async Task AddEntryAsync ( string ipAddress , string macAddress )
158199 {
159- try
160- {
161- PowerShellHelper . ExecuteCommand ( command , true ) ;
162- }
163- catch ( Win32Exception win32Ex )
164- {
165- switch ( win32Ex . NativeErrorCode )
166- {
167- case 1223 :
168- OnUserHasCanceled ( ) ;
169- break ;
170- default :
171- throw ;
172- }
173- }
200+ await InvokeAsync ( $ "arp -s '{ EscapePs ( ipAddress ) } ' '{ EscapePs ( macAddress ) } ' 2>&1 | Out-String") ;
174201 }
175202
176- // MAC separated with "-"
177- public Task AddEntryAsync ( string ipAddress , string macAddress )
203+ /// <summary>
204+ /// Removes a single ARP entry by running <c>arp -d</c> through the shared PowerShell
205+ /// runspace. Requires the application to run with elevated rights.
206+ /// </summary>
207+ /// <param name="ipAddress">The IP address of the entry to remove.</param>
208+ /// <exception cref="Exception">
209+ /// Thrown when the PowerShell pipeline reports one or more errors.
210+ /// </exception>
211+ public static async Task DeleteEntryAsync ( string ipAddress )
178212 {
179- return Task . Run ( ( ) => AddEntry ( ipAddress , macAddress ) ) ;
213+ await InvokeAsync ( $ "arp -d ' { EscapePs ( ipAddress ) } ' 2>&1 | Out-String" ) ;
180214 }
181215
182- private void AddEntry ( string ipAddress , string macAddress )
216+ /// <summary>
217+ /// Clears the entire ARP cache by running <c>netsh interface ip delete arpcache</c>
218+ /// through the shared PowerShell runspace. Requires the application to run with
219+ /// elevated rights.
220+ /// </summary>
221+ /// <exception cref="Exception">
222+ /// Thrown when the PowerShell pipeline reports one or more errors.
223+ /// </exception>
224+ public static async Task DeleteTableAsync ( )
183225 {
184- var command = $ "arp -s { ipAddress } { macAddress } ";
185-
186- RunPowerShellCommand ( command ) ;
226+ await InvokeAsync ( "netsh interface ip delete arpcache 2>&1 | Out-String" ) ;
187227 }
188228
189- public Task DeleteEntryAsync ( string ipAddress )
229+ /// <summary>
230+ /// Runs <paramref name="script"/> on the shared runspace and throws when the
231+ /// command exits with a non-zero exit code or writes to the PowerShell error stream.
232+ /// </summary>
233+ /// <param name="script">The PowerShell script to execute.</param>
234+ private static async Task InvokeAsync ( string script )
190235 {
191- return Task . Run ( ( ) => DeleteEntry ( ipAddress ) ) ;
192- }
193-
194- private void DeleteEntry ( string ipAddress )
195- {
196- var command = $ "arp -d { ipAddress } ";
197-
198- RunPowerShellCommand ( command ) ;
199- }
200-
201- public Task DeleteTableAsync ( )
202- {
203- return Task . Run ( ( ) => DeleteTable ( ) ) ;
236+ await Lock . WaitAsync ( ) ;
237+ try
238+ {
239+ await Task . Run ( ( ) =>
240+ {
241+ using var ps = SMA . PowerShell . Create ( ) ;
242+ ps . Runspace = SharedRunspace ;
243+
244+ ps . AddScript ( script + @"
245+ if ($LASTEXITCODE -ne 0) { Write-Error ""Exit code: $LASTEXITCODE"" }" ) ;
246+ var results = ps . Invoke ( ) ;
247+
248+ if ( ps . Streams . Error . Count > 0 )
249+ {
250+ var output = string . Join ( Environment . NewLine ,
251+ results . Select ( r => r ? . ToString ( ) ) . Where ( s => ! string . IsNullOrWhiteSpace ( s ) ) ) ;
252+ var errors = string . Join ( Environment . NewLine , ps . Streams . Error ) ;
253+
254+ var message = string . IsNullOrWhiteSpace ( output )
255+ ? errors
256+ : $ "{ output . Trim ( ) } { Environment . NewLine } { errors } ";
257+
258+ Log . Warn ( $ "PowerShell error: { message } ") ;
259+ throw new Exception ( message ) ;
260+ }
261+ } ) ;
262+ }
263+ finally
264+ {
265+ Lock . Release ( ) ;
266+ }
204267 }
205268
206- private void DeleteTable ( )
207- {
208- const string command = "netsh interface ip delete arpcache" ;
209-
210- RunPowerShellCommand ( command ) ;
211- }
269+ /// <summary>
270+ /// Escapes a string for embedding inside a PowerShell single-quoted string by
271+ /// doubling any single-quote characters.
272+ /// </summary>
273+ /// <param name="value">The raw string value to escape.</param>
274+ private static string EscapePs ( string value ) => value . Replace ( "'" , "''" ) ;
212275
213276 #endregion
214277}
0 commit comments