Skip to content

Commit 3852c5c

Browse files
committed
Remove extra public api
1 parent b43797b commit 3852c5c

File tree

4 files changed

+421
-354
lines changed

4 files changed

+421
-354
lines changed
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO.Pipelines;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Connections;
11+
using Microsoft.AspNetCore.Connections.Experimental;
12+
using Microsoft.AspNetCore.Hosting.Server;
13+
using Microsoft.AspNetCore.Hosting.Server.Features;
14+
using Microsoft.AspNetCore.Http.Features;
15+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
16+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
17+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
18+
using Microsoft.Extensions.Logging;
19+
using Microsoft.Extensions.Options;
20+
using Microsoft.Extensions.Primitives;
21+
22+
namespace Microsoft.AspNetCore.Server.Kestrel.Core
23+
{
24+
internal class KestrelServerImpl : IServer
25+
{
26+
private readonly ServerAddressesFeature _serverAddresses;
27+
private readonly TransportManager _transportManager;
28+
private readonly IConnectionListenerFactory _transportFactory;
29+
private readonly IMultiplexedConnectionListenerFactory _multiplexedTransportFactory;
30+
31+
private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1);
32+
private bool _hasStarted;
33+
private int _stopping;
34+
private readonly CancellationTokenSource _stopCts = new CancellationTokenSource();
35+
private readonly TaskCompletionSource _stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
36+
37+
private IDisposable _configChangedRegistration;
38+
39+
public KestrelServerImpl(
40+
IOptions<KestrelServerOptions> options,
41+
IEnumerable<IConnectionListenerFactory> transportFactories,
42+
ILoggerFactory loggerFactory)
43+
: this(transportFactories, null, CreateServiceContext(options, loggerFactory))
44+
{
45+
}
46+
47+
public KestrelServerImpl(
48+
IOptions<KestrelServerOptions> options,
49+
IEnumerable<IConnectionListenerFactory> transportFactories,
50+
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
51+
ILoggerFactory loggerFactory)
52+
: this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory))
53+
{
54+
}
55+
56+
// For testing
57+
internal KestrelServerImpl(IEnumerable<IConnectionListenerFactory> transportFactories, ServiceContext serviceContext)
58+
: this(transportFactories, null, serviceContext)
59+
{
60+
}
61+
62+
// For testing
63+
internal KestrelServerImpl(
64+
IEnumerable<IConnectionListenerFactory> transportFactories,
65+
IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
66+
ServiceContext serviceContext)
67+
{
68+
if (transportFactories == null)
69+
{
70+
throw new ArgumentNullException(nameof(transportFactories));
71+
}
72+
73+
_transportFactory = transportFactories?.LastOrDefault();
74+
_multiplexedTransportFactory = multiplexedFactories?.LastOrDefault();
75+
76+
if (_transportFactory == null && _multiplexedTransportFactory == null)
77+
{
78+
throw new InvalidOperationException(CoreStrings.TransportNotFound);
79+
}
80+
81+
ServiceContext = serviceContext;
82+
83+
Features = new FeatureCollection();
84+
_serverAddresses = new ServerAddressesFeature();
85+
Features.Set<IServerAddressesFeature>(_serverAddresses);
86+
87+
_transportManager = new TransportManager(_transportFactory, _multiplexedTransportFactory, ServiceContext);
88+
89+
HttpCharacters.Initialize();
90+
}
91+
92+
private static ServiceContext CreateServiceContext(IOptions<KestrelServerOptions> options, ILoggerFactory loggerFactory)
93+
{
94+
if (options == null)
95+
{
96+
throw new ArgumentNullException(nameof(options));
97+
}
98+
if (loggerFactory == null)
99+
{
100+
throw new ArgumentNullException(nameof(loggerFactory));
101+
}
102+
103+
var serverOptions = options.Value ?? new KestrelServerOptions();
104+
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel");
105+
var trace = new KestrelTrace(logger);
106+
var connectionManager = new ConnectionManager(
107+
trace,
108+
serverOptions.Limits.MaxConcurrentUpgradedConnections);
109+
110+
var heartbeatManager = new HeartbeatManager(connectionManager);
111+
var dateHeaderValueManager = new DateHeaderValueManager();
112+
113+
var heartbeat = new Heartbeat(
114+
new IHeartbeatHandler[] { dateHeaderValueManager, heartbeatManager },
115+
new SystemClock(),
116+
DebuggerWrapper.Singleton,
117+
trace);
118+
119+
return new ServiceContext
120+
{
121+
Log = trace,
122+
HttpParser = new HttpParser<Http1ParsingHandler>(trace.IsEnabled(LogLevel.Information)),
123+
Scheduler = PipeScheduler.ThreadPool,
124+
SystemClock = heartbeatManager,
125+
DateHeaderValueManager = dateHeaderValueManager,
126+
ConnectionManager = connectionManager,
127+
Heartbeat = heartbeat,
128+
ServerOptions = serverOptions,
129+
};
130+
}
131+
132+
public IFeatureCollection Features { get; }
133+
134+
public KestrelServerOptions Options => ServiceContext.ServerOptions;
135+
136+
private ServiceContext ServiceContext { get; }
137+
138+
private IKestrelTrace Trace => ServiceContext.Log;
139+
140+
private AddressBindContext AddressBindContext { get; set; }
141+
142+
public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
143+
{
144+
try
145+
{
146+
if (!BitConverter.IsLittleEndian)
147+
{
148+
throw new PlatformNotSupportedException(CoreStrings.BigEndianNotSupported);
149+
}
150+
151+
ValidateOptions();
152+
153+
if (_hasStarted)
154+
{
155+
// The server has already started and/or has not been cleaned up yet
156+
throw new InvalidOperationException(CoreStrings.ServerAlreadyStarted);
157+
}
158+
_hasStarted = true;
159+
160+
ServiceContext.Heartbeat?.Start();
161+
162+
async Task OnBind(ListenOptions options)
163+
{
164+
// INVESTIGATE: For some reason, MsQuic needs to bind before
165+
// sockets for it to successfully listen. It also seems racy.
166+
if ((options.Protocols & HttpProtocols.Http3) == HttpProtocols.Http3)
167+
{
168+
if (_multiplexedTransportFactory is null)
169+
{
170+
throw new InvalidOperationException($"Cannot start HTTP/3 server if no {nameof(IMultiplexedConnectionListenerFactory)} is registered.");
171+
}
172+
173+
options.UseHttp3Server(ServiceContext, application, options.Protocols);
174+
var multiplexedConnectionDelegate = ((IMultiplexedConnectionBuilder)options).Build();
175+
176+
// Add the connection limit middleware
177+
multiplexedConnectionDelegate = EnforceConnectionLimit(multiplexedConnectionDelegate, Options.Limits.MaxConcurrentConnections, Trace);
178+
179+
options.EndPoint = await _transportManager.BindAsync(options.EndPoint, multiplexedConnectionDelegate, options.EndpointConfig).ConfigureAwait(false);
180+
}
181+
182+
// Add the HTTP middleware as the terminal connection middleware
183+
if ((options.Protocols & HttpProtocols.Http1) == HttpProtocols.Http1
184+
|| (options.Protocols & HttpProtocols.Http2) == HttpProtocols.Http2
185+
|| options.Protocols == HttpProtocols.None) // TODO a test fails because it doesn't throw an exception in the right place
186+
// when there is no HttpProtocols in KestrelServer, can we remove/change the test?
187+
{
188+
if (_transportFactory is null)
189+
{
190+
throw new InvalidOperationException($"Cannot start HTTP/1.x or HTTP/2 server if no {nameof(IConnectionListenerFactory)} is registered.");
191+
}
192+
193+
options.UseHttpServer(ServiceContext, application, options.Protocols);
194+
var connectionDelegate = options.Build();
195+
196+
// Add the connection limit middleware
197+
connectionDelegate = EnforceConnectionLimit(connectionDelegate, Options.Limits.MaxConcurrentConnections, Trace);
198+
199+
options.EndPoint = await _transportManager.BindAsync(options.EndPoint, connectionDelegate, options.EndpointConfig).ConfigureAwait(false);
200+
}
201+
}
202+
203+
AddressBindContext = new AddressBindContext
204+
{
205+
ServerAddressesFeature = _serverAddresses,
206+
ServerOptions = Options,
207+
Logger = Trace,
208+
CreateBinding = OnBind,
209+
};
210+
211+
await BindAsync(cancellationToken).ConfigureAwait(false);
212+
}
213+
catch (Exception ex)
214+
{
215+
Trace.LogCritical(0, ex, "Unable to start Kestrel.");
216+
Dispose();
217+
throw;
218+
}
219+
}
220+
221+
// Graceful shutdown if possible
222+
public async Task StopAsync(CancellationToken cancellationToken)
223+
{
224+
if (Interlocked.Exchange(ref _stopping, 1) == 1)
225+
{
226+
await _stoppedTcs.Task.ConfigureAwait(false);
227+
return;
228+
}
229+
230+
_stopCts.Cancel();
231+
232+
// Don't use cancellationToken when acquiring the semaphore. Dispose calls this with a pre-canceled token.
233+
await _bindSemaphore.WaitAsync().ConfigureAwait(false);
234+
235+
try
236+
{
237+
await _transportManager.StopAsync(cancellationToken).ConfigureAwait(false);
238+
}
239+
catch (Exception ex)
240+
{
241+
_stoppedTcs.TrySetException(ex);
242+
throw;
243+
}
244+
finally
245+
{
246+
ServiceContext.Heartbeat?.Dispose();
247+
_configChangedRegistration?.Dispose();
248+
_stopCts.Dispose();
249+
_bindSemaphore.Release();
250+
}
251+
252+
_stoppedTcs.TrySetResult();
253+
}
254+
255+
// Ungraceful shutdown
256+
public void Dispose()
257+
{
258+
StopAsync(new CancellationToken(canceled: true)).GetAwaiter().GetResult();
259+
}
260+
261+
private async Task BindAsync(CancellationToken cancellationToken)
262+
{
263+
await _bindSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
264+
265+
try
266+
{
267+
if (_stopping == 1)
268+
{
269+
throw new InvalidOperationException("Kestrel has already been stopped.");
270+
}
271+
272+
IChangeToken reloadToken = null;
273+
274+
_serverAddresses.InternalCollection.PreventPublicMutation();
275+
276+
if (Options.ConfigurationLoader?.ReloadOnChange == true && (!_serverAddresses.PreferHostingUrls || _serverAddresses.InternalCollection.Count == 0))
277+
{
278+
reloadToken = Options.ConfigurationLoader.Configuration.GetReloadToken();
279+
}
280+
281+
Options.ConfigurationLoader?.Load();
282+
283+
await AddressBinder.BindAsync(Options.ListenOptions, AddressBindContext).ConfigureAwait(false);
284+
_configChangedRegistration = reloadToken?.RegisterChangeCallback(async state => await ((KestrelServerImpl)state).RebindAsync(), this);
285+
}
286+
finally
287+
{
288+
_bindSemaphore.Release();
289+
}
290+
}
291+
292+
private async Task RebindAsync()
293+
{
294+
await _bindSemaphore.WaitAsync();
295+
296+
IChangeToken reloadToken = null;
297+
298+
try
299+
{
300+
if (_stopping == 1)
301+
{
302+
return;
303+
}
304+
305+
reloadToken = Options.ConfigurationLoader.Configuration.GetReloadToken();
306+
var (endpointsToStop, endpointsToStart) = Options.ConfigurationLoader.Reload();
307+
308+
Trace.LogDebug("Config reload token fired. Checking for changes...");
309+
310+
if (endpointsToStop.Count > 0)
311+
{
312+
var urlsToStop = endpointsToStop.Select(lo => lo.EndpointConfig.Url ?? "<unknown>");
313+
Trace.LogInformation("Config changed. Stopping the following endpoints: '{endpoints}'", string.Join("', '", urlsToStop));
314+
315+
// 5 is the default value for WebHost's "shutdownTimeoutSeconds", so use that.
316+
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
317+
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(_stopCts.Token, timeoutCts.Token);
318+
319+
// TODO: It would be nice to start binding to new endpoints immediately and reconfigured endpoints as soon
320+
// as the unbinding finished for the given endpoint rather than wait for all transports to unbind first.
321+
var configsToStop = endpointsToStop.Select(lo => lo.EndpointConfig).ToList();
322+
await _transportManager.StopEndpointsAsync(configsToStop, combinedCts.Token).ConfigureAwait(false);
323+
324+
foreach (var listenOption in endpointsToStop)
325+
{
326+
Options.OptionsInUse.Remove(listenOption);
327+
_serverAddresses.InternalCollection.Remove(listenOption.GetDisplayName());
328+
}
329+
}
330+
331+
if (endpointsToStart.Count > 0)
332+
{
333+
var urlsToStart = endpointsToStart.Select(lo => lo.EndpointConfig.Url ?? "<unknown>");
334+
Trace.LogInformation("Config changed. Starting the following endpoints: '{endpoints}'", string.Join("', '", urlsToStart));
335+
336+
foreach (var listenOption in endpointsToStart)
337+
{
338+
try
339+
{
340+
// TODO: This should probably be canceled by the _stopCts too, but we don't currently support bind cancellation even in StartAsync().
341+
await listenOption.BindAsync(AddressBindContext).ConfigureAwait(false);
342+
}
343+
catch (Exception ex)
344+
{
345+
Trace.LogCritical(0, ex, "Unable to bind to '{url}' on config reload.", listenOption.EndpointConfig.Url ?? "<unknown>");
346+
}
347+
}
348+
}
349+
}
350+
catch (Exception ex)
351+
{
352+
Trace.LogCritical(0, ex, "Unable to reload configuration.");
353+
}
354+
finally
355+
{
356+
_configChangedRegistration = reloadToken?.RegisterChangeCallback(async state => await ((KestrelServerImpl)state).RebindAsync(), this);
357+
_bindSemaphore.Release();
358+
}
359+
}
360+
361+
private void ValidateOptions()
362+
{
363+
if (Options.Limits.MaxRequestBufferSize.HasValue &&
364+
Options.Limits.MaxRequestBufferSize < Options.Limits.MaxRequestLineSize)
365+
{
366+
throw new InvalidOperationException(
367+
CoreStrings.FormatMaxRequestBufferSmallerThanRequestLineBuffer(Options.Limits.MaxRequestBufferSize.Value, Options.Limits.MaxRequestLineSize));
368+
}
369+
370+
if (Options.Limits.MaxRequestBufferSize.HasValue &&
371+
Options.Limits.MaxRequestBufferSize < Options.Limits.MaxRequestHeadersTotalSize)
372+
{
373+
throw new InvalidOperationException(
374+
CoreStrings.FormatMaxRequestBufferSmallerThanRequestHeaderBuffer(Options.Limits.MaxRequestBufferSize.Value, Options.Limits.MaxRequestHeadersTotalSize));
375+
}
376+
}
377+
378+
private static ConnectionDelegate EnforceConnectionLimit(ConnectionDelegate innerDelegate, long? connectionLimit, IKestrelTrace trace)
379+
{
380+
if (!connectionLimit.HasValue)
381+
{
382+
return innerDelegate;
383+
}
384+
385+
return new ConnectionLimitMiddleware<ConnectionContext>(c => innerDelegate(c), connectionLimit.Value, trace).OnConnectionAsync;
386+
}
387+
388+
private static MultiplexedConnectionDelegate EnforceConnectionLimit(MultiplexedConnectionDelegate innerDelegate, long? connectionLimit, IKestrelTrace trace)
389+
{
390+
if (!connectionLimit.HasValue)
391+
{
392+
return innerDelegate;
393+
}
394+
395+
return new ConnectionLimitMiddleware<MultiplexedConnectionContext>(c => innerDelegate(c), connectionLimit.Value, trace).OnConnectionAsync;
396+
}
397+
}
398+
}

0 commit comments

Comments
 (0)