Skip to content

Instantly share code, notes, and snippets.

@JustinGrote
Last active February 14, 2023 18:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JustinGrote/ec85e3ca276b12911eaedd381fd2c5ed to your computer and use it in GitHub Desktop.
Save JustinGrote/ec85e3ca276b12911eaedd381fd2c5ed to your computer and use it in GitHub Desktop.
Async Pinger Powershell Cmdlet in C#
<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>
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);
}
}
@JustinGrote
Copy link
Author

JustinGrote commented Feb 13, 2023

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment