Add support for port 0 in HttpSys (#13841)
This commit is contained in:
parent
71c5c66b21
commit
96f1c92caa
|
|
@ -56,34 +56,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const int BasePort = 5001;
|
||||
private const int MaxPort = 8000;
|
||||
private static int NextPort = BasePort;
|
||||
|
||||
// GetNextPort doesn't check for HttpSys urlacls.
|
||||
public static int GetNextHttpSysPort(string scheme)
|
||||
{
|
||||
while (NextPort < MaxPort)
|
||||
{
|
||||
var port = NextPort++;
|
||||
|
||||
using (var server = new HttpListener())
|
||||
{
|
||||
server.Prefixes.Add($"{scheme}://localhost:{port}/");
|
||||
try
|
||||
{
|
||||
server.Start();
|
||||
server.Stop();
|
||||
return port;
|
||||
}
|
||||
catch (HttpListenerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
NextPort = BasePort;
|
||||
throw new Exception("Failed to locate a free port.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
||||
{
|
||||
public static class TestUriHelper
|
||||
|
|
@ -34,7 +36,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
|
|||
}
|
||||
else if (serverType == ServerType.HttpSys)
|
||||
{
|
||||
return new UriBuilder(scheme, "localhost", TestPortHelper.GetNextHttpSysPort(scheme)).Uri;
|
||||
Debug.Assert(scheme == "http", "Https not supported");
|
||||
return new UriBuilder(scheme, "localhost", 0).Uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.HttpSys
|
||||
{
|
||||
|
|
@ -74,6 +76,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
}
|
||||
|
||||
var hostingUrlsPresent = _serverAddresses.Addresses.Count > 0;
|
||||
var serverAddressCopy = _serverAddresses.Addresses.ToList();
|
||||
_serverAddresses.Addresses.Clear();
|
||||
|
||||
if (_serverAddresses.PreferHostingUrls && hostingUrlsPresent)
|
||||
{
|
||||
|
|
@ -85,10 +89,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
Listener.Options.UrlPrefixes.Clear();
|
||||
}
|
||||
|
||||
foreach (var value in _serverAddresses.Addresses)
|
||||
{
|
||||
Listener.Options.UrlPrefixes.Add(value);
|
||||
}
|
||||
UpdateUrlPrefixes(serverAddressCopy);
|
||||
}
|
||||
else if (_options.UrlPrefixes.Count > 0)
|
||||
{
|
||||
|
|
@ -100,23 +101,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
_serverAddresses.Addresses.Clear();
|
||||
}
|
||||
|
||||
foreach (var prefix in _options.UrlPrefixes)
|
||||
{
|
||||
_serverAddresses.Addresses.Add(prefix.FullPrefix);
|
||||
}
|
||||
}
|
||||
else if (hostingUrlsPresent)
|
||||
{
|
||||
foreach (var value in _serverAddresses.Addresses)
|
||||
{
|
||||
Listener.Options.UrlPrefixes.Add(value);
|
||||
}
|
||||
UpdateUrlPrefixes(serverAddressCopy);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.LogDebug(_logger, $"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
|
||||
|
||||
_serverAddresses.Addresses.Add(Constants.DefaultServerAddress);
|
||||
Listener.Options.UrlPrefixes.Add(Constants.DefaultServerAddress);
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +122,13 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
|
||||
Listener.Start();
|
||||
|
||||
// Update server addresses after we start listening as port 0
|
||||
// needs to be selected at the point of binding.
|
||||
foreach (var prefix in _options.UrlPrefixes)
|
||||
{
|
||||
_serverAddresses.Addresses.Add(prefix.FullPrefix);
|
||||
}
|
||||
|
||||
ActivateRequestProcessingLimits();
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -142,6 +142,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdateUrlPrefixes(IList<string> serverAddressCopy)
|
||||
{
|
||||
foreach (var value in serverAddressCopy)
|
||||
{
|
||||
Listener.Options.UrlPrefixes.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
// The message pump.
|
||||
// When we start listening for the next request on one thread, we may need to be sure that the
|
||||
// completion continues on another thread as to not block the current request processing.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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;
|
||||
|
|
@ -73,7 +73,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
{
|
||||
LogHelper.LogInfo(_logger, "Listening on prefix: " + uriPrefix);
|
||||
CheckDisposed();
|
||||
|
||||
var statusCode = HttpApi.HttpAddUrlToUrlGroup(Id, uriPrefix, (ulong)contextId, 0);
|
||||
|
||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.HttpSys.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.HttpSys
|
||||
{
|
||||
|
|
@ -15,6 +20,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
private UrlGroup _urlGroup;
|
||||
private int _nextId = 1;
|
||||
|
||||
// Valid port range of 5000 - 48000.
|
||||
private const int BasePort = 5000;
|
||||
private const int MaxPortIndex = 43000;
|
||||
private const int MaxRetries = 1000;
|
||||
private static int NextPortIndex;
|
||||
|
||||
internal UrlPrefixCollection()
|
||||
{
|
||||
}
|
||||
|
|
@ -138,10 +149,55 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
{
|
||||
_urlGroup = urlGroup;
|
||||
// go through the uri list and register for each one of them
|
||||
foreach (var pair in _prefixes)
|
||||
// Call ToList to avoid modification when enumerating.
|
||||
foreach (var pair in _prefixes.ToList())
|
||||
{
|
||||
// We'll get this index back on each request and use it to look up the prefix to calculate PathBase.
|
||||
_urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
|
||||
var urlPrefix = pair.Value;
|
||||
if (urlPrefix.PortValue == 0)
|
||||
{
|
||||
if (urlPrefix.IsHttps)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot bind to port 0 with https.");
|
||||
}
|
||||
|
||||
FindHttpPortUnsynchronized(pair.Key, urlPrefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We'll get this index back on each request and use it to look up the prefix to calculate PathBase.
|
||||
_urlGroup.RegisterPrefix(pair.Value.FullPrefix, pair.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FindHttpPortUnsynchronized(int key, UrlPrefix urlPrefix)
|
||||
{
|
||||
for (var index = 0; index < MaxRetries; index++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Bit of complicated math to always try 3000 ports, starting from NextPortIndex + 5000,
|
||||
// circling back around if we go above 8000 back to 5000, and so on.
|
||||
var port = ((index + NextPortIndex) % MaxPortIndex) + BasePort;
|
||||
|
||||
Debug.Assert(port >= 5000 || port < 8000);
|
||||
|
||||
var newPrefix = UrlPrefix.Create(urlPrefix.Scheme, urlPrefix.Host, port, urlPrefix.Path);
|
||||
_urlGroup.RegisterPrefix(newPrefix.FullPrefix, key);
|
||||
_prefixes[key] = newPrefix;
|
||||
|
||||
NextPortIndex += index + 1;
|
||||
return;
|
||||
}
|
||||
catch (HttpSysException ex)
|
||||
{
|
||||
if ((ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ACCESS_DENIED
|
||||
&& ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SHARING_VIOLATION
|
||||
&& ex.ErrorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_ALREADY_EXISTS) || index == MaxRetries - 1)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,4 +215,4 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
@ECHO OFF
|
||||
|
||||
%~dp0..\..\..\startvs.cmd %~dp0HttpSysServer.sln
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Linq;
|
||||
|
|
@ -114,7 +114,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
{
|
||||
server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
|
||||
|
||||
Assert.Equal(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single());
|
||||
// Trailing slash is added when put in UrlPrefix.
|
||||
Assert.StartsWith(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
|
@ -21,11 +22,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
{
|
||||
// When tests projects are run in parallel, overlapping port ranges can cause a race condition when looking for free
|
||||
// ports during dynamic port allocation.
|
||||
private const int BasePort = 5001;
|
||||
private const int MaxPort = 8000;
|
||||
private const int BaseHttpsPort = 44300;
|
||||
private const int MaxHttpsPort = 44399;
|
||||
private static int NextPort = BasePort;
|
||||
private static int NextHttpsPort = BaseHttpsPort;
|
||||
private static object PortLock = new object();
|
||||
internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
|
||||
|
|
@ -84,39 +82,26 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
|
||||
internal static IWebHost CreateDynamicHost(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
|
||||
{
|
||||
lock (PortLock)
|
||||
{
|
||||
while (NextPort < MaxPort)
|
||||
var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.UseHttpSys(options =>
|
||||
{
|
||||
var port = NextPort++;
|
||||
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
|
||||
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
||||
baseAddress = prefix.ToString();
|
||||
options.UrlPrefixes.Add(prefix);
|
||||
configureOptions(options);
|
||||
})
|
||||
.Configure(appBuilder => appBuilder.Run(app));
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.UseHttpSys(options =>
|
||||
{
|
||||
options.UrlPrefixes.Add(prefix);
|
||||
configureOptions(options);
|
||||
})
|
||||
.Configure(appBuilder => appBuilder.Run(app));
|
||||
var host = builder.Build();
|
||||
|
||||
var host = builder.Build();
|
||||
host.Start();
|
||||
|
||||
var options = host.Services.GetRequiredService<IOptions<HttpSysOptions>>();
|
||||
prefix = options.Value.UrlPrefixes.First(); // Has new port
|
||||
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
||||
baseAddress = prefix.ToString();
|
||||
|
||||
try
|
||||
{
|
||||
host.Start();
|
||||
return host;
|
||||
}
|
||||
catch (HttpSysException)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
NextPort = BasePort;
|
||||
}
|
||||
throw new Exception("Failed to locate a free port.");
|
||||
return host;
|
||||
}
|
||||
|
||||
internal static MessagePump CreatePump()
|
||||
|
|
@ -124,31 +109,18 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
|
||||
internal static IServer CreateDynamicHttpServer(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
|
||||
{
|
||||
lock (PortLock)
|
||||
{
|
||||
while (NextPort < MaxPort)
|
||||
{
|
||||
var prefix = UrlPrefix.Create("http", "localhost", "0", basePath);
|
||||
|
||||
var port = NextPort++;
|
||||
var prefix = UrlPrefix.Create("http", "localhost", port, basePath);
|
||||
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
||||
baseAddress = prefix.ToString();
|
||||
var server = CreatePump();
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add(prefix.ToString());
|
||||
configureOptions(server.Listener.Options);
|
||||
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
|
||||
|
||||
var server = CreatePump();
|
||||
server.Features.Get<IServerAddressesFeature>().Addresses.Add(baseAddress);
|
||||
configureOptions(server.Listener.Options);
|
||||
try
|
||||
{
|
||||
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
|
||||
return server;
|
||||
}
|
||||
catch (HttpSysException)
|
||||
{
|
||||
}
|
||||
}
|
||||
NextPort = BasePort;
|
||||
}
|
||||
throw new Exception("Failed to locate a free port.");
|
||||
prefix = server.Listener.Options.UrlPrefixes.First(); // Has new port
|
||||
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
|
||||
baseAddress = prefix.ToString();
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app)
|
||||
|
|
@ -184,6 +156,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
throw new Exception("Failed to locate a free port.");
|
||||
}
|
||||
|
||||
|
||||
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
|
||||
|
||||
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
|
|||
internal static class ErrorCodes
|
||||
{
|
||||
internal const uint ERROR_SUCCESS = 0;
|
||||
internal const uint ERROR_ACCESS_DENIED = 5;
|
||||
internal const uint ERROR_SHARING_VIOLATION = 32;
|
||||
internal const uint ERROR_HANDLE_EOF = 38;
|
||||
internal const uint ERROR_NOT_SUPPORTED = 50;
|
||||
internal const uint ERROR_INVALID_PARAMETER = 87;
|
||||
|
|
|
|||
Loading…
Reference in New Issue