Last active
February 14, 2023 18:10
-
-
Save JustinGrote/ec85e3ca276b12911eaedd381fd2c5ed to your computer and use it in GitHub Desktop.
Async Pinger Powershell Cmdlet in C#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>net7.0</TargetFramework> | |
<ImplicitUsings>enable</ImplicitUsings> | |
<Nullable>enable</Nullable> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="System.Management.Automation" Version="7.3.2" PrivateAssets="all" /> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace TestICMP; | |
using System.Diagnostics; // For Debug.Assert | |
using System.Management.Automation; // Windows PowerShell assembly. | |
using System.Net; | |
using System.Net.NetworkInformation; // For Ping | |
// Used to store the result of a ping | |
record PingResult(string Hostname, IPAddress IP, string ResolvedName, long RoundtripTime, IPStatus Status, string Detail); | |
// Used to store the metadata of a test, because we can't store this in the task itself | |
record PingMetadata(string Hostname, Ping PingObject, int Timeout); | |
[Cmdlet(VerbsDiagnostic.Test, "IcmpConnection")] | |
public class TestIcmpConnection : Cmdlet, IDisposable | |
{ | |
// What IP address or DNS name to ping | |
[Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] | |
[ValidateNotNullOrEmpty] | |
public string? Hostname; | |
// How long in seconds to wait for a response | |
[Parameter(Position = 1)] | |
public int Timeout = 10; | |
// Used to store a relation of the address to the task for output purposes. Dictionary.Values is not stable so we cant use it with WhenAll | |
readonly Dictionary<Task<PingReply>, PingMetadata> pingTasks = new(); | |
protected override void ProcessRecord() | |
{ | |
// ValidateNotNullOrEmpty should have caught this, this is just to make the compiler happy when using nullable | |
Debug.Assert(Hostname is not null); | |
try | |
{ | |
using Ping ping = new(); | |
Task<PingReply> pingTask = ping.SendPingAsync(Hostname, Timeout * 1000); | |
pingTasks.Add(pingTask, new PingMetadata(Hostname, ping, Timeout)); | |
} | |
catch (Exception err) | |
{ | |
WriteError(new ErrorRecord(err, "PingError", ErrorCategory.NotSpecified, null)); | |
} | |
} | |
protected override void EndProcessing() | |
{ | |
while (pingTasks.Count > 0) | |
{ | |
// Wait for any ping task to complete. WhenAny itself is async that must be "awaited". | |
// This is generally where the code will "pause" while waiting for ping replies to come back | |
Task<PingReply> completedTask = Task.WhenAny(pingTasks.Keys).GetAwaiter().GetResult(); | |
// Get the address for the completed task | |
var Hostname = pingTasks[completedTask].Hostname; | |
try | |
{ | |
// GetResult will unwrap the AggregateException, making it easier to catch stuff | |
PingReply taskResult = completedTask.GetAwaiter().GetResult(); | |
string hostname = Dns.GetHostEntry(Hostname).HostName; | |
// Write the result | |
WriteObject( | |
new PingResult( | |
Hostname, | |
taskResult.Address, | |
hostname, | |
taskResult.RoundtripTime, | |
taskResult.Status, | |
string.Empty | |
) | |
); | |
} | |
catch (Exception err) | |
{ | |
string message = err.InnerException?.Message ?? err.Message; | |
WriteObject( | |
new PingResult(Hostname, IPAddress.None, string.Empty, 0, IPStatus.Unknown, message) | |
); | |
} | |
finally | |
{ | |
pingTasks.Remove(completedTask); | |
} | |
} | |
} | |
protected override void StopProcessing() | |
{ | |
Dispose(); | |
} | |
public void Dispose() | |
{ | |
foreach (PingMetadata metadata in pingTasks.Values) | |
{ | |
metadata.PingObject.SendAsyncCancel(); | |
} | |
GC.SuppressFinalize(this); | |
} | |
} |
Author
JustinGrote
commented
Feb 13, 2023
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment