diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestPortHelper.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestPortHelper.cs new file mode 100644 index 0000000000..05c6080cd5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestPortHelper.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common +{ + public static class TestPortHelper + { + // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508 + // + // This method is an attempt to safely get a free port from the OS. Most of the time, + // when binding to dynamic port "0" the OS increments the assigned port, so it's safe + // to re-use the assigned port in another process. However, occasionally the OS will reuse + // a recently assigned port instead of incrementing, which causes flaky tests with AddressInUse + // exceptions. This method should only be used when the application itself cannot use + // dynamic port "0" (e.g. IISExpress). Most functional tests using raw Kestrel + // (with status messages enabled) should directly bind to dynamic port "0" and scrape + // the assigned port from the status message, which should be 100% reliable since the port + // is bound once and never released. + public static int GetNextPort() + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + } + + // IIS Express preregisteres 44300-44399 ports with SSL bindings. + // So some tests always have to use ports in this range, and we can't rely on OS-allocated ports without a whole lot of ceremony around + // creating self-signed certificates and registering SSL bindings with HTTP.sys + public static int GetNextSSLPort() + { + var next = 44300; + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + while (true) + { + try + { + var port = next++; + socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); + return port; + } + catch (SocketException) + { + // Retry unless exhausted + if (next > 44399) + { + throw; + } + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs index 012105a8ff..1c09b08dfa 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common // from which to scrape the assigned port, so the less reliable GetNextPort() must be used. The // port is bound on "localhost" (both IPv4 and IPv6), since this is supported when using a specific // (non-zero) port. - return new UriBuilder(scheme, "localhost", GetNextPort()).Uri; + return new UriBuilder(scheme, "localhost", TestPortHelper.GetNextPort()).Uri; } } else @@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common // Only a few tests use this codepath, so it's fine to use the less reliable GetNextPort() for simplicity. // The tests using this codepath will be reviewed to see if they can be changed to directly bind to dynamic // port "0" on "127.0.0.1" and scrape the assigned port from the status message (the default codepath). - return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri; + return new UriBuilder(uriHint) { Port = TestPortHelper.GetNextPort() }.Uri; } else { @@ -61,25 +61,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common } } } - - // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508 - // - // This method is an attempt to safely get a free port from the OS. Most of the time, - // when binding to dynamic port "0" the OS increments the assigned port, so it's safe - // to re-use the assigned port in another process. However, occasionally the OS will reuse - // a recently assigned port instead of incrementing, which causes flaky tests with AddressInUse - // exceptions. This method should only be used when the application itself cannot use - // dynamic port "0" (e.g. IISExpress). Most functional tests using raw Kestrel - // (with status messages enabled) should directly bind to dynamic port "0" and scrape - // the assigned port from the status message, which should be 100% reliable since the port - // is bound once and never released. - internal static int GetNextPort() - { - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - return ((IPEndPoint)socket.LocalEndPoint).Port; - } - } } } diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs index bdb70d4287..91c9c7eae3 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting var port = uri.Port; if (port == 0) { - port = TestUriHelper.GetNextPort(); + port = (uri.Scheme == "https") ? TestPortHelper.GetNextSSLPort() : TestPortHelper.GetNextPort(); } for (var attempt = 0; attempt < MaximumAttempts; attempt++)