Handle graceful shutdown from ANCM
This commit is contained in:
parent
a224b1a833
commit
5155456653
|
|
@ -2,12 +2,11 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -20,13 +19,17 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
private const string MSAspNetCoreClientCert = "MS-ASPNETCORE-CLIENTCERT";
|
private const string MSAspNetCoreClientCert = "MS-ASPNETCORE-CLIENTCERT";
|
||||||
private const string MSAspNetCoreToken = "MS-ASPNETCORE-TOKEN";
|
private const string MSAspNetCoreToken = "MS-ASPNETCORE-TOKEN";
|
||||||
|
private const string MSAspNetCoreEvent = "MS-ASPNETCORE-EVENT";
|
||||||
|
private const string ANCMShutdownEventHeaderValue = "shutdown";
|
||||||
|
private static readonly PathString ANCMRequestPath = new PathString("/iisintegration");
|
||||||
|
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly IISOptions _options;
|
private readonly IISOptions _options;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly string _pairingToken;
|
private readonly string _pairingToken;
|
||||||
|
private readonly IApplicationLifetime _applicationLifetime;
|
||||||
|
|
||||||
public IISMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<IISOptions> options, string pairingToken, IAuthenticationSchemeProvider authentication)
|
public IISMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<IISOptions> options, string pairingToken, IAuthenticationSchemeProvider authentication, IApplicationLifetime applicationLifetime)
|
||||||
{
|
{
|
||||||
if (next == null)
|
if (next == null)
|
||||||
{
|
{
|
||||||
|
|
@ -40,6 +43,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
if (applicationLifetime == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(applicationLifetime));
|
||||||
|
}
|
||||||
if (string.IsNullOrEmpty(pairingToken))
|
if (string.IsNullOrEmpty(pairingToken))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Missing or empty pairing token.");
|
throw new ArgumentException("Missing or empty pairing token.");
|
||||||
|
|
@ -54,6 +61,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
}
|
}
|
||||||
|
|
||||||
_pairingToken = pairingToken;
|
_pairingToken = pairingToken;
|
||||||
|
_applicationLifetime = applicationLifetime;
|
||||||
_logger = loggerFactory.CreateLogger<IISMiddleware>();
|
_logger = loggerFactory.CreateLogger<IISMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +70,18 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
if (!string.Equals(_pairingToken, httpContext.Request.Headers[MSAspNetCoreToken], StringComparison.Ordinal))
|
if (!string.Equals(_pairingToken, httpContext.Request.Headers[MSAspNetCoreToken], StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_logger.LogError($"'{MSAspNetCoreToken}' does not match the expected pairing token '{_pairingToken}', request rejected.");
|
_logger.LogError($"'{MSAspNetCoreToken}' does not match the expected pairing token '{_pairingToken}', request rejected.");
|
||||||
httpContext.Response.StatusCode = 400;
|
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle shutdown from ANCM
|
||||||
|
if (HttpMethods.IsPost(httpContext.Request.Method) &&
|
||||||
|
httpContext.Request.Path.Equals(ANCMRequestPath) &&
|
||||||
|
string.Equals(ANCMShutdownEventHeaderValue, httpContext.Request.Headers[MSAspNetCoreEvent], StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Execute shutdown task on background thread without waiting for completion
|
||||||
|
var shutdownTask = Task.Run(() => _applicationLifetime.StopApplication());
|
||||||
|
httpContext.Response.StatusCode = StatusCodes.Status202Accepted;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
// 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.
|
// 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;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
|
@ -72,6 +74,165 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/", "/iisintegration", "shutdown")]
|
||||||
|
[InlineData("/", "/iisintegration", "Shutdown")]
|
||||||
|
[InlineData("/pathBase", "/pathBase/iisintegration", "shutdown")]
|
||||||
|
[InlineData("/pathBase", "/pathBase/iisintegration", "Shutdown")]
|
||||||
|
public async Task MiddlewareShutsdownGivenANCMShutdown(string pathBase, string requestPath, string shutdownEvent)
|
||||||
|
{
|
||||||
|
var requestExecuted = new ManualResetEvent(false);
|
||||||
|
var applicationStoppingFired = new ManualResetEvent(false);
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.UseSetting("TOKEN", "TestToken")
|
||||||
|
.UseSetting("PORT", "12345")
|
||||||
|
.UseSetting("APPL_PATH", pathBase)
|
||||||
|
.UseIISIntegration()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
var appLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
|
||||||
|
appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set());
|
||||||
|
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
requestExecuted.Set();
|
||||||
|
return Task.FromResult(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken");
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", shutdownEvent);
|
||||||
|
var response = await server.CreateClient().SendAsync(request);
|
||||||
|
|
||||||
|
Assert.True(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(5)));
|
||||||
|
Assert.False(requestExecuted.WaitOne(0));
|
||||||
|
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<HttpMethod> InvalidShutdownMethods
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<HttpMethod>
|
||||||
|
{
|
||||||
|
HttpMethod.Put,
|
||||||
|
HttpMethod.Trace,
|
||||||
|
HttpMethod.Head,
|
||||||
|
HttpMethod.Get,
|
||||||
|
HttpMethod.Delete,
|
||||||
|
HttpMethod.Options
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(InvalidShutdownMethods))]
|
||||||
|
public async Task MiddlewareIgnoresShutdownGivenWrongMethod(HttpMethod method)
|
||||||
|
{
|
||||||
|
var requestExecuted = new ManualResetEvent(false);
|
||||||
|
var applicationStoppingFired = new ManualResetEvent(false);
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.UseSetting("TOKEN", "TestToken")
|
||||||
|
.UseSetting("PORT", "12345")
|
||||||
|
.UseSetting("APPL_PATH", "/")
|
||||||
|
.UseIISIntegration()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
var appLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
|
||||||
|
appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set());
|
||||||
|
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
requestExecuted.Set();
|
||||||
|
return Task.FromResult(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(method, "/iisintegration");
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken");
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", "shutdown");
|
||||||
|
var response = await server.CreateClient().SendAsync(request);
|
||||||
|
|
||||||
|
Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1)));
|
||||||
|
Assert.True(requestExecuted.WaitOne(0));
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/")]
|
||||||
|
[InlineData("/path")]
|
||||||
|
[InlineData("/path/iisintegration")]
|
||||||
|
public async Task MiddlewareIgnoresShutdownGivenWrongPath(string path)
|
||||||
|
{
|
||||||
|
var requestExecuted = new ManualResetEvent(false);
|
||||||
|
var applicationStoppingFired = new ManualResetEvent(false);
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.UseSetting("TOKEN", "TestToken")
|
||||||
|
.UseSetting("PORT", "12345")
|
||||||
|
.UseSetting("APPL_PATH", "/")
|
||||||
|
.UseIISIntegration()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
var appLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
|
||||||
|
appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set());
|
||||||
|
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
requestExecuted.Set();
|
||||||
|
return Task.FromResult(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, path);
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken");
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", "shutdown");
|
||||||
|
var response = await server.CreateClient().SendAsync(request);
|
||||||
|
|
||||||
|
Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1)));
|
||||||
|
Assert.True(requestExecuted.WaitOne(0));
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("event")]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(null)]
|
||||||
|
public async Task MiddlewareIgnoresShutdownGivenWrongEvent(string shutdownEvent)
|
||||||
|
{
|
||||||
|
var requestExecuted = new ManualResetEvent(false);
|
||||||
|
var applicationStoppingFired = new ManualResetEvent(false);
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.UseSetting("TOKEN", "TestToken")
|
||||||
|
.UseSetting("PORT", "12345")
|
||||||
|
.UseSetting("APPL_PATH", "/")
|
||||||
|
.UseIISIntegration()
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
var appLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
|
||||||
|
appLifetime.ApplicationStopping.Register(() => applicationStoppingFired.Set());
|
||||||
|
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
requestExecuted.Set();
|
||||||
|
return Task.FromResult(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var server = new TestServer(builder);
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, "/iisintegration");
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-TOKEN", "TestToken");
|
||||||
|
request.Headers.TryAddWithoutValidation("MS-ASPNETCORE-EVENT", shutdownEvent);
|
||||||
|
var response = await server.CreateClient().SendAsync(request);
|
||||||
|
|
||||||
|
Assert.False(applicationStoppingFired.WaitOne(TimeSpan.FromSeconds(1)));
|
||||||
|
Assert.True(requestExecuted.WaitOne(0));
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UrlDelayRegisteredAndPreferHostingUrlsSet()
|
public void UrlDelayRegisteredAndPreferHostingUrlsSet()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue