@@ -168,54 +168,77 @@ public static async Task<List<FirewallRule>> GetRulesAsync()
168168 }
169169
170170 /// <summary>
171- /// Enables or disables the given <paramref name="rule"/> by running
172- /// <c>Enable-NetFirewallRule</c> or <c>Disable-NetFirewallRule</c> against
173- /// the rule's internal <see cref="FirewallRule.Id"/>.
171+ /// Enables or disables the given <paramref name="rules"/> by running
172+ /// <c>Enable-NetFirewallRule</c> or <c>Disable-NetFirewallRule</c> against the rules'
173+ /// internal <see cref="FirewallRule.Id"/>. The cmdlet is run for all rules (also for rules that already
174+ /// seem to be in the target state, since the state may have changed outside the application).
175+ /// Nothing is changed if one of the rules no longer exists.
174176 /// </summary>
175- /// <param name="rule ">
176- /// The firewall rule to modify.
177+ /// <param name="rules ">
178+ /// The firewall rules to modify.
177179 /// </param>
178180 /// <param name="enabled">
179- /// <see langword="true"/> to enable the rule ; <see langword="false"/> to disable it .
181+ /// <see langword="true"/> to enable the rules ; <see langword="false"/> to disable them .
180182 /// </param>
181183 /// <exception cref="Exception">
182- /// Thrown when the PowerShell pipeline reports one or more errors.
184+ /// Thrown when a rule no longer exists or the PowerShell pipeline reports one or more errors.
183185 /// </exception>
184- public static async Task SetRuleEnabledAsync ( FirewallRule rule , bool enabled )
186+ public static Task SetRulesEnabledAsync ( IEnumerable < FirewallRule > rules , bool enabled )
185187 {
186- await RunspaceLock . WaitAsync ( ) ;
187- try
188- {
189- await Task . Run ( ( ) =>
190- {
191- using var ps = SMA . PowerShell . Create ( ) ;
192- ps . Runspace = SharedRunspace ;
193-
194- ps . AddScript ( $@ "{ ( enabled ? "Enable" : "Disable" ) } -NetFirewallRule -Name '{ rule . Id } '") ;
195- ps . Invoke ( ) ;
196-
197- if ( ps . Streams . Error . Count > 0 )
198- throw new Exception ( string . Join ( "; " , ps . Streams . Error ) ) ;
199- } ) ;
200- }
201- finally
202- {
203- RunspaceLock . Release ( ) ;
204- }
188+ return InvokeRuleCommandAsync ( enabled ? "Enable-NetFirewallRule" : "Disable-NetFirewallRule" ,
189+ rules . Select ( rule => rule . Id ) . ToList ( ) ) ;
205190 }
206191
207192 /// <summary>
208- /// Permanently removes the given <paramref name="rule"/> by running
209- /// <c>Remove-NetFirewallRule</c> against the rule's internal <see cref="FirewallRule.Id"/>.
193+ /// Permanently removes the given <paramref name="rule"/>.
210194 /// </summary>
211195 /// <param name="rule">
212196 /// The firewall rule to delete.
213197 /// </param>
214198 /// <exception cref="Exception">
215- /// Thrown when the PowerShell pipeline reports one or more errors.
199+ /// Thrown when the rule no longer exists or the PowerShell pipeline reports one or more errors.
200+ /// </exception>
201+ public static Task DeleteRuleAsync ( FirewallRule rule )
202+ {
203+ return DeleteRulesAsync ( [ rule ] ) ;
204+ }
205+
206+ /// <summary>
207+ /// Permanently removes the given <paramref name="rules"/> by running
208+ /// <c>Remove-NetFirewallRule</c> against the rules' internal <see cref="FirewallRule.Id"/>.
209+ /// Nothing is deleted if one of the rules no longer exists.
210+ /// </summary>
211+ /// <param name="rules">
212+ /// The firewall rules to delete.
213+ /// </param>
214+ /// <exception cref="Exception">
215+ /// Thrown when a rule no longer exists or the PowerShell pipeline reports one or more errors.
216+ /// </exception>
217+ public static Task DeleteRulesAsync ( IEnumerable < FirewallRule > rules )
218+ {
219+ return InvokeRuleCommandAsync ( "Remove-NetFirewallRule" , rules . Select ( rule => rule . Id ) . ToList ( ) ) ;
220+ }
221+
222+ /// <summary>
223+ /// Runs the given firewall cmdlet (e.g. <c>Enable-NetFirewallRule</c>) for all rule IDs in a
224+ /// single PowerShell call. It is checked first that all rules still exist, otherwise nothing is changed.
225+ /// This is not a transaction: if the cmdlet fails for a single rule (e.g. access denied), the other
226+ /// rules may already be changed.
227+ /// </summary>
228+ /// <param name="command">
229+ /// The cmdlet to run. Must accept the rule IDs via <c>-Name</c>.
230+ /// </param>
231+ /// <param name="ids">
232+ /// The internal IDs of the rules. Nothing is done if the list is empty.
233+ /// </param>
234+ /// <exception cref="Exception">
235+ /// Thrown when a rule no longer exists or the PowerShell pipeline reports one or more errors.
216236 /// </exception>
217- public static async Task DeleteRuleAsync ( FirewallRule rule )
237+ private static async Task InvokeRuleCommandAsync ( string command , IReadOnlyCollection < string > ids )
218238 {
239+ if ( ids . Count == 0 )
240+ return ;
241+
219242 await RunspaceLock . WaitAsync ( ) ;
220243 try
221244 {
@@ -224,7 +247,11 @@ await Task.Run(() =>
224247 using var ps = SMA . PowerShell . Create ( ) ;
225248 ps . Runspace = SharedRunspace ;
226249
227- ps . AddScript ( $@ "Remove-NetFirewallRule -Name '{ rule . Id } '") ;
250+ // Check that all rules still exist before changing anything
251+ ps . AddScript ( $@ "
252+ $ids = { ToPsArray ( ids ) }
253+ Get-NetFirewallRule -Name $ids -ErrorAction Stop | Out-Null
254+ { command } -Name $ids") ;
228255 ps . Invoke ( ) ;
229256
230257 if ( ps . Streams . Error . Count > 0 )
0 commit comments