diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs
index 739fbbbfc94..64af1553948 100644
--- a/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs
+++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs
@@ -1,8 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+#nullable enable
+
using System;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Net;
@@ -18,30 +22,64 @@ namespace Microsoft.PowerShell.Commands
///
[Cmdlet(VerbsDiagnostic.Test, "Connection", DefaultParameterSetName = DefaultPingParameterSet,
HelpUri = "https://go.microsoft.com/fwlink/?LinkID=135266")]
- [OutputType(typeof(PingReport), ParameterSetName = new string[] { DefaultPingParameterSet })]
- [OutputType(typeof(PingReply), ParameterSetName = new string[] { RepeatPingParameterSet, MtuSizeDetectParameterSet })]
+ [OutputType(typeof(PingStatus), ParameterSetName = new string[] { DefaultPingParameterSet })]
+ [OutputType(typeof(PingStatus), ParameterSetName = new string[] { RepeatPingParameterSet, MtuSizeDetectParameterSet })]
[OutputType(typeof(bool), ParameterSetName = new string[] { DefaultPingParameterSet, RepeatPingParameterSet, TcpPortParameterSet })]
+ [OutputType(typeof(PingMtuStatus), ParameterSetName = new string[] { MtuSizeDetectParameterSet })]
[OutputType(typeof(int), ParameterSetName = new string[] { MtuSizeDetectParameterSet })]
- [OutputType(typeof(TraceRouteReply), ParameterSetName = new string[] { TraceRouteParameterSet })]
+ [OutputType(typeof(TraceStatus), ParameterSetName = new string[] { TraceRouteParameterSet })]
public class TestConnectionCommand : PSCmdlet, IDisposable
{
+ #region Parameter Set Names
private const string DefaultPingParameterSet = "DefaultPing";
private const string RepeatPingParameterSet = "RepeatPing";
private const string TraceRouteParameterSet = "TraceRoute";
private const string TcpPortParameterSet = "TcpPort";
private const string MtuSizeDetectParameterSet = "MtuSizeDetect";
+ #endregion
+
+ #region Cmdlet Defaults
+
+ // Count of pings sent to each trace route hop. Default mimics Windows' defaults.
+ // If this value changes, we need to update 'ConsoleTraceRouteReply' resource string.
+ private const int DefaultTraceRoutePingCount = 3;
+
+ // Default size for the send buffer.
+ private const int DefaultSendBufferSize = 32;
+
+ private const int DefaultMaxHops = 128;
+
+ private const string TestConnectionExceptionId = "TestConnectionException";
+
+ #endregion
+
+ #region Private Fields
+
+ private static byte[]? s_DefaultSendBuffer;
+
+ private bool _disposed;
+
+ private Ping? _sender;
+
+ private readonly ManualResetEventSlim _pingComplete = new ManualResetEventSlim();
+
+ private PingCompletedEventArgs? _pingCompleteArgs;
+
+ #endregion
+
#region Parameters
///
- /// Do ping test.
+ /// Gets or sets whether to do ping test.
+ /// Default is true.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
[Parameter(ParameterSetName = RepeatPingParameterSet)]
public SwitchParameter Ping { get; set; } = true;
///
- /// Force using IPv4 protocol.
+ /// Gets or sets whether to force use of IPv4 protocol.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
[Parameter(ParameterSetName = RepeatPingParameterSet)]
@@ -51,7 +89,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public SwitchParameter IPv4 { get; set; }
///
- /// Force using IPv6 protocol.
+ /// Gets or sets whether to force use of IPv6 protocol.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
[Parameter(ParameterSetName = RepeatPingParameterSet)]
@@ -61,7 +99,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public SwitchParameter IPv6 { get; set; }
///
- /// Do reverse DNS lookup to get names for IP addresses.
+ /// Gets or sets whether to do reverse DNS lookup to get names for IP addresses.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
[Parameter(ParameterSetName = RepeatPingParameterSet)]
@@ -71,8 +109,8 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public SwitchParameter ResolveDestination { get; set; }
///
- /// Source from which to do a test (ping, trace route, ...).
- /// The default is Local Host.
+ /// Gets the source from which to run the selected test.
+ /// The default is localhost.
/// Remoting is not yet implemented internally in the cmdlet.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
@@ -82,22 +120,20 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public string Source { get; } = Dns.GetHostName();
///
- /// The number of times the Ping data packets can be forwarded by routers.
- /// As gateways and routers transmit packets through a network,
- /// they decrement the Time-to-Live (TTL) value found in the packet header.
+ /// Gets or sets the number of times the Ping data packets can be forwarded by routers.
+ /// As gateways and routers transmit packets through a network, they decrement the Time-to-Live (TTL)
+ /// value found in the packet header.
/// The default (from Windows) is 128 hops.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
[Parameter(ParameterSetName = RepeatPingParameterSet)]
[Parameter(ParameterSetName = TraceRouteParameterSet)]
- [ValidateRange(0, sMaxHops)]
+ [ValidateRange(1, DefaultMaxHops)]
[Alias("Ttl", "TimeToLive", "Hops")]
- public int MaxHops { get; set; } = sMaxHops;
-
- private const int sMaxHops = 128;
+ public int MaxHops { get; set; } = DefaultMaxHops;
///
- /// Count of attempts.
+ /// Gets or sets the number of ping attempts.
/// The default (from Windows) is 4 times.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
@@ -105,7 +141,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public int Count { get; set; } = 4;
///
- /// Delay between attempts.
+ /// Gets or sets the delay between ping attempts.
/// The default (from Windows) is 1 second.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
@@ -114,9 +150,9 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public int Delay { get; set; } = 1;
///
- /// Buffer size to send.
- /// The default (from Windows) is 32 bites.
- /// Max value is 65500 (limit from Windows API).
+ /// Gets or sets the buffer size to send with the ping packet.
+ /// The default (from Windows) is 32 bytes.
+ /// Max value is 65500 (limitation imposed by Windows API).
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
[Parameter(ParameterSetName = RepeatPingParameterSet)]
@@ -125,7 +161,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public int BufferSize { get; set; } = DefaultSendBufferSize;
///
- /// Don't fragment ICMP packages.
+ /// Gets or sets whether to prevent fragmentation of the ICMP packets.
/// Currently CoreFX not supports this on Unix.
///
[Parameter(ParameterSetName = DefaultPingParameterSet)]
@@ -133,31 +169,31 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
public SwitchParameter DontFragment { get; set; }
///
- /// Continue ping until user press Ctrl-C
- /// or Int.MaxValue threshold reached.
+ /// Gets or sets whether to continue pinging until user presses Ctrl-C (or Int.MaxValue threshold reached).
///
- [Parameter(ParameterSetName = RepeatPingParameterSet)]
- public SwitchParameter Continues { get; set; }
+ [Parameter(Mandatory = true, ParameterSetName = RepeatPingParameterSet)]
+ [Alias("Continuous")]
+ public SwitchParameter Repeat { get; set; }
///
- /// Set short output kind ('bool' for Ping, 'int' for MTU size ...).
- /// Default is to return typed result object(s).
+ /// Gets or sets whether to enable quiet output mode, reducing output to a single simple value only.
+ /// By default, PingStatus, PingMtuStatus, or TraceStatus objects are emitted.
+ /// With this switch, standard ping and -Traceroute returns only true / false, and -MtuSize returns an integer.
///
[Parameter]
public SwitchParameter Quiet;
///
- /// Time-out value in seconds.
+ /// Gets or sets the timeout value for an individual ping in seconds.
/// If a response is not received in this time, no response is assumed.
- /// It is not the cmdlet timeout! It is a timeout for waiting one ping response.
- /// The default (from Windows) is 5 second.
+ /// The default (from Windows) is 5 seconds.
///
[Parameter]
[ValidateRange(ValidateRangeKind.Positive)]
public int TimeoutSeconds { get; set; } = 5;
///
- /// Destination - computer name or IP address.
+ /// Gets or sets the destination hostname or IP address.
///
[Parameter(
Mandatory = true,
@@ -166,36 +202,37 @@ public class TestConnectionCommand : PSCmdlet, IDisposable
ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("ComputerName")]
- public string[] TargetName { get; set; }
+ public string[]? TargetName { get; set; }
///
- /// Detect MTU size.
+ /// Gets or sets whether to detect Maximum Transmission Unit size.
+ /// When selected, only a single ping result is returned, indicating the maximum buffer size
+ /// the route to the destination can support without fragmenting the ICMP packets.
///
[Parameter(Mandatory = true, ParameterSetName = MtuSizeDetectParameterSet)]
- public SwitchParameter MTUSizeDetect { get; set; }
+ [Alias("MtuSizeDetect")]
+ public SwitchParameter MtuSize { get; set; }
///
- /// Do traceroute test.
+ /// Gets or sets whether to perform a traceroute test.
///
[Parameter(Mandatory = true, ParameterSetName = TraceRouteParameterSet)]
public SwitchParameter Traceroute { get; set; }
///
- /// Do tcp connection test.
+ /// Gets or sets whether to perform a TCP connection test.
///
[ValidateRange(0, 65535)]
[Parameter(Mandatory = true, ParameterSetName = TcpPortParameterSet)]
- public int TCPPort { get; set; }
+ public int TcpPort { get; set; }
#endregion Parameters
///
- /// Init the cmdlet.
+ /// BeginProcessing implementation for TestConnectionCommand.
///
protected override void BeginProcessing()
{
- base.BeginProcessing();
-
switch (ParameterSetName)
{
case RepeatPingParameterSet:
@@ -209,6 +246,11 @@ protected override void BeginProcessing()
///
protected override void ProcessRecord()
{
+ if (TargetName == null)
+ {
+ return;
+ }
+
foreach (var targetName in TargetName)
{
switch (ParameterSetName)
@@ -230,13 +272,20 @@ protected override void ProcessRecord()
}
}
+ ///
+ /// On receiving the StopProcessing() request, the cmdlet will immediately cancel any in-progress ping request.
+ /// This allows a cancellation to occur during a ping request without having to wait for the timeout.
+ ///
+ protected override void StopProcessing()
+ {
+ _sender?.SendAsyncCancel();
+ }
+
#region ConnectionTest
private void ProcessConnectionByTCPPort(string targetNameOrAddress)
{
- string resolvedTargetName;
- IPAddress targetAddress;
- if (!InitProcessPing(targetNameOrAddress, out resolvedTargetName, out targetAddress))
+ if (!InitProcessPing(targetNameOrAddress, out _, out IPAddress? targetAddress))
{
return;
}
@@ -245,7 +294,7 @@ private void ProcessConnectionByTCPPort(string targetNameOrAddress)
try
{
- Task connectionTask = client.ConnectAsync(targetAddress, TCPPort);
+ Task connectionTask = client.ConnectAsync(targetAddress, TcpPort);
string targetString = targetAddress.ToString();
for (var i = 1; i <= TimeoutSeconds; i++)
@@ -278,43 +327,101 @@ private void ProcessConnectionByTCPPort(string targetNameOrAddress)
WriteObject(false);
}
+
#endregion ConnectionTest
#region TracerouteTest
+
private void ProcessTraceroute(string targetNameOrAddress)
{
byte[] buffer = GetSendBuffer(BufferSize);
- string resolvedTargetName;
- IPAddress targetAddress;
- if (!InitProcessPing(targetNameOrAddress, out resolvedTargetName, out targetAddress))
+ if (!InitProcessPing(targetNameOrAddress, out string resolvedTargetName, out IPAddress? targetAddress))
{
return;
}
- TraceRouteResult traceRouteResult = new TraceRouteResult(Source, targetAddress, resolvedTargetName);
-
int currentHop = 1;
PingOptions pingOptions = new PingOptions(currentHop, DontFragment.IsPresent);
- PingReply reply = null;
+ PingReply reply;
+ PingReply discoveryReply;
int timeout = TimeoutSeconds * 1000;
+ Stopwatch timer = new Stopwatch();
+ IPAddress hopAddress;
do
{
- TraceRouteReply traceRouteReply = new TraceRouteReply();
-
- pingOptions.Ttl = traceRouteReply.Hop = currentHop;
- currentHop++;
-
- // In the specific case we don't use 'Count' property.
+ // Clear the stored router name for every hop
+ string routerName = string.Empty;
+ pingOptions.Ttl = currentHop;
+
+#if !UNIX
+ // Get intermediate hop target. This needs to be done first, so that we can target it properly
+ // and get useful responses.
+ var discoveryAttempts = 0;
+ bool addressIsValid = false;
+ do
+ {
+ discoveryReply = SendCancellablePing(targetAddress, timeout, buffer, pingOptions);
+ discoveryAttempts++;
+ addressIsValid = !(discoveryReply.Address.Equals(IPAddress.Any)
+ || discoveryReply.Address.Equals(IPAddress.IPv6Any));
+ }
+ while (discoveryAttempts <= DefaultTraceRoutePingCount && addressIsValid);
+
+ // If we aren't able to get a valid address, just re-target the final destination of the trace.
+ hopAddress = addressIsValid ? discoveryReply.Address : targetAddress;
+#else
+ // Unix Ping API returns nonsense "TimedOut" for ALL intermediate hops. No way around this
+ // issue for traceroutes as we rely on information (intermediate addresses, etc.) that is
+ // simply not returned to us by the API.
+ // The only supported states on Unix seem to be Success and TimedOut. Workaround is to
+ // keep targeting the final address; at the very least we will be able to tell the user
+ // the required number of hops to reach the destination.
+ hopAddress = targetAddress;
+ discoveryReply = SendCancellablePing(targetAddress, timeout, buffer, pingOptions);
+#endif
+ var hopAddressString = discoveryReply.Address.ToString();
+
+ // In traceroutes we don't use 'Count' parameter.
// If we change 'DefaultTraceRoutePingCount' we should change 'ConsoleTraceRouteReply' resource string.
- for (int i = 1; i <= DefaultTraceRoutePingCount; i++)
+ for (uint i = 1; i <= DefaultTraceRoutePingCount; i++)
{
try
{
- reply = _sender.Send(targetAddress, timeout, buffer, pingOptions);
+#if !UNIX
+ if (ResolveDestination.IsPresent && routerName == string.Empty)
+ {
+ try
+ {
+ InitProcessPing(hopAddressString, out routerName, out _);
+ }
+ catch
+ {
+ // Swallow host resolve exceptions and just use the IP address.
+ }
+ }
+#endif
+ reply = SendCancellablePing(hopAddress, timeout, buffer, pingOptions, timer);
- traceRouteReply.PingReplies.Add(reply);
+ if (!Quiet.IsPresent)
+ {
+ var status = new PingStatus(
+ Source,
+ routerName,
+ reply,
+ reply.Status == IPStatus.Success
+ ? reply.RoundtripTime
+ : timer.ElapsedMilliseconds,
+ buffer.Length,
+ pingNum: i);
+ WriteObject(new TraceStatus(
+ currentHop,
+ status,
+ Source,
+ resolvedTargetName,
+ targetAddress));
+ }
}
catch (PingException ex)
{
@@ -332,98 +439,34 @@ private void ProcessTraceroute(string targetNameOrAddress)
continue;
}
- catch
- {
- // Ignore host resolve exceptions.
- }
// We use short delay because it is impossible DoS with trace route.
- Thread.Sleep(200);
- }
-
- if (ResolveDestination && reply.Status == IPStatus.Success)
- {
- traceRouteReply.ReplyRouterName = Dns.GetHostEntry(reply.Address).HostName;
+ Thread.Sleep(50);
+ timer.Reset();
}
- traceRouteReply.ReplyRouterAddress = reply.Address;
- traceRouteResult.Replies.Add(traceRouteReply);
- } while (reply != null
- && currentHop <= sMaxHops
- && (reply.Status == IPStatus.TtlExpired || reply.Status == IPStatus.TimedOut));
+ currentHop++;
+ } while (currentHop <= MaxHops
+ && (discoveryReply.Status == IPStatus.TtlExpired
+ || discoveryReply.Status == IPStatus.TimedOut));
if (Quiet.IsPresent)
{
- WriteObject(currentHop <= sMaxHops);
- }
- else
- {
- WriteObject(traceRouteResult);
+ WriteObject(currentHop <= MaxHops);
}
- }
-
- ///
- /// The class contains an information about a trace route attempt.
- ///
- public class TraceRouteReply
- {
- internal TraceRouteReply()
- {
- PingReplies = new List(DefaultTraceRoutePingCount);
- }
-
- ///
- /// Number of current hop (router).
- ///
- public int Hop;
-
- ///
- /// List of ping replies for current hop (router).
- ///
- public List PingReplies;
-
- ///
- /// Router IP address.
- ///
- public IPAddress ReplyRouterAddress;
-
- ///
- /// Resolved router name.
- ///
- public string ReplyRouterName;
- }
-
- ///
- /// The class contains an information about the source, the destination and trace route results.
- ///
- public class TraceRouteResult
- {
- internal TraceRouteResult(string source, IPAddress destinationAddress, string destinationHost)
+ else if (currentHop > MaxHops)
{
- Source = source;
- DestinationAddress = destinationAddress;
- DestinationHost = destinationHost;
- Replies = new List();
+ var message = StringUtil.Format(
+ TestConnectionResources.MaxHopsExceeded,
+ resolvedTargetName,
+ MaxHops);
+ var pingException = new PingException(message);
+ WriteError(new ErrorRecord(
+ pingException,
+ TestConnectionExceptionId,
+ ErrorCategory.ConnectionError,
+ targetAddress));
}
-
- ///
- /// Source from which to trace route.
- ///
- public string Source { get; }
-
- ///
- /// Destination to which to trace route.
- ///
- public IPAddress DestinationAddress { get; }
-
- ///
- /// Destination to which to trace route.
- ///
- public string DestinationHost { get; }
-
- ///
- ///
- public List Replies { get; }
}
#endregion TracerouteTest
@@ -431,15 +474,13 @@ internal TraceRouteResult(string source, IPAddress destinationAddress, string de
#region MTUSizeTest
private void ProcessMTUSize(string targetNameOrAddress)
{
- PingReply reply, replyResult = null;
- string resolvedTargetName;
- IPAddress targetAddress;
- if (!InitProcessPing(targetNameOrAddress, out resolvedTargetName, out targetAddress))
+ PingReply? reply, replyResult = null;
+ if (!InitProcessPing(targetNameOrAddress, out string resolvedTargetName, out IPAddress? targetAddress))
{
return;
}
- // Cautious! Algorithm is sensitive to changing boundary values.
+ // Caution! Algorithm is sensitive to changing boundary values.
int HighMTUSize = 10000;
int CurrentMTUSize = 1473;
int LowMTUSize = targetAddress.AddressFamily == AddressFamily.InterNetworkV6 ? 1280 : 68;
@@ -460,10 +501,9 @@ private void ProcessMTUSize(string targetNameOrAddress)
CurrentMTUSize,
HighMTUSize));
- reply = _sender.Send(targetAddress, timeout, buffer, pingOptions);
+ reply = SendCancellablePing(targetAddress, timeout, buffer, pingOptions);
- // Cautious! Algorithm is sensitive to changing boundary values.
- if (reply.Status == IPStatus.PacketTooBig)
+ if (reply.Status == IPStatus.PacketTooBig || reply.Status == IPStatus.TimedOut)
{
HighMTUSize = CurrentMTUSize;
retry = 1;
@@ -476,7 +516,7 @@ private void ProcessMTUSize(string targetNameOrAddress)
}
else
{
- // Target host don't reply - try again up to 'Count'.
+ // If the host didn't reply, try again up to the 'Count' value.
if (retry >= Count)
{
string message = StringUtil.Format(
@@ -524,17 +564,11 @@ private void ProcessMTUSize(string targetNameOrAddress)
}
else
{
- var res = PSObject.AsPSObject(replyResult);
-
- PSMemberInfo sourceProperty = new PSNoteProperty("Source", Source);
- res.Members.Add(sourceProperty);
- PSMemberInfo destinationProperty = new PSNoteProperty("Destination", targetNameOrAddress);
- res.Members.Add(destinationProperty);
- PSMemberInfo mtuSizeProperty = new PSNoteProperty("MTUSize", CurrentMTUSize);
- res.Members.Add(mtuSizeProperty);
- res.TypeNames.Insert(0, "PingReply#MTUSize");
-
- WriteObject(res);
+ WriteObject(new PingMtuStatus(
+ Source,
+ resolvedTargetName,
+ replyResult ?? throw new ArgumentNullException(nameof(replyResult)),
+ CurrentMTUSize));
}
}
@@ -544,9 +578,7 @@ private void ProcessMTUSize(string targetNameOrAddress)
private void ProcessPing(string targetNameOrAddress)
{
- string resolvedTargetName;
- IPAddress targetAddress;
- if (!InitProcessPing(targetNameOrAddress, out resolvedTargetName, out targetAddress))
+ if (!InitProcessPing(targetNameOrAddress, out string resolvedTargetName, out IPAddress? targetAddress))
{
return;
}
@@ -556,15 +588,14 @@ private void ProcessPing(string targetNameOrAddress)
PingReply reply;
PingOptions pingOptions = new PingOptions(MaxHops, DontFragment.IsPresent);
- PingReport pingReport = new PingReport(Source, resolvedTargetName);
int timeout = TimeoutSeconds * 1000;
int delay = Delay * 1000;
- for (int i = 1; i <= Count; i++)
+ for (uint i = 1; i <= Count; i++)
{
try
{
- reply = _sender.Send(targetAddress, timeout, buffer, pingOptions);
+ reply = SendCancellablePing(targetAddress, timeout, buffer, pingOptions);
}
catch (PingException ex)
{
@@ -581,21 +612,23 @@ private void ProcessPing(string targetNameOrAddress)
continue;
}
- if (Continues.IsPresent)
- {
- WriteObject(reply);
- }
- else if (Quiet.IsPresent)
+ if (Quiet.IsPresent)
{
// Return 'true' only if all pings have completed successfully.
quietResult &= reply.Status == IPStatus.Success;
}
else
{
- pingReport.Replies.Add(reply);
+ WriteObject(new PingStatus(
+ Source,
+ resolvedTargetName,
+ reply,
+ reply.RoundtripTime,
+ buffer.Length,
+ pingNum: i));
}
- // Delay between ping but not after last ping.
+ // Delay between pings, but not after last ping.
if (i < Count && Delay > 0)
{
Thread.Sleep(delay);
@@ -606,54 +639,47 @@ private void ProcessPing(string targetNameOrAddress)
{
WriteObject(quietResult);
}
- else
- {
- WriteObject(pingReport);
- }
- }
-
- ///
- /// The class contains an information about the source, the destination and ping results.
- ///
- public class PingReport
- {
- internal PingReport(string source, string destination)
- {
- Source = source;
- Destination = destination;
- Replies = new List();
- }
-
- ///
- /// Source from which to ping.
- ///
- public string Source { get; }
-
- ///
- /// Destination to which to ping.
- ///
- public string Destination { get; }
-
- ///
- /// Ping results for every ping attempt.
- ///
- public List Replies { get; }
}
#endregion PingTest
- private bool InitProcessPing(string targetNameOrAddress, out string resolvedTargetName, out IPAddress targetAddress)
+ private bool InitProcessPing(
+ string targetNameOrAddress,
+ out string resolvedTargetName,
+ [NotNullWhen(true)]
+ out IPAddress? targetAddress)
{
resolvedTargetName = targetNameOrAddress;
IPHostEntry hostEntry;
if (IPAddress.TryParse(targetNameOrAddress, out targetAddress))
{
+ if ((IPv4 && targetAddress.AddressFamily != AddressFamily.InterNetwork)
+ || (IPv6 && targetAddress.AddressFamily != AddressFamily.InterNetworkV6))
+ {
+ string message = StringUtil.Format(
+ TestConnectionResources.NoPingResult,
+ resolvedTargetName,
+ TestConnectionResources.TargetAddressAbsent);
+ Exception pingException = new PingException(message, null);
+ ErrorRecord errorRecord = new ErrorRecord(
+ pingException,
+ TestConnectionExceptionId,
+ ErrorCategory.ResourceUnavailable,
+ resolvedTargetName);
+ WriteError(errorRecord);
+ return false;
+ }
+
if (ResolveDestination)
{
hostEntry = Dns.GetHostEntry(targetNameOrAddress);
resolvedTargetName = hostEntry.HostName;
}
+ else
+ {
+ resolvedTargetName = targetAddress.ToString();
+ }
}
else
{
@@ -685,16 +711,7 @@ private bool InitProcessPing(string targetNameOrAddress, out string resolvedTarg
if (IPv6 || IPv4)
{
- AddressFamily addressFamily = IPv6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork;
-
- foreach (var address in hostEntry.AddressList)
- {
- if (address.AddressFamily == addressFamily)
- {
- targetAddress = address;
- break;
- }
- }
+ targetAddress = GetHostAddress(hostEntry);
if (targetAddress == null)
{
@@ -721,8 +738,23 @@ private bool InitProcessPing(string targetNameOrAddress, out string resolvedTarg
return true;
}
+ private IPAddress? GetHostAddress(IPHostEntry hostEntry)
+ {
+ AddressFamily addressFamily = IPv6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork;
+
+ foreach (var address in hostEntry.AddressList)
+ {
+ if (address.AddressFamily == addressFamily)
+ {
+ return address;
+ }
+ }
+
+ return null;
+ }
+
// Users most often use the default buffer size so we cache the buffer.
- // Creates and filles a send buffer. This follows the ping.exe and CoreFX model.
+ // Creates and fills a send buffer. This follows the ping.exe and CoreFX model.
private byte[] GetSendBuffer(int bufferSize)
{
if (bufferSize == DefaultSendBufferSize && s_DefaultSendBuffer != null)
@@ -762,31 +794,280 @@ public void Dispose()
///
protected virtual void Dispose(bool disposing)
{
- if (!this._disposed)
+ if (!_disposed)
{
if (disposing)
{
- _sender.Dispose();
+ _sender?.Dispose();
+ _pingComplete?.Dispose();
}
_disposed = true;
}
}
- // Count of pings sent per each trace route hop.
- // Default = 3 (from Windows).
- // If we change 'DefaultTraceRoutePingCount' we should change 'ConsoleTraceRouteReply' resource string.
- private const int DefaultTraceRoutePingCount = 3;
+ // Uses the SendAsync() method to send pings, so that Ctrl+C can halt the request early if needed.
+ private PingReply SendCancellablePing(
+ IPAddress targetAddress,
+ int timeout,
+ byte[] buffer,
+ PingOptions pingOptions,
+ Stopwatch? timer = null)
+ {
+ try
+ {
+ _sender = new Ping();
+ _sender.PingCompleted += OnPingComplete;
- /// Create the default send buffer once and cache it.
- private const int DefaultSendBufferSize = 32;
- private static byte[] s_DefaultSendBuffer = null;
+ timer?.Start();
+ _sender.SendAsync(targetAddress, timeout, buffer, pingOptions, this);
+ _pingComplete.Wait();
+ timer?.Stop();
+ _pingComplete.Reset();
- private bool _disposed;
+ if (_pingCompleteArgs == null)
+ {
+ throw new PingException(string.Format(
+ TestConnectionResources.NoPingResult,
+ targetAddress,
+ IPStatus.Unknown));
+ }
- private readonly Ping _sender = new Ping();
+ if (_pingCompleteArgs.Cancelled)
+ {
+ // The only cancellation we have implemented is on pipeline stops via StopProcessing().
+ throw new PipelineStoppedException();
+ }
- private const string TestConnectionExceptionId = "TestConnectionException";
+ if (_pingCompleteArgs.Error != null)
+ {
+ throw new PingException(_pingCompleteArgs.Error.Message, _pingCompleteArgs.Error);
+ }
+
+ return _pingCompleteArgs.Reply;
+ }
+ finally
+ {
+ _sender?.Dispose();
+ _sender = null;
+ }
+ }
+
+ // This event is triggered when the ping is completed, and passes along the eventargs so that we know
+ // if the ping was cancelled, or an exception was thrown.
+ private static void OnPingComplete(object sender, PingCompletedEventArgs e)
+ {
+ ((TestConnectionCommand)e.UserState)._pingCompleteArgs = e;
+ ((TestConnectionCommand)e.UserState)._pingComplete.Set();
+ }
+
+ ///
+ /// The class contains information about the source, the destination and ping results.
+ ///
+ public class PingStatus
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// This constructor allows manually specifying the initial values for the cases where the PingReply
+ /// object may be missing some information, specifically in the instances where PingReply objects are
+ /// utilised to perform a traceroute.
+ ///
+ /// The source machine name or IP of the ping.
+ /// The destination machine name of the ping.
+ /// The response from the ping attempt.
+ /// The latency of the ping.
+ /// The buffer size.
+ /// The sequence number in the sequence of pings to the hop point.
+ internal PingStatus(
+ string source,
+ string destination,
+ PingReply reply,
+ long latency,
+ int bufferSize,
+ uint pingNum)
+ : this(source, destination, reply, pingNum)
+ {
+ _bufferSize = bufferSize;
+ _latency = latency;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source machine name or IP of the ping.
+ /// The destination machine name of the ping.
+ /// The response from the ping attempt.
+ /// The sequence number of the ping in the sequence of pings to the target.
+ internal PingStatus(string source, string destination, PingReply reply, uint pingNum)
+ {
+ Ping = pingNum;
+ Reply = reply;
+ Source = source;
+ Destination = destination;
+ }
+
+ // These values can be set manually to skirt issues with the Ping API on Unix platforms
+ // so that we can return meaningful known data that is discarded by the API.
+ private readonly int _bufferSize = -1;
+
+ private readonly long _latency = -1;
+
+ ///
+ /// Gets the sequence number of this ping in the sequence of pings to the
+ ///
+ public uint Ping { get; }
+
+ ///
+ /// Gets the source from which the ping was sent.
+ ///
+ public string Source { get; }
+
+ ///
+ /// Gets the destination which was pinged.
+ ///
+ public string Destination { get; }
+
+ ///
+ /// Gets the target address of the ping.
+ ///
+ public IPAddress? Address { get => Reply.Status == IPStatus.Success ? Reply.Address : null; }
+
+ ///
+ /// Gets the target address of the ping if one is available, or "*" if it is not.
+ ///
+ public string DisplayAddress { get => Address?.ToString() ?? "*"; }
+
+ ///
+ /// Gets the roundtrip time of the ping in milliseconds.
+ ///
+ public long Latency { get => _latency >= 0 ? _latency : Reply.RoundtripTime; }
+
+ ///
+ /// Gets the returned status of the ping.
+ ///
+ public IPStatus Status { get => Reply.Status; }
+
+ ///
+ /// Gets the size in bytes of the buffer data sent in the ping.
+ ///
+ public int BufferSize { get => _bufferSize >= 0 ? _bufferSize : Reply.Buffer.Length; }
+
+ ///
+ /// Gets the reply object from this ping.
+ ///
+ public PingReply Reply { get; }
+ }
+
+ ///
+ /// The class contains information about the source, the destination and ping results.
+ ///
+ public class PingMtuStatus : PingStatus
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source machine name or IP of the ping.
+ /// The destination machine name of the ping.
+ /// The response from the ping attempt.
+ /// The buffer size from the successful ping attempt.
+ internal PingMtuStatus(string source, string destination, PingReply reply, int bufferSize)
+ : base(source, destination, reply, 1)
+ {
+ MtuSize = bufferSize;
+ }
+
+ ///
+ /// Gets the maximum transmission unit size on the network path between the source and destination.
+ ///
+ public int MtuSize { get; }
+ }
+
+ ///
+ /// The class contains an information about a trace route attempt.
+ ///
+ public class TraceStatus
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The hop number of this trace hop.
+ /// The PingStatus response from this trace hop.
+ /// The source computer name or IP address of the traceroute.
+ /// The target destination of the traceroute.
+ /// The target IPAddress of the overall traceroute.
+ internal TraceStatus(
+ int hop,
+ PingStatus status,
+ string source,
+ string destination,
+ IPAddress destinationAddress)
+ {
+ _status = status;
+ Hop = hop;
+ Source = source;
+ Target = destination;
+ TargetAddress = destinationAddress;
+ }
+
+ private readonly PingStatus _status;
+
+ ///
+ /// Gets the number of the current hop / router.
+ ///
+ public int Hop { get; }
+
+ ///
+ /// Gets the hostname of the current hop point.
+ ///
+ ///
+ public string? Hostname
+ {
+ get => _status.Destination != IPAddress.Any.ToString()
+ && _status.Destination != IPAddress.IPv6Any.ToString()
+ ? _status.Destination
+ : null;
+ }
+
+ ///
+ /// Gets the sequence number of the ping in the sequence of pings to the hop point.
+ ///
+ public uint Ping { get => _status.Ping; }
+
+ ///
+ /// Gets the IP address of the current hop point.
+ ///
+ public IPAddress? HopAddress { get => _status.Address; }
+
+ ///
+ /// Gets the latency values of each ping to the current hop point.
+ ///
+ public long Latency { get => _status.Latency; }
+
+ ///
+ /// Gets the status of the traceroute hop.
+ ///
+ public IPStatus Status { get => _status.Status; }
+
+ ///
+ /// Gets the source address of the traceroute command.
+ ///
+ public string Source { get; }
+
+ ///
+ /// Gets the final destination hostname of the trace.
+ ///
+ public string Target { get; }
+
+ ///
+ /// Gets the final destination IP address of the trace.
+ ///
+ public IPAddress TargetAddress { get; }
+
+ ///
+ /// Gets the raw PingReply object received from the ping to this hop point.
+ ///
+ public PingReply Reply { get => _status.Reply; }
+ }
///
/// Finalizes an instance of the class.
diff --git a/src/Microsoft.PowerShell.Commands.Management/resources/TestConnectionResources.resx b/src/Microsoft.PowerShell.Commands.Management/resources/TestConnectionResources.resx
index 08dcf439e28..ddd4ba8f815 100644
--- a/src/Microsoft.PowerShell.Commands.Management/resources/TestConnectionResources.resx
+++ b/src/Microsoft.PowerShell.Commands.Management/resources/TestConnectionResources.resx
@@ -126,4 +126,7 @@
Target IPv4/IPv6 address absent.
+
+ Cannot complete traceroute to destination '{0}': Number of hops required to reach host exceeds MaxHops ({1}).
+
diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs
index e93275ab70e..c05e5ab74f4 100644
--- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs
+++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs
@@ -241,6 +241,18 @@ internal static IEnumerable GetFormatData()
"Microsoft.PowerShell.MarkdownRender.PSMarkdownOptionInfo",
ViewsOf_Microsoft_PowerShell_MarkdownRender_MarkdownOptionInfo());
+ yield return new ExtendedTypeDefinition(
+ "Microsoft.PowerShell.Commands.TestConnectionCommand+PingStatus",
+ ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_PingStatus());
+
+ yield return new ExtendedTypeDefinition(
+ "Microsoft.PowerShell.Commands.TestConnectionCommand+PingMtuStatus",
+ ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_PingMtuStatus());
+
+ yield return new ExtendedTypeDefinition(
+ "Microsoft.PowerShell.Commands.TestConnectionCommand+TraceStatus",
+ ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_TraceStatus());
+
yield return new ExtendedTypeDefinition(
"Microsoft.PowerShell.Commands.ByteCollection",
ViewsOf_Microsoft_PowerShell_Commands_ByteCollection());
@@ -1770,6 +1782,110 @@ private static IEnumerable ViewsOf_Microsoft_PowerShell_Ma
.EndList());
}
+ private static IEnumerable ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_PingStatus()
+ {
+ yield return new FormatViewDefinition(
+ "Microsoft.PowerShell.Commands.TestConnectionCommand+PingStatus",
+ TableControl.Create()
+ .AddHeader(Alignment.Right, label: "Ping", width: 4)
+ .AddHeader(Alignment.Left, label: "Source", width: 16)
+ .AddHeader(Alignment.Left, label: "Address", width: 25)
+ .AddHeader(Alignment.Right, label: "Latency(ms)", width: 7)
+ .AddHeader(Alignment.Right, label: "BufferSize(B)", width: 10)
+ .AddHeader(Alignment.Left, label: "Status", width: 16)
+ .StartRowDefinition()
+ .AddPropertyColumn("Ping")
+ .AddPropertyColumn("Source")
+ .AddPropertyColumn("DisplayAddress")
+ .AddScriptBlockColumn(@"
+ if ($_.Status -eq 'TimedOut') {
+ '*'
+ }
+ else {
+ $_.Latency
+ }
+ ")
+ .AddScriptBlockColumn(@"
+ if ($_.Status -eq 'TimedOut') {
+ '*'
+ }
+ else {
+ $_.BufferSize
+ }
+ ")
+ .AddPropertyColumn("Status")
+ .EndRowDefinition()
+ .GroupByProperty("Destination")
+ .EndTable());
+ }
+
+ private static IEnumerable ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_PingMtuStatus()
+ {
+ yield return new FormatViewDefinition(
+ "Microsoft.PowerShell.Commands.TestConnectionCommand+PingMtuStatus",
+ TableControl.Create()
+ .AddHeader(Alignment.Left, label: "Source", width: 16)
+ .AddHeader(Alignment.Left, label: "Address", width: 25)
+ .AddHeader(Alignment.Right, label: "Latency(ms)", width: 7)
+ .AddHeader(Alignment.Left, label: "Status", width: 16)
+ .AddHeader(Alignment.Right, label: "MtuSize(B)", width: 7)
+ .StartRowDefinition()
+ .AddPropertyColumn("Source")
+ .AddPropertyColumn("DisplayAddress")
+ .AddScriptBlockColumn(@"
+ if ($_.Status -eq 'TimedOut') {
+ '*'
+ }
+ else {
+ $_.Latency
+ }
+ ")
+ .AddPropertyColumn("Status")
+ .AddPropertyColumn("MtuSize")
+ .EndRowDefinition()
+ .GroupByProperty("Destination")
+ .EndTable());
+ }
+
+ private static IEnumerable ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_TraceStatus()
+ {
+ yield return new FormatViewDefinition(
+ "Microsoft.PowerShell.Commands.TestConnectionCommand+TraceStatus",
+ TableControl.Create()
+ .AddHeader(Alignment.Right, label: "Hop", width: 3)
+ .AddHeader(Alignment.Left, label: "Hostname", width: 25)
+ .AddHeader(Alignment.Right, label: "Ping", width: 4)
+ .AddHeader(Alignment.Right, label: "Latency(ms)", width: 7)
+ .AddHeader(Alignment.Left, label: "Status", width: 16)
+ .AddHeader(Alignment.Left, label: "Source", width: 12)
+ .AddHeader(Alignment.Left, label: "TargetAddress", width: 15)
+ .StartRowDefinition()
+ .AddPropertyColumn("Hop")
+ .AddScriptBlockColumn(@"
+ if ($_.Hostname) {
+ $_.HostName
+ }
+ else {
+ '*'
+ }
+ ")
+ .AddPropertyColumn("Ping")
+ .AddScriptBlockColumn(@"
+ if ($_.Status -eq 'TimedOut') {
+ '*'
+ }
+ else {
+ $_.Latency
+ }
+ ")
+ .AddPropertyColumn("Status")
+ .AddPropertyColumn("Source")
+ .AddPropertyColumn("TargetAddress")
+ .EndRowDefinition()
+ .GroupByProperty("Target")
+ .EndTable());
+ }
+
private static IEnumerable ViewsOf_Microsoft_PowerShell_Commands_ByteCollection()
{
yield return new FormatViewDefinition(
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1
index 15bd53bf495..d3ec629f80f 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1
@@ -5,54 +5,40 @@ Import-Module HelpersCommon
Describe "Test-Connection" -tags "CI" {
BeforeAll {
- $oldInformationPreference = $InformationPreference
- $oldProgressPreference = $ProgressPreference
- $InformationPreference = "Ignore"
- $ProgressPreference = "SilentlyContinue"
-
$hostName = [System.Net.Dns]::GetHostName()
$targetName = "localhost"
$targetAddress = "127.0.0.1"
- # TODO:
- # CI Travis don't support IPv6
- # so we use the workaround.
- # $targetAddressIPv6 = "::1"
- $targetAddressIPv6 = [System.Net.Dns]::GetHostEntry($targetName).AddressList[0].IPAddressToString
+ $targetAddressIPv6 = "::1"
$UnreachableAddress = "10.11.12.13"
# this resolves to an actual IP rather than 127.0.0.1
# this can also include both IPv4 and IPv6, so select InterNetwork rather than InterNetworkV6
$realAddress = [System.Net.Dns]::GetHostEntry($hostName).AddressList |
- Where-Object {$_.AddressFamily -eq "InterNetwork"} |
+ Where-Object { $_.AddressFamily -eq "InterNetwork" } |
Select-Object -First 1 |
- Foreach-Object {$_.IPAddressToString}
+ ForEach-Object { $_.IPAddressToString }
# under some environments, we can't round trip this and retrieve the real name from the address
# in this case we will simply use the hostname
- $jobContinues = Start-Job { Test-Connection $using:targetAddress -Continues }
- }
-
- AfterAll {
- $InformationPreference = $oldInformationPreference
- $ProgressPreference = $oldProgressPreference
+ $jobContinues = Start-Job { Test-Connection $using:targetAddress -Repeat }
}
Context "Ping" {
It "Default parameter set is 'Ping'" {
- $result = Test-Connection $targetName
- $replies = $result.Replies
-
- $result.Count | Should -Be 1
- $result[0] | Should -BeOfType "Microsoft.PowerShell.Commands.TestConnectionCommand+PingReport"
- $result[0].Source | Should -BeExactly $hostName
- $result[0].Destination | Should -BeExactly $targetName
-
- $replies.Count | Should -Be 4
- $replies[0] | Should -BeOfType "System.Net.NetworkInformation.PingReply"
- $replies[0].Address | Should -BeExactly $targetAddressIPv6
- $replies[0].Status | Should -BeExactly "Success"
- # TODO: Here and below we skip the check on Unix because .Net Core issue
- if ($isWindows) {
- $replies[0].Buffer.Count | Should -Be 32
- }
+ $pingResults = Test-Connection $targetName
+ $pingResults.Count | Should -Be 4
+
+ $result = $pingResults |
+ Where-Object Status -eq 'Success' |
+ Select-Object -First 1
+
+ $result | Should -BeOfType "Microsoft.PowerShell.Commands.TestConnectionCommand+PingStatus"
+ $result.Ping | Should -Be 1
+ $result.Source | Should -BeExactly $hostName
+ $result.Destination | Should -BeExactly $targetName
+ $result.Address | Should -BeIn @($targetAddress, $targetAddressIPv6)
+ $result.Status | Should -BeExactly "Success"
+ $result.Latency | Should -BeOfType "long"
+ $result.Reply | Should -BeOfType "System.Net.NetworkInformation.PingReply"
+ $result.BufferSize | Should -Be 32
}
It "Count parameter" {
@@ -60,8 +46,8 @@ Describe "Test-Connection" -tags "CI" {
$result1 = Test-Connection -Ping $targetName -Count 1
$result2 = Test-Connection $targetName -Count 2
- $result1.Replies.Count | Should -Be 1
- $result2.Replies.Count | Should -Be 2
+ $result1.Count | Should -Be 1
+ $result2.Count | Should -Be 2
}
It "Quiet works" {
@@ -75,7 +61,8 @@ Describe "Test-Connection" -tags "CI" {
It "Ping fake host" {
- { $result = Test-Connection "fakeHost" -Count 1 -Quiet -ErrorAction Stop } | Should -Throw -ErrorId "TestConnectionException,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { $result = Test-Connection "fakeHost" -Count 1 -Quiet -ErrorAction Stop } |
+ Should -Throw -ErrorId "TestConnectionException,Microsoft.PowerShell.Commands.TestConnectionCommand"
# Error code = 11001 - Host not found.
if (!$isWindows) {
$Error[0].Exception.InnerException.ErrorCode | Should -Be -131073
@@ -88,68 +75,91 @@ Describe "Test-Connection" -tags "CI" {
It "Force IPv4 with implicit PingOptions" {
$result = Test-Connection $hostName -Count 1 -IPv4
- $result.Replies[0].Address | Should -BeExactly $realAddress
- $result.Replies[0].Options.Ttl | Should -BeLessOrEqual 128
+ $result[0].Address | Should -BeExactly $realAddress
+ $result[0].Reply.Options.Ttl | Should -BeLessOrEqual 128
if ($isWindows) {
- $result.Replies[0].Options.DontFragment | Should -BeFalse
+ $result[0].Reply.Options.DontFragment | Should -BeFalse
}
}
# In VSTS, address is 0.0.0.0
- It "Force IPv4 with explicit PingOptions" {
+ # This test is marked as PENDING as .NET Core does not return correct PingOptions from ping request
+ It "Force IPv4 with explicit PingOptions" -Pending {
$result1 = Test-Connection $hostName -Count 1 -IPv4 -MaxHops 10 -DontFragment
# explicitly go to google dns. this test will pass even if the destination is unreachable
# it's more about breaking out of the loop
$result2 = Test-Connection 8.8.8.8 -Count 1 -IPv4 -MaxHops 1 -DontFragment
- $result1.Replies[0].Address | Should -BeExactly $realAddress
- # .Net Core (.Net Framework) returns Options based on default PingOptions() constructor (Ttl=128, DontFragment = false).
- # After .Net Core fix we should have 'DontFragment | Should -Be $true' here.
- $result1.Replies[0].Options.Ttl | Should -BeLessOrEqual 128
+ $result1.Address | Should -BeExactly $realAddress
+ $result1.Reply.Options.Ttl | Should -BeLessOrEqual 128
+
if (!$isWindows) {
- if ( (Get-PlatformInfo) -eq "alpine" ) {
- $result1.Replies[0].Options.DontFragment | Should -Be $true
- }
- else {
- $result1.Replies[0].Options.DontFragment | Should -BeNullOrEmpty
- }
- # depending on the network configuration any of the following should be returned
- $result2.Replies[0].Status | Should -BeIn "TtlExpired","TimedOut","Success"
+ $result1.Reply.Options.DontFragment | Should -BeFalse
+ # Depending on the network configuration any of the following should be returned
+ $result2.Status | Should -BeIn "TtlExpired", "TimedOut", "Success"
} else {
- $result1.Replies[0].Options.DontFragment | Should -BeFalse
+ $result1.Reply.Options.DontFragment | Should -BeTrue
# We expect 'TtlExpired' but if a router don't reply we get `TimedOut`
# AzPipelines returns $null
- $result2.Replies[0].Status | Should -BeIn "TtlExpired","TimedOut",$null
+ $result2.Status | Should -BeIn "TtlExpired", "TimedOut", $null
}
}
- It "Force IPv6" -Pending {
- $result = Test-Connection $targetName -Count 1 -IPv6
+ Context 'IPv6 Tests' {
+ # IPv6 tests are marked pending because while the functionality is present
+ # and works in local testing, it is not functional in CI. There appears to
+ # be a lack of or inconsistent support for IPv6 in CI environments.
+ It "Allows us to Force IPv6" -Pending {
+ $result = Test-Connection $targetName -IPv6 -Count 4 |
+ Where-Object Status -eq Success |
+ Select-Object -First 1
+
+ $result.Address | Should -BeExactly $targetAddressIPv6
+ $result.Reply.Options | Should -Not -BeNullOrEmpty
+ }
+
+ It 'can convert IPv6 addresses to IPv4 with -IPv4 parameter' -Pending {
+ $result = Test-Connection '2001:4860:4860::8888' -IPv4 -Count 4 |
+ Where-Object Status -eq Success |
+ Select-Object -First 1
+ # Google's DNS can resolve to either address.
+ $result.Address.IPAddressToString | Should -BeIn @('8.8.8.8', '8.8.4.4')
+ $result.Address.AddressFamily | Should -BeExactly 'InterNetwork'
+ }
- $result.Replies[0].Address | Should -BeExactly $targetAddressIPv6
- # We should check Null not Empty!
- $result.Replies[0].Options | Should -Be $null
+ It 'can convert IPv4 addresses to IPv6 with -IPv6 parameter' -Pending {
+ $result = Test-Connection '8.8.8.8' -IPv6 -Count 4 |
+ Where-Object Status -eq Success |
+ Select-Object -First 1
+ # Google's DNS can resolve to either address.
+ $result.Address.IPAddressToString | Should -BeIn @('2001:4860:4860::8888', '2001:4860:4860::8844')
+ $result.Address.AddressFamily | Should -BeExactly 'InterNetworkV6'
+ }
}
It "MaxHops Should -Be greater 0" {
- { Test-Connection $targetName -MaxHops 0 } | Should -Throw -ErrorId "System.ArgumentOutOfRangeException,Microsoft.PowerShell.Commands.TestConnectionCommand"
- { Test-Connection $targetName -MaxHops -1 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { Test-Connection $targetName -MaxHops 0 } |
+ Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { Test-Connection $targetName -MaxHops -1 } |
+ Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
}
It "Count Should -Be greater 0" {
- { Test-Connection $targetName -Count 0 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { Test-Connection $targetName -Count 0 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
{ Test-Connection $targetName -Count -1 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
}
It "Delay Should -Be greater 0" {
- { Test-Connection $targetName -Delay 0 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
- { Test-Connection $targetName -Delay -1 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { Test-Connection $targetName -Delay 0 } |
+ Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { Test-Connection $targetName -Delay -1 } |
+ Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
}
It "Delay works" {
- $result1 = measure-command {Test-Connection localhost -Count 2}
- $result2 = measure-command {Test-Connection localhost -Delay 4 -Count 2}
+ $result1 = Measure-Command { Test-Connection localhost -Count 2 }
+ $result2 = Measure-Command { Test-Connection localhost -Delay 4 -Count 2 }
$result1.TotalSeconds | Should -BeGreaterThan 1
$result1.TotalSeconds | Should -BeLessThan 3
@@ -157,17 +167,17 @@ Describe "Test-Connection" -tags "CI" {
}
It "BufferSize Should -Be between 0 and 65500" {
- { Test-Connection $targetName -BufferSize 0 } | Should Not Throw
- { Test-Connection $targetName -BufferSize -1 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
- { Test-Connection $targetName -BufferSize 65501 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { Test-Connection $targetName -BufferSize 0 } | Should -Not -Throw
+ { Test-Connection $targetName -BufferSize -1 } |
+ Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
+ { Test-Connection $targetName -BufferSize 65501 } |
+ Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.TestConnectionCommand"
}
- It "BufferSize works" -Pending:(!$IsWindows) {
+ It "BufferSize works" {
$result = Test-Connection $targetName -Count 1 -BufferSize 2
- if ($isWindows) {
- $result.Replies[0].Buffer.Count | Should -Be 2
- }
+ $result.BufferSize | Should -Be 2
}
It "ResolveDestination for address" {
@@ -175,7 +185,7 @@ Describe "Test-Connection" -tags "CI" {
$resolvedName = [System.Net.DNS]::GetHostEntry($targetAddress).HostName
$result.Destination | Should -BeExactly $resolvedName
- $result.Replies[0].Address | Should -BeExactly $targetAddress
+ $result.Address | Should -BeExactly $targetAddress
}
It "ResolveDestination for name" {
@@ -187,42 +197,45 @@ Describe "Test-Connection" -tags "CI" {
$resolvedAddress = ([System.Net.DNS]::GetHostAddresses($resolvedName)[0] -split "%")[0]
$result.Destination | Should -BeExactly $resolvedName
- $result.Replies[0].Address | Should -BeExactly $resolvedAddress
+ $result.Address | Should -BeExactly $resolvedAddress
}
It "TimeOut works" {
- (Measure-Command { Test-Connection $UnreachableAddress -Count 1 -TimeOut 1 }).TotalSeconds | Should -BeLessThan 3
- (Measure-Command { Test-Connection $UnreachableAddress -Count 1 -TimeOut 4 }).TotalSeconds | Should -BeGreaterThan 3
+ (Measure-Command { Test-Connection $UnreachableAddress -Count 1 -TimeOut 1 }).TotalSeconds |
+ Should -BeLessThan 3
+ (Measure-Command { Test-Connection $UnreachableAddress -Count 1 -TimeOut 4 }).TotalSeconds |
+ Should -BeGreaterThan 3
}
- It "Continues works" {
- # By default we do 4 ping so for '-Continues' we expect to get >4 results.
+ It "Repeat works" {
+ # By default we do 4 ping so for '-Repeat' we expect to get >4 results.
# Also we should wait >4 seconds before check results but previous tests already did the pause.
- $result = Receive-Job $jobContinues
+ $pingResults = Receive-Job $jobContinues
Remove-Job $jobContinues -Force
- $result.Count | Should -BeGreaterThan 4
- $result[0].Address | Should -BeExactly $targetAddress
- $result[0].Status | Should -BeExactly "Success"
+ $pingResults.Count | Should -BeGreaterThan 4
+ $pingResults[0].Address | Should -BeExactly $targetAddress
+ $pingResults.Status | Should -Contain "Success"
if ($isWindows) {
- $result[0].Buffer.Count | Should -Be 32
+ $pingResults.Where( { $_.Status -eq 'Success' }, 'Default', 1 ).BufferSize | Should -Be 32
}
}
-}
+ }
- # TODO: We skip the MTUSizeDetect tests on Unix because we expect 'TtlExpired' but get 'TimeOut' internally from .Net Core
Context "MTUSizeDetect" {
- It "MTUSizeDetect works" -pending:($IsMacOS) {
- $result = Test-Connection $hostName -MTUSizeDetect
+ # We skip the MtuSize detection tests when in containers, as the environments throw raw exceptions
+ # instead of returning a PacketTooBig response cleanly.
+ It "MTUSizeDetect works" -Pending:($env:__INCONTAINER -eq 1) {
+ $result = Test-Connection $hostName -MtuSize
- $result | Should -BeOfType "System.Net.NetworkInformation.PingReply"
+ $result | Should -BeOfType "Microsoft.PowerShell.Commands.TestConnectionCommand+PingMtuStatus"
$result.Destination | Should -BeExactly $hostName
$result.Status | Should -BeExactly "Success"
- $result.MTUSize | Should -BeGreaterThan 0
+ $result.MtuSize | Should -BeGreaterThan 0
}
- It "Quiet works" -pending:($IsMacOS) {
- $result = Test-Connection $hostName -MTUSizeDetect -Quiet
+ It "Quiet works" -Pending:($env:__INCONTAINER -eq 1) {
+ $result = Test-Connection $hostName -MtuSize -Quiet
$result | Should -BeOfType "Int32"
$result | Should -BeGreaterThan 0
@@ -233,35 +246,35 @@ Describe "Test-Connection" -tags "CI" {
It "TraceRoute works" {
# real address is an ipv4 address, so force IPv4
$result = Test-Connection $hostName -TraceRoute -IPv4
- $replies = $result.Replies
- # Check target host reply.
- $pingReplies = $replies[-1].PingReplies
-
- $result.Count | Should -Be 1
- $result | Should -BeOfType "Microsoft.PowerShell.Commands.TestConnectionCommand+TraceRouteResult"
- $result.Source | Should -BeExactly $hostName
- $result.DestinationAddress | Should -BeExactly $realAddress
- $result.DestinationHost | Should -BeExactly $hostName
-
- $replies.Count | Should -BeGreaterThan 0
- $replies[0] | Should -BeOfType "Microsoft.PowerShell.Commands.TestConnectionCommand+TraceRouteReply"
- $replies[0].Hop | Should -Be 1
-
- $pingReplies.Count | Should -Be 3
- $pingReplies[0].Address | Should -BeExactly $realAddress
- $pingReplies[0].Status | Should -BeExactly "Success"
+
+ $result[0] | Should -BeOfType "Microsoft.PowerShell.Commands.TestConnectionCommand+TraceStatus"
+ $result[0].Source | Should -BeExactly $hostName
+ $result[0].TargetAddress | Should -BeExactly $realAddress
+ $result[0].Target | Should -BeExactly $hostName
+ $result[0].Hop | Should -Be 1
+ $result[0].HopAddress | Should -BeExactly $realAddress
+ $result[0].Status | Should -BeExactly "Success"
if (!$isWindows) {
- $pingReplies[0].Buffer.Count | Should -Match '^0$|^32$'
+ $result[0].Reply.Buffer.Count | Should -Match '^0$|^32$'
} else {
- $pingReplies[0].Buffer.Count | Should -Be 32
+ $result[0].Reply.Buffer.Count | Should -Be 32
}
}
It "Quiet works" {
- $result = Test-Connection $hostName -TraceRoute -Quiet 6> $null
+ $result = Test-Connection $hostName -TraceRoute -Quiet
$result | Should -BeTrue
}
+
+ It 'writes an error if MaxHops is exceeded during -Traceroute' {
+ { Test-Connection 8.8.8.8 -Traceroute -MaxHops 2 -ErrorAction Stop } |
+ Should -Throw -ErrorId 'TestConnectionException,Microsoft.PowerShell.Commands.TestConnectionCommand'
+ }
+
+ It 'returns false without error if MaxHops is exceeded during -Traceroute -Quiet' {
+ Test-Connection 8.8.8.8 -Traceroute -MaxHops 2 -Quiet | Should -BeFalse
+ }
}
}
@@ -273,10 +286,10 @@ Describe "Connection" -Tag "CI", "RequireAdminOnWindows" {
}
It "Test connection to local host port 80" {
- Test-Connection '127.0.0.1' -TCPPort $WebListener.HttpPort | Should -BeTrue
+ Test-Connection '127.0.0.1' -TcpPort $WebListener.HttpPort | Should -BeTrue
}
It "Test connection to unreachable host port 80" {
- Test-Connection $UnreachableAddress -TCPPort 80 -TimeOut 1 | Should -BeFalse
+ Test-Connection $UnreachableAddress -TcpPort 80 -TimeOut 1 | Should -BeFalse
}
}