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 } }