diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs index e6b33142457..98660f5a499 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs @@ -27,6 +27,7 @@ namespace Microsoft.PowerShell.Commands [OutputType(typeof(PingMtuStatus), ParameterSetName = new string[] { MtuSizeDetectParameterSet })] [OutputType(typeof(int), ParameterSetName = new string[] { MtuSizeDetectParameterSet })] [OutputType(typeof(TraceStatus), ParameterSetName = new string[] { TraceRouteParameterSet })] + [OutputType(typeof(TcpPortStatus), ParameterSetName = new string[] { TcpPortParameterSet })] public class TestConnectionCommand : PSCmdlet, IDisposable { #region Parameter Set Names @@ -134,6 +135,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable /// The default (from Windows) is 4 times. /// [Parameter(ParameterSetName = DefaultPingParameterSet)] + [Parameter(ParameterSetName = TcpPortParameterSet)] [ValidateRange(ValidateRangeKind.Positive)] public int Count { get; set; } = 4; @@ -143,6 +145,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable /// [Parameter(ParameterSetName = DefaultPingParameterSet)] [Parameter(ParameterSetName = RepeatPingParameterSet)] + [Parameter(ParameterSetName = TcpPortParameterSet)] [ValidateRange(ValidateRangeKind.Positive)] public int Delay { get; set; } = 1; @@ -169,6 +172,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable /// Gets or sets whether to continue pinging until user presses Ctrl-C (or Int.MaxValue threshold reached). /// [Parameter(Mandatory = true, ParameterSetName = RepeatPingParameterSet)] + [Parameter(ParameterSetName = TcpPortParameterSet)] [Alias("Continuous")] public SwitchParameter Repeat { get; set; } @@ -180,6 +184,13 @@ public class TestConnectionCommand : PSCmdlet, IDisposable [Parameter] public SwitchParameter Quiet { get; set; } + /// + /// Gets or sets whether to enable detailed output mode while running a TCP connection test. + /// Without this flag, the TCP test will return a boolean result. + /// + [Parameter] + public SwitchParameter Detailed; + /// /// 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. @@ -227,6 +238,7 @@ public class TestConnectionCommand : PSCmdlet, IDisposable /// /// BeginProcessing implementation for TestConnectionCommand. + /// Sets Count for different types of tests unless specified explicitly. /// protected override void BeginProcessing() { @@ -235,6 +247,9 @@ protected override void BeginProcessing() case RepeatPingParameterSet: Count = int.MaxValue; break; + case TcpPortParameterSet: + SetCountForTcpTest(); + break; } } @@ -281,6 +296,18 @@ protected override void StopProcessing() #region ConnectionTest + private void SetCountForTcpTest() + { + if (Repeat.IsPresent) + { + Count = int.MaxValue; + } + else if (!MyInvocation.BoundParameters.ContainsKey(nameof(Count))) + { + Count = 1; + } + } + private void ProcessConnectionByTCPPort(string targetNameOrAddress) { if (!TryResolveNameOrAddress(targetNameOrAddress, out _, out IPAddress? targetAddress)) @@ -293,42 +320,80 @@ private void ProcessConnectionByTCPPort(string targetNameOrAddress) return; } - TcpClient client = new(); + int timeoutMilliseconds = TimeoutSeconds * 1000; + int delayMilliseconds = Delay * 1000; - try + for (var i = 1; i <= Count; i++) { - Task connectionTask = client.ConnectAsync(targetAddress, TcpPort); - string targetString = targetAddress.ToString(); + long latency = 0; + SocketError status = SocketError.SocketError; + + Stopwatch stopwatch = new Stopwatch(); - for (var i = 1; i <= TimeoutSeconds; i++) + using var client = new TcpClient(); + + try { - Task timeoutTask = Task.Delay(millisecondsDelay: 1000); - Task.WhenAny(connectionTask, timeoutTask).Result.Wait(); + stopwatch.Start(); - if (timeoutTask.Status == TaskStatus.Faulted || timeoutTask.Status == TaskStatus.Canceled) + if (client.ConnectAsync(targetAddress, TcpPort).Wait(timeoutMilliseconds, _dnsLookupCancel.Token)) { - // Waiting is interrupted by Ctrl-C. - WriteObject(false); - return; + latency = stopwatch.ElapsedMilliseconds; + status = SocketError.Success; } - - if (connectionTask.Status == TaskStatus.RanToCompletion) + else { - WriteObject(true); - return; + status = SocketError.TimedOut; } } - } - catch - { - // Silently ignore connection errors. - } - finally - { - client.Close(); - } + catch (AggregateException ae) + { + ae.Handle((ex) => + { + if (ex is TaskCanceledException) + { + throw new PipelineStoppedException(); + } + if (ex is SocketException socketException) + { + status = socketException.SocketErrorCode; + return true; + } + else + { + return false; + } + }); + } + finally + { + stopwatch.Reset(); + } + + if (!Detailed.IsPresent) + { + WriteObject(status == SocketError.Success); + return; + } + else + { + WriteObject(new TcpPortStatus( + i, + Source, + targetNameOrAddress, + targetAddress, + TcpPort, + latency, + status == SocketError.Success, + status + )); + } - WriteObject(false); + if (i < Count) + { + Task.Delay(delayMilliseconds).Wait(_dnsLookupCancel.Token); + } + } } #endregion ConnectionTest @@ -875,6 +940,75 @@ private PingReply SendCancellablePing( } } + /// + /// The class contains information about the TCP connection test. + /// + public class TcpPortStatus + { + /// + /// Initializes a new instance of the class. + /// + /// The number of this test. + /// The source machine name or IP of the test. + /// The target machine name or IP of the test. + /// The resolved IP from the target. + /// The port used for the connection. + /// The latency of the test. + /// If the test connection succeeded. + /// Status of the underlying socket. + internal TcpPortStatus(int id, string source, string target, IPAddress targetAddress, int port, long latency, bool connected, SocketError status) + { + Id = id; + Source = source; + Target = target; + TargetAddress = targetAddress; + Port = port; + Latency = latency; + Connected = connected; + Status = status; + } + + /// + /// Gets and sets the count of the test. + /// + public int Id { get; set; } + + /// + /// Gets the source from which the test was sent. + /// + public string Source { get; } + + /// + /// Gets the target name. + /// + public string Target { get; } + + /// + /// Gets the resolved address for the target. + /// + public IPAddress TargetAddress { get; } + + /// + /// Gets the port used for the test. + /// + public int Port { get; } + + /// + /// Gets or sets the latancy of the connection. + /// + public long Latency { get; set; } + + /// + /// Gets or sets the result of the test. + /// + public bool Connected { get; set; } + + /// + /// Gets or sets the state of the socket after the test. + /// + public SocketError Status { get; set; } + } + /// /// The class contains information about the source, the destination and ping results. /// 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 ef7c40fd37b..d19df16bc1b 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs @@ -248,6 +248,10 @@ internal static IEnumerable GetFormatData() "Microsoft.PowerShell.MarkdownRender.PSMarkdownOptionInfo", ViewsOf_Microsoft_PowerShell_MarkdownRender_MarkdownOptionInfo()); + yield return new ExtendedTypeDefinition( + "Microsoft.PowerShell.Commands.TestConnectionCommand+TcpPortStatus", + ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_TcpPortStatus()); + yield return new ExtendedTypeDefinition( "Microsoft.PowerShell.Commands.TestConnectionCommand+PingStatus", ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_PingStatus()); @@ -1924,6 +1928,31 @@ private static IEnumerable ViewsOf_Microsoft_PowerShell_Ma .EndList()); } + private static IEnumerable ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_TcpPortStatus() + { + yield return new FormatViewDefinition( + "Microsoft.PowerShell.Commands.TestConnectionCommand+TcpPortStatus", + TableControl.Create() + .AddHeader(Alignment.Right, label: "Id", width: 4) + .AddHeader(Alignment.Left, label: "Source", width: 16) + .AddHeader(Alignment.Left, label: "Address", width: 25) + .AddHeader(Alignment.Right, label: "Port", width: 7) + .AddHeader(Alignment.Right, label: "Latency(ms)", width: 7) + .AddHeader(Alignment.Left, label: "Connected", width: 10) + .AddHeader(Alignment.Left, label: "Status", width: 24) + .StartRowDefinition() + .AddPropertyColumn("Id") + .AddPropertyColumn("Source") + .AddPropertyColumn("TargetAddress") + .AddPropertyColumn("Port") + .AddPropertyColumn("Latency") + .AddPropertyColumn("Connected") + .AddPropertyColumn("Status") + .EndRowDefinition() + .GroupByProperty("Target") + .EndTable()); + } + private static IEnumerable ViewsOf_Microsoft_PowerShell_Commands_TestConnectionCommand_PingStatus() { 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 1d639fe5f2d..2763d7d8991 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1 @@ -320,13 +320,49 @@ Describe "Connection" -Tag "CI", "RequireAdminOnWindows" { $UnreachableAddress = "10.11.12.13" } - It "Test connection to local host port 80" { + It "Test connection to local host on working port" { 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 } + + It "Test detailed connection to local host on working port" { + $result = Test-Connection '127.0.0.1' -TcpPort $WebListener.HttpPort -Detailed + + $result.Count | Should -Be 1 + $result[0].Id | Should -BeExactly 1 + $result[0].TargetAddress | Should -BeExactly '127.0.0.1' + $result[0].Port | Should -Be $WebListener.HttpPort + $result[0].Latency | Should -BeGreaterOrEqual 0 + $result[0].Connected | Should -BeTrue + $result[0].Status | Should -BeExactly 'Success' + } + + It "Test detailed connection to local host on working port with modified count" { + $result = Test-Connection '127.0.0.1' -TcpPort $WebListener.HttpPort -Detailed -Count 2 + + $result.Count | Should -Be 2 + $result[0].Id | Should -BeExactly 1 + $result[0].TargetAddress | Should -BeExactly '127.0.0.1' + $result[0].Port | Should -Be $WebListener.HttpPort + $result[0].Latency | Should -BeGreaterOrEqual 0 + $result[0].Connected | Should -BeTrue + $result[0].Status | Should -BeExactly 'Success' + } + + It "Test detailed connection to unreachable host port 80" { + $result = Test-Connection $UnreachableAddress -TcpPort 80 -Detailed -TimeOut 1 + + $result.Count | Should -Be 1 + $result[0].Id | Should -BeExactly 1 + $result[0].TargetAddress | Should -BeExactly $UnreachableAddress + $result[0].Port | Should -Be 80 + $result[0].Latency | Should -BeExactly 0 + $result[0].Connected | Should -BeFalse + $result[0].Status | Should -Not -BeExactly 'Success' + } } Describe "Test-Connection should run in the default synchronization context (threadpool)" -Tag "CI" {