Add support for port 0 in HttpSys (#13841)

This commit is contained in:
Justin Kotalik 2019-09-12 07:43:53 +09:00 committed by GitHub
parent 71c5c66b21
commit 96f1c92caa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 125 additions and 109 deletions

View File

@ -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.");
}
}
}

View File

@ -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
{

View File

@ -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.

View File

@ -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)

View File

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

View File

@ -0,0 +1,3 @@
@ECHO OFF
%~dp0..\..\..\startvs.cmd %~dp0HttpSysServer.sln

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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;