From 306ab78b886cc55515b4c499840021d1463778de Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 21 Jun 2018 16:35:11 -0700 Subject: [PATCH] Fix App_offline and start adding tests (#953) --- .../AspNetCore/src/applicationmanager.cxx | 2 + .../inprocessapplication.cpp | 8 +- .../outofprocess/outprocessapplication.cpp | 4 +- .../Inprocess/AppOfflineTests.cs | 135 ++++++++++++++++++ .../Inprocess/StartupTests.cs | 1 - .../Inprocess/TestServerTest.cs | 2 +- 6 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 test/IISIntegration.FunctionalTests/Inprocess/AppOfflineTests.cs diff --git a/src/AspNetCoreModuleV2/AspNetCore/src/applicationmanager.cxx b/src/AspNetCoreModuleV2/AspNetCore/src/applicationmanager.cxx index 45e0ae3b68..9690dcb2fd 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/src/applicationmanager.cxx +++ b/src/AspNetCoreModuleV2/AspNetCore/src/applicationmanager.cxx @@ -118,6 +118,8 @@ APPLICATION_MANAGER::GetOrCreateApplicationInfo( } *ppApplicationInfo = pApplicationInfo; + pApplicationInfo->StartMonitoringAppOffline(); + pApplicationInfo = NULL; } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 4b214457e7..0d5ce7670a 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -232,14 +232,18 @@ IN_PROCESS_APPLICATION::Recycle( if (!m_pHttpServer->IsCommandLineLaunch()) { // IIS scenario. - // notify IIS first so that new request will be routed to new worker process + // We don't actually handle any shutdown logic here. + // Instead, we notify IIS that the process needs to be recycled, which will call + // ApplicationManager->Shutdown(). This will call shutdown on the application. m_pHttpServer->RecycleProcess(L"AspNetCore InProcess Recycle Process on Demand"); } else { // IISExpress scenario - // Shutdown the managed application and call exit to terminate current process + // Try to graceful shutdown the managed application + // and call exit to terminate current process ShutDown(); + exit(0); } } diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outofprocess/outprocessapplication.cpp b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outofprocess/outprocessapplication.cpp index 356029efd3..0457b4d552 100644 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outofprocess/outprocessapplication.cpp +++ b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outofprocess/outprocessapplication.cpp @@ -118,8 +118,8 @@ OUT_OF_PROCESS_APPLICATION::SetWebsocketStatus( IHttpContext* pHttpContext ) { - // Even though the applicationhost.config file contains the websocket element, - // the websocket module may still not be enabled. + // Even though the applicationhost.config file contains the websocket element, + // the websocket module may still not be enabled. PCWSTR pszTempWebsocketValue; DWORD cbLength; HRESULT hr; diff --git a/test/IISIntegration.FunctionalTests/Inprocess/AppOfflineTests.cs b/test/IISIntegration.FunctionalTests/Inprocess/AppOfflineTests.cs new file mode 100644 index 0000000000..b5ac2758e0 --- /dev/null +++ b/test/IISIntegration.FunctionalTests/Inprocess/AppOfflineTests.cs @@ -0,0 +1,135 @@ +// 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.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using IISIntegration.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace IISIntegration.FunctionalTests.Inprocess +{ + [SkipIfIISExpressSchemaMissingInProcess] + public class AppOfflineTests : IISFunctionalTestBase + { + [ConditionalFact] + public async Task AppOfflineDroppedWhileSiteIsDown_SiteReturns503() + { + var deploymentResult = await DeployApp(); + + AddAppOffline(deploymentResult.DeploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + } + + [ConditionalFact] + public async Task AppOfflineDroppedWhileSiteIsDown_CustomResponse() + { + var expectedResponse = "The app is offline."; + var deploymentResult = await DeployApp(); + + AddAppOffline(deploymentResult.DeploymentResult.ContentRoot, expectedResponse); + + await AssertAppOffline(deploymentResult, expectedResponse); + } + + [ConditionalFact] + public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown() + { + var deploymentResult = await AssertStarts(); + + AddAppOffline(deploymentResult.DeploymentResult.ContentRoot); + + await AssertStopsProcess(deploymentResult); + } + + [ConditionalFact] + public async Task AppOfflineDropped_CanRemoveAppOfflineAfterAddingAndSiteWorks() + { + var deploymentResult = await DeployApp(); + + AddAppOffline(deploymentResult.DeploymentResult.ContentRoot); + + await AssertAppOffline(deploymentResult); + + RemoveAppOffline(deploymentResult.DeploymentResult.ContentRoot); + + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + } + + private async Task DeployApp() + { + var deploymentParameters = Helpers.GetBaseDeploymentParameters(); + + return await DeployAsync(deploymentParameters); + } + + private void AddAppOffline(string appPath, string content = "") + { + File.WriteAllText(Path.Combine(appPath, "app_offline.htm"), content); + } + + private void RemoveAppOffline(string appPath) + { + RetryHelper.RetryOperation( + () => File.Delete(Path.Combine(appPath, "app_offline.htm")), + e => Logger.LogError($"Failed to remove app_offline : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: 100); + } + + private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "") + { + var response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + + for (var i = 0; response.IsSuccessStatusCode && i < 5; i++) + { + // Keep retrying until app_offline is present. + response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); + } + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + + Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync()); + } + + private async Task AssertStopsProcess(IISDeploymentResult deploymentResult) + { + try + { + var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld"); + } + catch (HttpRequestException) + { + // dropping app_offline will kill the process + } + + var hostShutdownToken = deploymentResult.DeploymentResult.HostShutdownToken; + + Assert.True(hostShutdownToken.WaitHandle.WaitOne(millisecondsTimeout: 1000)); + Assert.True(hostShutdownToken.IsCancellationRequested); + } + + private async Task AssertStarts() + { + var deploymentParameters = Helpers.GetBaseDeploymentParameters(); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld"); + + var responseText = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello World", responseText); + + return deploymentResult; + } + } +} diff --git a/test/IISIntegration.FunctionalTests/Inprocess/StartupTests.cs b/test/IISIntegration.FunctionalTests/Inprocess/StartupTests.cs index 2f36110c90..68fac580db 100644 --- a/test/IISIntegration.FunctionalTests/Inprocess/StartupTests.cs +++ b/test/IISIntegration.FunctionalTests/Inprocess/StartupTests.cs @@ -95,7 +95,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests Assert.Equal("Hello World", responseText); } - public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.IISExpress) .WithTfms(Tfm.NetCoreApp22) diff --git a/test/IISIntegration.FunctionalTests/Inprocess/TestServerTest.cs b/test/IISIntegration.FunctionalTests/Inprocess/TestServerTest.cs index b48611b2fa..9c9f9918bb 100644 --- a/test/IISIntegration.FunctionalTests/Inprocess/TestServerTest.cs +++ b/test/IISIntegration.FunctionalTests/Inprocess/TestServerTest.cs @@ -22,7 +22,7 @@ namespace IISIntegration.FunctionalTests.Inprocess [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, "https://github.com/aspnet/IISIntegration/issues/866")] - public async Task Test() + public async Task SingleProcessTestServer_HelloWorld() { var helloWorld = "Hello World"; var expectedPath = "/Path";