diff --git a/src/AspNetCoreModuleV2/CommonLib/debugutil.cpp b/src/AspNetCoreModuleV2/CommonLib/debugutil.cpp index 4e9e73a936..e31351a764 100644 --- a/src/AspNetCoreModuleV2/CommonLib/debugutil.cpp +++ b/src/AspNetCoreModuleV2/CommonLib/debugutil.cpp @@ -47,8 +47,9 @@ PrintDebugHeader() RETURN_LAST_ERROR_IF(!VerQueryValue(verData.data(), _T("\\StringFileInfo\\040904b0\\FileDescription"), &pvProductName, &iProductNameLen)); // Major, minor are stored in dwFileVersionMS field and patch, build in dwFileVersionLS field as pair of 32 bit numbers - DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, "Initializing logs for %S. File Version: %d.%d.%d.%d. Description: %S", + DebugPrintf(ASPNETCORE_DEBUG_FLAG_INFO, "Initializing logs for %S. ProcessId: %d. File Version: %d.%d.%d.%d. Description: %S", path, + GetCurrentProcessId(), ( verInfo->dwFileVersionMS >> 16 ) & 0xffff, ( verInfo->dwFileVersionMS >> 0 ) & 0xffff, ( verInfo->dwFileVersionLS >> 16 ) & 0xffff, diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs index e257ed00a7..54efe1f3fe 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeployer.cs @@ -123,47 +123,61 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS private void GetLogsFromFile() { - // Handle cases where debug file is redirected by test - var debugLogLocations = new List(); - if (IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile")) + try { - debugLogLocations.Add(IISDeploymentParameters.HandlerSettings["debugFile"]); - } - if (DeploymentParameters.EnvironmentVariables.ContainsKey("ASPNETCORE_MODULE_DEBUG_FILE")) - { - debugLogLocations.Add(DeploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG_FILE"]); - } - - // default debug file name - debugLogLocations.Add("aspnetcore-debug.log"); - - foreach (var debugLogLocation in debugLogLocations) - { - if (string.IsNullOrEmpty(debugLogLocation)) + // Handle cases where debug file is redirected by test + var debugLogLocations = new List(); + if (IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile")) { - continue; + debugLogLocations.Add(IISDeploymentParameters.HandlerSettings["debugFile"]); } - var file = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, debugLogLocation); - if (File.Exists(file)) + if (DeploymentParameters.EnvironmentVariables.ContainsKey("ASPNETCORE_MODULE_DEBUG_FILE")) { - var lines = File.ReadAllLines(file); - if (!lines.Any()) + debugLogLocations.Add(DeploymentParameters.EnvironmentVariables["ASPNETCORE_MODULE_DEBUG_FILE"]); + } + + // default debug file name + debugLogLocations.Add("aspnetcore-debug.log"); + + foreach (var debugLogLocation in debugLogLocations) + { + if (string.IsNullOrEmpty(debugLogLocation)) { - Logger.LogInformation("Debug log file found but was empty"); continue; } - foreach (var line in lines) + var file = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, debugLogLocation); + if (File.Exists(file)) { - Logger.LogInformation(line); + var lines = File.ReadAllLines(file); + if (!lines.Any()) + { + Logger.LogInformation($"Debug log file {file} found but was empty"); + continue; + } + + foreach (var line in lines) + { + Logger.LogInformation(line); + } + return; } - return; + } + // ANCM V1 does not support logs + if (DeploymentParameters.AncmVersion == AncmVersion.AspNetCoreModuleV2) + { + throw new InvalidOperationException($"Unable to find non-empty debug log files. Tried: {string.Join(", ", debugLogLocations)}"); + } + } + finally + { + if (File.Exists(_debugLogFile)) + { + File.Delete(_debugLogFile); } } - - throw new InvalidOperationException($"Not able to find non-empty debug log files. Tried: {string.Join(", ", debugLogLocations)}"); } public void StartIIS(Uri uri, string contentRoot) @@ -339,6 +353,12 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS throw new InvalidOperationException("Site not stopped yet"); } + if (appPool.WorkerProcesses.Any(wp => wp.State == WorkerProcessState.Running || + wp.State == WorkerProcessState.Stopping)) + { + throw new InvalidOperationException("WorkerProcess not stopped yet"); + } + if (!HostProcess.HasExited) { throw new InvalidOperationException("Site is stopped but host process is not"); diff --git a/test/Common.FunctionalTests/AppOfflineTests.cs b/test/Common.FunctionalTests/AppOfflineTests.cs index 3e1580fcb9..41bf8874d9 100644 --- a/test/Common.FunctionalTests/AppOfflineTests.cs +++ b/test/Common.FunctionalTests/AppOfflineTests.cs @@ -82,7 +82,6 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests } [ConditionalFact] - [RequiresIIS(IISCapability.ShutdownToken)] public async Task AppOfflineDroppedWhileSiteFailedToStartInRequestHandler_SiteStops_InProcess() { var deploymentResult = await DeployApp(HostingModel.InProcess); @@ -95,7 +94,8 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests Assert.Contains("500.30", await result.Content.ReadAsStringAsync()); AddAppOffline(deploymentResult.ContentRoot); - AssertStopsProcess(deploymentResult); + + await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); } [ConditionalFact] @@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests } catch { - AssertStopsProcess(deploymentResult); + deploymentResult.AssertWorkerProcessStop(); return; } } @@ -139,18 +139,16 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests } [ConditionalFact] - [RequiresIIS(IISCapability.ShutdownToken)] public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess() { var deploymentResult = await AssertStarts(HostingModel.InProcess); AddAppOffline(deploymentResult.ContentRoot); - AssertStopsProcess(deploymentResult); + await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); } [ConditionalFact] - [RequiresIIS(IISCapability.ShutdownToken)] public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_OutOfProcess() { var deploymentResult = await AssertStarts(HostingModel.OutOfProcess); @@ -245,28 +243,10 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "The app is offline.") { - HttpResponseMessage response = null; - - for (var i = 0; i < 5 && response?.StatusCode != HttpStatusCode.ServiceUnavailable; i++) - { - // Keep retrying until app_offline is present. - response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); - await Task.Delay(RetryDelay); - } - - Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); - + var response = await deploymentResult.HttpClient.RetryRequestAsync("HelloWorld", r => r.StatusCode == HttpStatusCode.ServiceUnavailable); Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync()); } - private void AssertStopsProcess(IISDeploymentResult deploymentResult) - { - var hostShutdownToken = deploymentResult.HostShutdownToken; - - Assert.True(hostShutdownToken.WaitHandle.WaitOne(TimeoutExtensions.DefaultTimeout)); - Assert.True(hostShutdownToken.IsCancellationRequested); - } - private async Task AssertStarts(HostingModel hostingModel) { var deploymentResult = await DeployApp(hostingModel); @@ -278,14 +258,7 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests private static async Task AssertRunning(IISDeploymentResult deploymentResult) { - HttpResponseMessage response = null; - - for (var i = 0; i < 5 && response?.IsSuccessStatusCode != true; i++) - { - response = await deploymentResult.HttpClient.GetAsync("HelloWorld"); - await Task.Delay(RetryDelay); - } - + var response = await deploymentResult.HttpClient.RetryRequestAsync("HelloWorld", r => r.IsSuccessStatusCode); var responseText = await response.Content.ReadAsStringAsync(); Assert.Equal("Hello World", responseText); } diff --git a/test/Common.FunctionalTests/ConfigurationChangeTests.cs b/test/Common.FunctionalTests/ConfigurationChangeTests.cs new file mode 100644 index 0000000000..b809bf42ca --- /dev/null +++ b/test/Common.FunctionalTests/ConfigurationChangeTests.cs @@ -0,0 +1,58 @@ +// 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.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ConfigurationChangeTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ConfigurationChangeTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ConfigurationChangeStopsInProcess() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.InProcess, publish: true); + + var deploymentResult = await DeployAsync(deploymentParameters); + + await deploymentResult.AssertStarts(); + + // Just "touching" web.config should be enough + deploymentResult.ModifyWebConfig(element => {}); + + await deploymentResult.AssertRecycledAsync(); + } + + [ConditionalTheory] + [InlineData(AncmVersion.AspNetCoreModule)] + [InlineData(AncmVersion.AspNetCoreModuleV2)] + public async Task ConfigurationChangeForcesChildProcessRestart(AncmVersion version) + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(HostingModel.OutOfProcess, publish: true); + deploymentParameters.AncmVersion = version; + + var deploymentResult = await DeployAsync(deploymentParameters); + + var processBefore = await deploymentResult.HttpClient.GetStringAsync("/ProcessId"); + + // Just "touching" web.config should be enough + deploymentResult.ModifyWebConfig(element => {}); + + // Have to retry here to allow ANCM to receive notification and react to it + // Verify that worker process gets restarted with new process id + await deploymentResult.HttpClient.RetryRequestAsync("/ProcessId", async r => await r.Content.ReadAsStringAsync() != processBefore); + } + } +} diff --git a/test/Common.FunctionalTests/Utilities/Helpers.cs b/test/Common.FunctionalTests/Utilities/Helpers.cs index 2781da1944..30c597a111 100644 --- a/test/Common.FunctionalTests/Utilities/Helpers.cs +++ b/test/Common.FunctionalTests/Utilities/Helpers.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Xml.Linq; @@ -18,6 +17,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { public static class Helpers { + private static readonly TimeSpan RetryRequestDelay = TimeSpan.FromMilliseconds(100); + private static readonly int RetryRequestCount = 5; + public static string GetTestWebSitePath(string name) { return Path.Combine(TestPathUtilities.GetSolutionRootDirectory("IISIntegration"),"test", "WebSites", name); @@ -26,9 +28,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests public static string GetInProcessTestSitesPath() => GetTestWebSitePath("InProcessWebSite"); public static string GetOutOfProcessTestSitesPath() => GetTestWebSitePath("OutOfProcessWebSite"); - - public static async Task AssertStarts(IISDeploymentResult deploymentResult, string path = "/HelloWorld") + public static async Task AssertStarts(this IISDeploymentResult deploymentResult, string path = "/HelloWorld") { var response = await deploymentResult.HttpClient.GetAsync(path); @@ -73,5 +74,64 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests fileInfo.CopyTo(destFileName); } } + + public static void ModifyWebConfig(this DeploymentResult deploymentResult, Action action) + { + var webConfigPath = Path.Combine(deploymentResult.ContentRoot, "web.config"); + var document = XDocument.Load(webConfigPath); + + document.Save(webConfigPath); + } + + public static Task RetryRequestAsync(this HttpClient client, string uri, Func predicate) + { + return RetryRequestAsync(client, uri, message => Task.FromResult(predicate(message))); + } + + public static async Task RetryRequestAsync(this HttpClient client, string uri, Func> predicate) + { + HttpResponseMessage response = await client.GetAsync(uri); + + for (var i = 0; i < RetryRequestCount && !await predicate(response); i++) + { + // Keep retrying until app_offline is present. + response = await client.GetAsync(uri); + await Task.Delay(RetryRequestDelay); + } + + if (!await predicate(response)) + { + throw new InvalidOperationException($"Didn't get response that satisfies predicate after {RetryRequestCount} retries"); + } + + return response; + } + + public static void AssertWorkerProcessStop(this IISDeploymentResult deploymentResult) + { + var hostProcess = deploymentResult.HostProcess; + Assert.True(hostProcess.WaitForExit((int)TimeoutExtensions.DefaultTimeout.TotalMilliseconds)); + + if (deploymentResult.DeploymentParameters.ServerType == ServerType.IISExpress) + { + Assert.Equal(0, hostProcess.ExitCode); + } + } + + + public static async Task AssertRecycledAsync(this IISDeploymentResult deploymentResult, Func verificationAction = null) + { + if (deploymentResult.DeploymentParameters.HostingModel != HostingModel.InProcess) + { + throw new NotSupportedException(); + } + + deploymentResult.AssertWorkerProcessStop(); + if (deploymentResult.DeploymentParameters.ServerType == ServerType.IIS) + { + verificationAction = verificationAction ?? (() => deploymentResult.AssertStarts()); + await verificationAction(); + } + } } }