diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/ProcessLoggingExtensions.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/ProcessLoggingExtensions.cs new file mode 100644 index 0000000000..d85b255a47 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/ProcessLoggingExtensions.cs @@ -0,0 +1,34 @@ +// 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 Microsoft.Extensions.Logging; + +namespace System.Diagnostics +{ + public static class ProcessLoggingExtensions + { + public static void StartAndCaptureOutAndErrToLogger(this Process process, string prefix, ILogger logger) + { + process.EnableRaisingEvents = true; + process.OutputDataReceived += (_, dataArgs) => + { + if (!string.IsNullOrEmpty(dataArgs.Data)) + { + logger.LogWarning($"{prefix} stdout: {{line}}", dataArgs.Data); + } + }; + + process.ErrorDataReceived += (_, dataArgs) => + { + if (!string.IsNullOrEmpty(dataArgs.Data)) + { + logger.LogWarning($"{prefix} stderr: {{line}}", dataArgs.Data); + } + }; + + process.Start(); + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TaskTimeoutExtensions.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TaskTimeoutExtensions.cs new file mode 100644 index 0000000000..2045f47f9a --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TaskTimeoutExtensions.cs @@ -0,0 +1,35 @@ +// 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. + +namespace System.Threading.Tasks +{ + internal static class TaskTimeoutExtensions + { + public static async Task OrTimeout(this Task task, TimeSpan timeout) + { + var completed = await Task.WhenAny(task, Task.Delay(timeout)); + if (completed == task) + { + // Manifest any exception + task.GetAwaiter().GetResult(); + } + else + { + throw new TimeoutException(); + } + } + + public static async Task OrTimeout(this Task task, TimeSpan timeout) + { + var completed = await Task.WhenAny(task, Task.Delay(timeout)); + if (completed == task) + { + return await task; + } + else + { + throw new TimeoutException(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs index 9aa7701f8a..9fff9f1c59 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Common/TestUriHelper.cs @@ -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; @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common { public static Uri BuildTestUri() { - return new UriBuilder("http", "localhost", FindFreePort()).Uri; + return new UriBuilder("http", "localhost", GetNextPort()).Uri; } public static Uri BuildTestUri(string hint) @@ -23,28 +23,28 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common else { var uriHint = new Uri(hint); - return new UriBuilder(uriHint) { Port = FindFreePort(uriHint.Port) }.Uri; + if (uriHint.Port == 0) + { + return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri; + } + else + { + return uriHint; + } } } - public static int FindFreePort() - { - return FindFreePort(0); - } - - public static int FindFreePort(int initialPort) + // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508 + public static int GetNextPort() { using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { - try - { - socket.Bind(new IPEndPoint(IPAddress.Loopback, initialPort)); - } - catch (SocketException) - { - socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - } - + // Let the OS assign the next available port. Unless we cycle through all ports + // on a test run, the OS will always increment the port number when making these calls. + // This prevents races in parallel test runs where a test is already bound to + // a given port, and a new test is able to bind to the same port due to port + // reuse being enabled by default by the OS. + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); return ((IPEndPoint)socket.LocalEndPoint).Port; } } diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployer.cs index 56a25424c7..542d904795 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployer.cs @@ -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; @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -34,69 +36,75 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting protected ILogger Logger { get; } - public abstract DeploymentResult Deploy(); + public abstract Task DeployAsync(); protected void DotnetPublish(string publishRoot = null) { - if (string.IsNullOrEmpty(DeploymentParameters.TargetFramework)) + using (Logger.BeginScope("dotnet-publish")) { - throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment"); + if (string.IsNullOrEmpty(DeploymentParameters.TargetFramework)) + { + throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment"); + } + + DeploymentParameters.PublishedApplicationRootPath = publishRoot ?? Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + var parameters = $"publish " + + $" --output \"{DeploymentParameters.PublishedApplicationRootPath}\"" + + $" --framework {DeploymentParameters.TargetFramework}" + + $" --configuration {DeploymentParameters.Configuration}" + + $" {DeploymentParameters.AdditionalPublishParameters}"; + + var startInfo = new ProcessStartInfo + { + FileName = DotnetCommandName, + Arguments = parameters, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + WorkingDirectory = DeploymentParameters.ApplicationPath, + }; + + AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.PublishEnvironmentVariables); + + var hostProcess = new Process() { StartInfo = startInfo }; + + Logger.LogInformation($"Executing command {DotnetCommandName} {parameters}"); + + hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", Logger); + + hostProcess.WaitForExit(); + + if (hostProcess.ExitCode != 0) + { + var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}"; + Logger.LogError(message); + throw new Exception(message); + } + + Logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}"); } - - DeploymentParameters.PublishedApplicationRootPath = publishRoot ?? Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - - var parameters = $"publish " - + $" --output \"{DeploymentParameters.PublishedApplicationRootPath}\"" - + $" --framework {DeploymentParameters.TargetFramework}" - + $" --configuration {DeploymentParameters.Configuration}" - + $" {DeploymentParameters.AdditionalPublishParameters}"; - - Logger.LogInformation($"Executing command {DotnetCommandName} {parameters}"); - - var startInfo = new ProcessStartInfo - { - FileName = DotnetCommandName, - Arguments = parameters, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - WorkingDirectory = DeploymentParameters.ApplicationPath, - }; - - AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.PublishEnvironmentVariables); - - var hostProcess = new Process() { StartInfo = startInfo }; - hostProcess.ErrorDataReceived += (sender, dataArgs) => { Logger.LogWarning(dataArgs.Data ?? string.Empty); }; - hostProcess.OutputDataReceived += (sender, dataArgs) => { Logger.LogInformation(dataArgs.Data ?? string.Empty); }; - hostProcess.Start(); - hostProcess.BeginErrorReadLine(); - hostProcess.BeginOutputReadLine(); - hostProcess.WaitForExit(); - - if (hostProcess.ExitCode != 0) - { - throw new Exception($"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}"); - } - - Logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}"); } protected void CleanPublishedOutput() { - if (DeploymentParameters.PreservePublishedApplicationForDebugging) + using (Logger.BeginScope("CleanPublishedOutput")) { - Logger.LogWarning( - "Skipping deleting the locally published folder as property " + - $"'{nameof(DeploymentParameters.PreservePublishedApplicationForDebugging)}' is set to 'true'."); - } - else - { - RetryHelper.RetryOperation( - () => Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, true), - e => Logger.LogWarning($"Failed to delete directory : {e.Message}"), - retryCount: 3, - retryDelayMilliseconds: 100); + if (DeploymentParameters.PreservePublishedApplicationForDebugging) + { + Logger.LogWarning( + "Skipping deleting the locally published folder as property " + + $"'{nameof(DeploymentParameters.PreservePublishedApplicationForDebugging)}' is set to 'true'."); + } + else + { + RetryHelper.RetryOperation( + () => Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, true), + e => Logger.LogWarning($"Failed to delete directory : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: 100); + } } } @@ -150,16 +158,19 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting protected void InvokeUserApplicationCleanup() { - if (DeploymentParameters.UserAdditionalCleanup != null) + using (Logger.BeginScope("UserAdditionalCleanup")) { - // User cleanup. - try + if (DeploymentParameters.UserAdditionalCleanup != null) { - DeploymentParameters.UserAdditionalCleanup(DeploymentParameters); - } - catch (Exception exception) - { - Logger.LogWarning("User cleanup code failed with exception : {exception}", exception.Message); + // User cleanup. + try + { + DeploymentParameters.UserAdditionalCleanup(DeploymentParameters); + } + catch (Exception exception) + { + Logger.LogWarning("User cleanup code failed with exception : {exception}", exception.Message); + } } } } diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployerFactory.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployerFactory.cs index 694671b495..31be63ea03 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployerFactory.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/ApplicationDeployerFactory.cs @@ -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; @@ -15,36 +15,31 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// Creates a deployer instance based on settings in . /// /// - /// + /// /// - public static IApplicationDeployer Create(DeploymentParameters deploymentParameters, ILogger logger) + public static IApplicationDeployer Create(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) { if (deploymentParameters == null) { throw new ArgumentNullException(nameof(deploymentParameters)); } - if (logger == null) + if (loggerFactory == null) { - throw new ArgumentNullException(nameof(logger)); + throw new ArgumentNullException(nameof(loggerFactory)); } switch (deploymentParameters.ServerType) { case ServerType.IISExpress: - return new IISExpressDeployer(deploymentParameters, logger); -#if NET46 + return new IISExpressDeployer(deploymentParameters, loggerFactory.CreateLogger()); case ServerType.IIS: - return new IISDeployer(deploymentParameters, logger); -#elif NETSTANDARD1_3 -#else -#error Target framework needs to be updated. -#endif + throw new NotSupportedException("The IIS deployer is no longer supported"); case ServerType.WebListener: case ServerType.Kestrel: - return new SelfHostDeployer(deploymentParameters, logger); + return new SelfHostDeployer(deploymentParameters, loggerFactory.CreateLogger()); case ServerType.Nginx: - return new NginxDeployer(deploymentParameters, logger); + return new NginxDeployer(deploymentParameters, loggerFactory.CreateLogger()); default: throw new NotSupportedException( string.Format("Found no deployers suitable for server type '{0}' with the current runtime.", diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IApplicationDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IApplicationDeployer.cs index 671d8b5ef8..400ae978ed 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IApplicationDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IApplicationDeployer.cs @@ -1,7 +1,8 @@ -// 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.Threading.Tasks; namespace Microsoft.AspNetCore.Server.IntegrationTesting { @@ -14,6 +15,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// Deploys the application to the target with specified . /// /// - DeploymentResult Deploy(); + Task DeployAsync(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISDeployer.cs deleted file mode 100644 index f5cc53bb6d..0000000000 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISDeployer.cs +++ /dev/null @@ -1,154 +0,0 @@ -// 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. - -#if NET46 - -using System; -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Server.IntegrationTesting.Common; -using Microsoft.Extensions.Logging; -using Microsoft.Web.Administration; - -namespace Microsoft.AspNetCore.Server.IntegrationTesting -{ - /// - /// Deployer for IIS. - /// - public class IISDeployer : ApplicationDeployer - { - private IISApplication _application; - private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource(); - private static object _syncObject = new object(); - - public IISDeployer(DeploymentParameters startParameters, ILogger logger) - : base(startParameters, logger) - { - } - - public override DeploymentResult Deploy() - { - // Start timer - StartTimer(); - - // Only supports publish and run on IIS. - DeploymentParameters.PublishApplicationBeforeDeployment = true; - - _application = new IISApplication(DeploymentParameters, Logger); - - if (DeploymentParameters.PublishApplicationBeforeDeployment) - { - DotnetPublish(publishRoot: _application.WebSiteRootFolder); - } - - // Drop a json file instead of setting environment variable. - SetAspEnvironmentWithJson(); - - var uri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); - - lock (_syncObject) - { - // To prevent modifying the IIS setup concurrently. - _application.Deploy(uri); - } - - // Warm up time for IIS setup. - Thread.Sleep(1 * 1000); - Logger.LogInformation("Successfully finished IIS application directory setup."); - - return new DeploymentResult - { - ContentRoot = DeploymentParameters.PublishedApplicationRootPath, - DeploymentParameters = DeploymentParameters, - // Accomodate the vdir name. - ApplicationBaseUri = uri.ToString(), - HostShutdownToken = _hostShutdownToken.Token - }; - } - - public override void Dispose() - { - if (_application != null) - { - lock (_syncObject) - { - // Sequentialize IIS operations. - _application.StopAndDeleteAppPool(); - } - - TriggerHostShutdown(_hostShutdownToken); - - Thread.Sleep(TimeSpan.FromSeconds(3)); - } - - CleanPublishedOutput(); - InvokeUserApplicationCleanup(); - - StopTimer(); - } - - private void SetAspEnvironmentWithJson() - { - ////S Drop a hosting.json with environment information. - // Logger.LogInformation("Creating hosting.json file with environment information."); - // var jsonFile = Path.Combine(DeploymentParameters.ApplicationPath, "hosting.json"); - // File.WriteAllText(jsonFile, string.Format("{ \"environment\":\"{0}\" }", DeploymentParameters.EnvironmentName)); - } - - private class IISApplication - { - private readonly ServerManager _serverManager = new ServerManager(); - private readonly DeploymentParameters _deploymentParameters; - private readonly ILogger _logger; - - public IISApplication(DeploymentParameters deploymentParameters, ILogger logger) - { - _deploymentParameters = deploymentParameters; - _logger = logger; - WebSiteName = CreateTestSiteName(); - } - - public string WebSiteName { get; } - - public string WebSiteRootFolder => $"{Environment.GetEnvironmentVariable("SystemDrive")}\\inetpub\\{WebSiteName}"; - - public void Deploy(Uri uri) - { - var contentRoot = _deploymentParameters.PublishApplicationBeforeDeployment ? _deploymentParameters.PublishedApplicationRootPath : _deploymentParameters.ApplicationPath; - _serverManager.Sites.Add(WebSiteName, contentRoot, uri.Port); - _serverManager.CommitChanges(); - } - - public void StopAndDeleteAppPool() - { - if (string.IsNullOrEmpty(WebSiteName)) - { - return; - } - - var siteToRemove = _serverManager.Sites.FirstOrDefault(site => site.Name == WebSiteName); - if (siteToRemove != null) - { - siteToRemove.Stop(); - _serverManager.Sites.Remove(siteToRemove); - _serverManager.CommitChanges(); - } - } - private string CreateTestSiteName() - { - if (!string.IsNullOrEmpty(_deploymentParameters.SiteName)) - { - return $"{_deploymentParameters.SiteName}{DateTime.Now.ToString("yyyyMMddHHmmss")}"; - } - else - { - return $"testsite{DateTime.Now.ToString("yyyyMMddHHmmss")}"; - } - } - } - } -} -#elif NETSTANDARD1_3 -#else -#error Target framework needs to be updated. -#endif diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs index 48381c499f..9234f55a15 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/IISExpressDeployer.cs @@ -1,11 +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.Diagnostics; using System.IO; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.Logging; @@ -16,6 +18,13 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// public class IISExpressDeployer : ApplicationDeployer { + private const string IISExpressRunningMessage = "IIS Express is running."; + private const string FailedToInitializeBindingsMessage = "Failed to initialize site bindings"; + private const string UnableToStartIISExpressMessage = "Unable to start iisexpress."; + private const int MaximumAttempts = 5; + + private static readonly Regex UrlDetectorRegex = new Regex(@"^\s*Successfully registered URL ""(?[^""]+)"" for site.*$"); + private Process _hostProcess; public IISExpressDeployer(DeploymentParameters deploymentParameters, ILogger logger) @@ -42,126 +51,200 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } } - public override DeploymentResult Deploy() + public override async Task DeployAsync() { - // Start timer - StartTimer(); - - // For now we always auto-publish. Otherwise we'll have to write our own local web.config for the HttpPlatformHandler - DeploymentParameters.PublishApplicationBeforeDeployment = true; - if (DeploymentParameters.PublishApplicationBeforeDeployment) + using (Logger.BeginScope("Deployment")) { - DotnetPublish(); + // Start timer + StartTimer(); + + // For now we always auto-publish. Otherwise we'll have to write our own local web.config for the HttpPlatformHandler + DeploymentParameters.PublishApplicationBeforeDeployment = true; + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + DotnetPublish(); + } + + var contentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath; + + var testUri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); + + // Launch the host process. + var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot); + + Logger.LogInformation("Application ready at URL: {appUrl}", actualUri); + + return new DeploymentResult + { + ContentRoot = contentRoot, + DeploymentParameters = DeploymentParameters, + // Right now this works only for urls like http://localhost:5001/. Does not work for http://localhost:5001/subpath. + ApplicationBaseUri = actualUri.ToString(), + HostShutdownToken = hostExitToken + }; } - - var contentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath; - - var uri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); - // Launch the host process. - var hostExitToken = StartIISExpress(uri, contentRoot); - - return new DeploymentResult - { - ContentRoot = contentRoot, - DeploymentParameters = DeploymentParameters, - // Right now this works only for urls like http://localhost:5001/. Does not work for http://localhost:5001/subpath. - ApplicationBaseUri = uri.ToString(), - HostShutdownToken = hostExitToken - }; } - private CancellationToken StartIISExpress(Uri uri, string contentRoot) + private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(Uri uri, string contentRoot) { - if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigTemplateContent)) + using (Logger.BeginScope("StartIISExpress")) { - // Pass on the applicationhost.config to iis express. With this don't need to pass in the /path /port switches as they are in the applicationHost.config - // We take a copy of the original specified applicationHost.Config to prevent modifying the one in the repo. - - if (DeploymentParameters.ServerConfigTemplateContent.Contains("[ANCMPath]")) + var port = uri.Port; + if (port == 0) { - string ancmPath; - if (!IsWin8OrLater) + port = TestUriHelper.GetNextPort(); + } + + for (var attempt = 0; attempt < MaximumAttempts; attempt++) + { + Logger.LogInformation("Attempting to start IIS Express on port: {port}", port); + + if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigTemplateContent)) { - // The nupkg build of ANCM does not support Win7. https://github.com/aspnet/AspNetCoreModule/issues/40. - ancmPath = @"%ProgramFiles%\IIS Express\aspnetcore.dll"; + var serverConfig = DeploymentParameters.ServerConfigTemplateContent; + + // Pass on the applicationhost.config to iis express. With this don't need to pass in the /path /port switches as they are in the applicationHost.config + // We take a copy of the original specified applicationHost.Config to prevent modifying the one in the repo. + + if (serverConfig.Contains("[ANCMPath]")) + { + string ancmPath; + if (!IsWin8OrLater) + { + // The nupkg build of ANCM does not support Win7. https://github.com/aspnet/AspNetCoreModule/issues/40. + ancmPath = @"%ProgramFiles%\IIS Express\aspnetcore.dll"; + } + else + { + // We need to pick the bitness based the OS / IIS Express, not the application. + // We'll eventually add support for choosing which IIS Express bitness to run: https://github.com/aspnet/Hosting/issues/880 + var ancmFile = Is64BitHost ? "aspnetcore_x64.dll" : "aspnetcore_x86.dll"; + // Bin deployed by Microsoft.AspNetCore.AspNetCoreModule.nupkg + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr + && DeploymentParameters.ApplicationType == ApplicationType.Portable) + { + ancmPath = Path.Combine(contentRoot, @"runtimes\win7\native\", ancmFile); + } + else + { + ancmPath = Path.Combine(contentRoot, ancmFile); + } + } + + if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmPath))) + { + throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmPath); + } + + Logger.LogDebug("Writing ANCMPath '{ancmPath}' to config", ancmPath); + serverConfig = + serverConfig.Replace("[ANCMPath]", ancmPath); + } + + Logger.LogDebug("Writing ApplicationPhysicalPath '{applicationPhysicalPath}' to config", contentRoot); + Logger.LogDebug("Writing Port '{port}' to config", port); + serverConfig = + serverConfig + .Replace("[ApplicationPhysicalPath]", contentRoot) + .Replace("[PORT]", port.ToString()); + + DeploymentParameters.ServerConfigLocation = Path.GetTempFileName(); + + Logger.LogDebug("Saving Config to {configPath}", DeploymentParameters.ServerConfigLocation); + + if (Logger.IsEnabled(LogLevel.Trace)) + { + Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", serverConfig); + } + + File.WriteAllText(DeploymentParameters.ServerConfigLocation, serverConfig); + } + + var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) ? + string.Format("/port:{0} /path:\"{1}\" /trace:error", uri.Port, contentRoot) : + string.Format("/site:{0} /config:{1} /trace:error", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); + + var iisExpressPath = GetIISExpressPath(); + + Logger.LogInformation("Executing command : {iisExpress} {parameters}", iisExpressPath, parameters); + + var startInfo = new ProcessStartInfo + { + FileName = iisExpressPath, + Arguments = parameters, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); + + Uri url = null; + var started = new TaskCompletionSource(); + + var process = new Process() { StartInfo = startInfo }; + process.OutputDataReceived += (sender, dataArgs) => + { + if (string.Equals(dataArgs.Data, UnableToStartIISExpressMessage)) + { + // We completely failed to start and we don't really know why + started.TrySetException(new InvalidOperationException("Failed to start IIS Express")); + } + else if (string.Equals(dataArgs.Data, FailedToInitializeBindingsMessage)) + { + started.TrySetResult(false); + } + else if (string.Equals(dataArgs.Data, IISExpressRunningMessage)) + { + started.TrySetResult(true); + } + else if (!string.IsNullOrEmpty(dataArgs.Data)) + { + var m = UrlDetectorRegex.Match(dataArgs.Data); + if (m.Success) + { + url = new Uri(m.Groups["url"].Value); + } + } + }; + process.EnableRaisingEvents = true; + var hostExitTokenSource = new CancellationTokenSource(); + process.Exited += (sender, e) => + { + Logger.LogInformation("iisexpress Process {pid} shut down", process.Id); + TriggerHostShutdown(hostExitTokenSource); + }; + process.StartAndCaptureOutAndErrToLogger("iisexpress", Logger); + Logger.LogInformation("iisexpress Process {pid} started", process.Id); + + if (process.HasExited) + { + Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, process.Id, process.ExitCode); + throw new Exception("Failed to start host"); + } + + // Wait for the app to start + if (!await started.Task) + { + Logger.LogInformation("iisexpress Process {pid} failed to bind to port {port}, trying again", _hostProcess.Id, port); + + // Wait for the process to exit and try again + process.WaitForExit(30 * 1000); + await Task.Delay(1000); // Wait a second to make sure the socket is completely cleaned up } else { - // We need to pick the bitness based the OS / IIS Express, not the application. - // We'll eventually add support for choosing which IIS Express bitness to run: https://github.com/aspnet/Hosting/issues/880 - var ancmFile = Is64BitHost ? "aspnetcore_x64.dll" : "aspnetcore_x86.dll"; - // Bin deployed by Microsoft.AspNetCore.AspNetCoreModule.nupkg - if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr - && DeploymentParameters.ApplicationType == ApplicationType.Portable) - { - ancmPath = Path.Combine(contentRoot, @"runtimes\win7\native\", ancmFile); - } - else - { - ancmPath = Path.Combine(contentRoot, ancmFile); - } + _hostProcess = process; + Logger.LogInformation("Started iisexpress successfully. Process Id : {processId}, Port: {port}", _hostProcess.Id, port); + return (url: url, hostExitToken: hostExitTokenSource.Token); } - - if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmPath))) - { - throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmPath); - } - - DeploymentParameters.ServerConfigTemplateContent = - DeploymentParameters.ServerConfigTemplateContent.Replace("[ANCMPath]", ancmPath); } - DeploymentParameters.ServerConfigTemplateContent = - DeploymentParameters.ServerConfigTemplateContent - .Replace("[ApplicationPhysicalPath]", contentRoot) - .Replace("[PORT]", uri.Port.ToString()); - - DeploymentParameters.ServerConfigLocation = Path.GetTempFileName(); - - File.WriteAllText(DeploymentParameters.ServerConfigLocation, DeploymentParameters.ServerConfigTemplateContent); + var message = $"Failed to initialize IIS Express after {MaximumAttempts} attempts to select a port"; + Logger.LogError(message); + throw new TimeoutException(message); } - - var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) ? - string.Format("/port:{0} /path:\"{1}\" /trace:error", uri.Port, contentRoot) : - string.Format("/site:{0} /config:{1} /trace:error", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); - - var iisExpressPath = GetIISExpressPath(); - - Logger.LogInformation("Executing command : {iisExpress} {args}", iisExpressPath, parameters); - - var startInfo = new ProcessStartInfo - { - FileName = iisExpressPath, - Arguments = parameters, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true - }; - - AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); - - _hostProcess = new Process() { StartInfo = startInfo }; - _hostProcess.ErrorDataReceived += (sender, dataArgs) => { Logger.LogError(dataArgs.Data ?? string.Empty); }; - _hostProcess.OutputDataReceived += (sender, dataArgs) => { Logger.LogInformation(dataArgs.Data ?? string.Empty); }; - _hostProcess.EnableRaisingEvents = true; - var hostExitTokenSource = new CancellationTokenSource(); - _hostProcess.Exited += (sender, e) => - { - TriggerHostShutdown(hostExitTokenSource); - }; - _hostProcess.Start(); - _hostProcess.BeginErrorReadLine(); - _hostProcess.BeginOutputReadLine(); - - if (_hostProcess.HasExited) - { - Logger.LogError("Host process {processName} exited with code {exitCode} or failed to start.", startInfo.FileName, _hostProcess.ExitCode); - throw new Exception("Failed to start host"); - } - - Logger.LogInformation("Started iisexpress. Process Id : {processId}", _hostProcess.Id); - return hostExitTokenSource.Token; } private string GetIISExpressPath() @@ -179,31 +262,35 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting public override void Dispose() { - ShutDownIfAnyHostProcess(_hostProcess); - - if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) - && File.Exists(DeploymentParameters.ServerConfigLocation)) + using (Logger.BeginScope("Dispose")) { - // Delete the temp applicationHostConfig that we created. - try + ShutDownIfAnyHostProcess(_hostProcess); + + if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) + && File.Exists(DeploymentParameters.ServerConfigLocation)) { - File.Delete(DeploymentParameters.ServerConfigLocation); + // Delete the temp applicationHostConfig that we created. + Logger.LogDebug("Deleting applicationHost.config file from {configLocation}", DeploymentParameters.ServerConfigLocation); + try + { + File.Delete(DeploymentParameters.ServerConfigLocation); + } + catch (Exception exception) + { + // Ignore delete failures - just write a log. + Logger.LogWarning("Failed to delete '{config}'. Exception : {exception}", DeploymentParameters.ServerConfigLocation, exception.Message); + } } - catch (Exception exception) + + if (DeploymentParameters.PublishApplicationBeforeDeployment) { - // Ignore delete failures - just write a log. - Logger.LogWarning("Failed to delete '{config}'. Exception : {exception}", DeploymentParameters.ServerConfigLocation, exception.Message); + CleanPublishedOutput(); } + + InvokeUserApplicationCleanup(); + + StopTimer(); } - - if (DeploymentParameters.PublishApplicationBeforeDeployment) - { - CleanPublishedOutput(); - } - - InvokeUserApplicationCleanup(); - - StopTimer(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/NginxDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/NginxDeployer.cs index 2f696cfe1a..fb881f7710 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/NginxDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/NginxDeployer.cs @@ -1,10 +1,11 @@ -// 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; using System.IO; using System.Net.Http; +using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.Logging; @@ -23,105 +24,79 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { } - public override DeploymentResult Deploy() + public override async Task DeployAsync() { - _configFile = Path.GetTempFileName(); - var uri = new Uri(DeploymentParameters.ApplicationBaseUriHint); - - var redirectUri = $"http://localhost:{TestUriHelper.FindFreePort()}"; - - if (DeploymentParameters.PublishApplicationBeforeDeployment) + using (Logger.BeginScope("Deploy")) { - DotnetPublish(); - } + _configFile = Path.GetTempFileName(); + var uri = new Uri(DeploymentParameters.ApplicationBaseUriHint); - var exitToken = StartSelfHost(new Uri(redirectUri)); + var redirectUri = $"http://localhost:{TestUriHelper.GetNextPort()}"; - SetupNginx(redirectUri, uri); - - // Wait for App to be loaded since Nginx returns 502 instead of 503 when App isn't loaded - // Target actual address to avoid going through Nginx proxy - using (var httpClient = new HttpClient()) - { - var response = RetryHelper.RetryRequest(() => + if (DeploymentParameters.PublishApplicationBeforeDeployment) { - return httpClient.GetAsync(redirectUri); - }, Logger, exitToken).Result; - - if (!response.IsSuccessStatusCode) - { - throw new InvalidOperationException("Deploy failed"); + DotnetPublish(); } - } - return new DeploymentResult - { - ContentRoot = DeploymentParameters.ApplicationPath, - DeploymentParameters = DeploymentParameters, - ApplicationBaseUri = uri.ToString(), - HostShutdownToken = exitToken - }; + var (appUri, exitToken) = await StartSelfHostAsync(new Uri(redirectUri)); + + SetupNginx(appUri.ToString(), uri); + + Logger.LogInformation("Application ready at URL: {appUrl}", uri); + + // Wait for App to be loaded since Nginx returns 502 instead of 503 when App isn't loaded + // Target actual address to avoid going through Nginx proxy + using (var httpClient = new HttpClient()) + { + var response = await RetryHelper.RetryRequest(() => + { + return httpClient.GetAsync(redirectUri); + }, Logger, exitToken); + + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Deploy failed"); + } + } + + return new DeploymentResult + { + ContentRoot = DeploymentParameters.ApplicationPath, + DeploymentParameters = DeploymentParameters, + ApplicationBaseUri = uri.ToString(), + HostShutdownToken = exitToken + }; + } } private void SetupNginx(string redirectUri, Uri originalUri) { - // copy nginx.conf template and replace pertinent information - DeploymentParameters.ServerConfigTemplateContent = DeploymentParameters.ServerConfigTemplateContent - .Replace("[user]", Environment.GetEnvironmentVariable("LOGNAME")) - .Replace("[errorlog]", Path.Combine(DeploymentParameters.ApplicationPath, "nginx.error.log")) - .Replace("[accesslog]", Path.Combine(DeploymentParameters.ApplicationPath, "nginx.access.log")) - .Replace("[listenPort]", originalUri.Port.ToString()) - .Replace("[redirectUri]", redirectUri) - .Replace("[pidFile]", Path.Combine(DeploymentParameters.ApplicationPath, Guid.NewGuid().ToString())); - File.WriteAllText(_configFile, DeploymentParameters.ServerConfigTemplateContent); - - var startInfo = new ProcessStartInfo + using (Logger.BeginScope("SetupNginx")) { - FileName = "nginx", - Arguments = $"-c {_configFile}", - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - // Trying a work around for https://github.com/aspnet/Hosting/issues/140. - RedirectStandardInput = true - }; - - using (var runNginx = new Process() { StartInfo = startInfo }) - { - runNginx.ErrorDataReceived += (sender, dataArgs) => + // copy nginx.conf template and replace pertinent information + var pidFile = Path.Combine(DeploymentParameters.ApplicationPath, $"{Guid.NewGuid()}.nginx.pid"); + var errorLog = Path.Combine(DeploymentParameters.ApplicationPath, "nginx.error.log"); + var accessLog = Path.Combine(DeploymentParameters.ApplicationPath, "nginx.access.log"); + DeploymentParameters.ServerConfigTemplateContent = DeploymentParameters.ServerConfigTemplateContent + .Replace("[user]", Environment.GetEnvironmentVariable("LOGNAME")) + .Replace("[errorlog]", errorLog) + .Replace("[accesslog]", accessLog) + .Replace("[listenPort]", originalUri.Port.ToString()) + .Replace("[redirectUri]", redirectUri) + .Replace("[pidFile]", pidFile); + Logger.LogDebug("Using PID file: {pidFile}", pidFile); + Logger.LogDebug("Using Error Log file: {errorLog}", pidFile); + Logger.LogDebug("Using Access Log file: {accessLog}", pidFile); + if (Logger.IsEnabled(LogLevel.Trace)) { - if (!string.IsNullOrEmpty(dataArgs.Data)) - { - Logger.LogWarning("nginx: " + dataArgs.Data); - } - }; - runNginx.OutputDataReceived += (sender, dataArgs) => - { - if (!string.IsNullOrEmpty(dataArgs.Data)) - { - Logger.LogInformation("nginx: " + dataArgs.Data); - } - }; - runNginx.Start(); - runNginx.BeginErrorReadLine(); - runNginx.BeginOutputReadLine(); - runNginx.WaitForExit(_waitTime); - if (runNginx.ExitCode != 0) - { - throw new Exception("Failed to start Nginx"); + Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", DeploymentParameters.ServerConfigTemplateContent); } - } - } + File.WriteAllText(_configFile, DeploymentParameters.ServerConfigTemplateContent); - public override void Dispose() - { - if (!string.IsNullOrEmpty(_configFile)) - { var startInfo = new ProcessStartInfo { FileName = "nginx", - Arguments = $"-s stop -c {_configFile}", + Arguments = $"-c {_configFile}", UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, @@ -132,14 +107,58 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting using (var runNginx = new Process() { StartInfo = startInfo }) { - runNginx.Start(); + runNginx.StartAndCaptureOutAndErrToLogger("nginx start", Logger); runNginx.WaitForExit(_waitTime); + if (runNginx.ExitCode != 0) + { + throw new Exception("Failed to start nginx"); + } + + // Read the PID file + if(!File.Exists(pidFile)) + { + Logger.LogWarning("Unable to find nginx PID file: {pidFile}", pidFile); + } + else + { + var pid = File.ReadAllText(pidFile); + Logger.LogInformation("nginx process ID {pid} started", pid); + } + } + } + } + + public override void Dispose() + { + using (Logger.BeginScope("Dispose")) + { + if (!string.IsNullOrEmpty(_configFile)) + { + var startInfo = new ProcessStartInfo + { + FileName = "nginx", + Arguments = $"-s stop -c {_configFile}", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + // Trying a work around for https://github.com/aspnet/Hosting/issues/140. + RedirectStandardInput = true + }; + + using (var runNginx = new Process() { StartInfo = startInfo }) + { + runNginx.StartAndCaptureOutAndErrToLogger("nginx stop", Logger); + runNginx.WaitForExit(_waitTime); + Logger.LogInformation("nginx stop command issued"); + } + + Logger.LogDebug("Deleting config file: {configFile}", _configFile); + File.Delete(_configFile); } - File.Delete(_configFile); + base.Dispose(); } - - base.Dispose(); } } } diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs index 0a0f5b2cb9..f9b60f62ad 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs @@ -7,7 +7,9 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Threading.Tasks; using System.Xml.Linq; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; @@ -73,76 +75,82 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } } - public override DeploymentResult Deploy() + public override async Task DeployAsync() { - if (_isDisposed) + using (Logger.BeginScope("Deploy")) { - throw new ObjectDisposedException("This instance of deployer has already been disposed."); + if (_isDisposed) + { + throw new ObjectDisposedException("This instance of deployer has already been disposed."); + } + + // Publish the app to a local temp folder on the machine where the test is running + DotnetPublish(); + + if (_deploymentParameters.ServerType == ServerType.IIS) + { + UpdateWebConfig(); + } + + var folderId = Guid.NewGuid().ToString(); + _deployedFolderPathInFileShare = Path.Combine(_deploymentParameters.RemoteServerFileSharePath, folderId); + + DirectoryCopy( + _deploymentParameters.PublishedApplicationRootPath, + _deployedFolderPathInFileShare, + copySubDirs: true); + Logger.LogInformation($"Copied the locally published folder to the file share path '{_deployedFolderPathInFileShare}'"); + + await RunScriptAsync("StartServer"); + + return new DeploymentResult + { + ApplicationBaseUri = DeploymentParameters.ApplicationBaseUriHint, + DeploymentParameters = DeploymentParameters + }; } - - // Publish the app to a local temp folder on the machine where the test is running - DotnetPublish(); - - if (_deploymentParameters.ServerType == ServerType.IIS) - { - UpdateWebConfig(); - } - - var folderId = Guid.NewGuid().ToString(); - _deployedFolderPathInFileShare = Path.Combine(_deploymentParameters.RemoteServerFileSharePath, folderId); - - DirectoryCopy( - _deploymentParameters.PublishedApplicationRootPath, - _deployedFolderPathInFileShare, - copySubDirs: true); - Logger.LogInformation($"Copied the locally published folder to the file share path '{_deployedFolderPathInFileShare}'"); - - RunScript("StartServer"); - - return new DeploymentResult - { - ApplicationBaseUri = DeploymentParameters.ApplicationBaseUriHint, - DeploymentParameters = DeploymentParameters - }; } public override void Dispose() { - if (_isDisposed) + using (Logger.BeginScope("Dispose")) { - return; - } + if (_isDisposed) + { + return; + } - _isDisposed = true; + _isDisposed = true; - try - { - Logger.LogInformation($"Stopping the application on the server '{_deploymentParameters.ServerName}'"); - RunScript("StopServer"); - } - catch (Exception ex) - { - Logger.LogWarning(0, "Failed to stop the server.", ex); - } + try + { + Logger.LogInformation($"Stopping the application on the server '{_deploymentParameters.ServerName}'"); + RunScriptAsync("StopServer").Wait(); + } + catch (Exception ex) + { + Logger.LogWarning(0, "Failed to stop the server.", ex); + } - try - { - Logger.LogInformation($"Deleting the deployed folder '{_deployedFolderPathInFileShare}'"); - Directory.Delete(_deployedFolderPathInFileShare, recursive: true); - } - catch (Exception ex) - { - Logger.LogWarning(0, $"Failed to delete the deployed folder '{_deployedFolderPathInFileShare}'.", ex); - } + try + { + Logger.LogInformation($"Deleting the deployed folder '{_deployedFolderPathInFileShare}'"); + Directory.Delete(_deployedFolderPathInFileShare, recursive: true); + } + catch (Exception ex) + { + Logger.LogWarning(0, $"Failed to delete the deployed folder '{_deployedFolderPathInFileShare}'.", ex); + } - try - { - Logger.LogInformation($"Deleting the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'"); - Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, recursive: true); - } - catch (Exception ex) - { - Logger.LogWarning(0, $"Failed to delete the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'.", ex); + try + { + Logger.LogInformation($"Deleting the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'"); + Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, recursive: true); + } + catch (Exception ex) + { + Logger.LogWarning(0, $"Failed to delete the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'.", ex); + } } } @@ -176,92 +184,92 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting environmentVariablesSection.Add(environmentVariable); } + if(Logger.IsEnabled(LogLevel.Trace)) + { + Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", webConfig.ToString()); + } + using (var fileStream = File.Open(webConfigFilePath, FileMode.Open)) { webConfig.Save(fileStream); } } - private void RunScript(string serverAction) + private async Task RunScriptAsync(string serverAction) { - var remotePSSessionHelperScript = _scripts.Value.RemotePSSessionHelper; - - string executablePath = null; - string executableParameters = null; - var applicationName = new DirectoryInfo(DeploymentParameters.ApplicationPath).Name; - if (DeploymentParameters.ApplicationType == ApplicationType.Portable) + using (Logger.BeginScope($"RunScript:{serverAction}")) { - executablePath = "dotnet.exe"; - executableParameters = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".dll"); - } - else - { - executablePath = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".exe"); - } + var remotePSSessionHelperScript = _scripts.Value.RemotePSSessionHelper; - var parameterBuilder = new StringBuilder(); - parameterBuilder.Append($"\"{remotePSSessionHelperScript}\""); - parameterBuilder.Append($" -serverName {_deploymentParameters.ServerName}"); - parameterBuilder.Append($" -accountName {_deploymentParameters.ServerAccountName}"); - parameterBuilder.Append($" -accountPassword {_deploymentParameters.ServerAccountPassword}"); - parameterBuilder.Append($" -deployedFolderPath {_deployedFolderPathInFileShare}"); - - if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) - { - parameterBuilder.Append($" -dotnetRuntimePath \"{_deploymentParameters.DotnetRuntimePath}\""); - } - - parameterBuilder.Append($" -executablePath \"{executablePath}\""); - - if (!string.IsNullOrEmpty(executableParameters)) - { - parameterBuilder.Append($" -executableParameters \"{executableParameters}\""); - } - - parameterBuilder.Append($" -serverType {_deploymentParameters.ServerType}"); - parameterBuilder.Append($" -serverAction {serverAction}"); - parameterBuilder.Append($" -applicationBaseUrl {_deploymentParameters.ApplicationBaseUriHint}"); - var environmentVariables = string.Join("`,", _deploymentParameters.EnvironmentVariables.Select(envVariable => $"{envVariable.Key}={envVariable.Value}")); - parameterBuilder.Append($" -environmentVariables \"{environmentVariables}\""); - - var startInfo = new ProcessStartInfo - { - FileName = "powershell.exe", - Arguments = parameterBuilder.ToString(), - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - RedirectStandardInput = true - }; - - using (var runScriptsOnRemoteServerProcess = new Process() { StartInfo = startInfo }) - { - runScriptsOnRemoteServerProcess.EnableRaisingEvents = true; - runScriptsOnRemoteServerProcess.ErrorDataReceived += (sender, dataArgs) => + string executablePath = null; + string executableParameters = null; + var applicationName = new DirectoryInfo(DeploymentParameters.ApplicationPath).Name; + if (DeploymentParameters.ApplicationType == ApplicationType.Portable) { - if (!string.IsNullOrEmpty(dataArgs.Data)) - { - Logger.LogWarning($"[{_deploymentParameters.ServerName}]: {dataArgs.Data}"); - } + executablePath = "dotnet.exe"; + executableParameters = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".dll"); + } + else + { + executablePath = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".exe"); + } + + var parameterBuilder = new StringBuilder(); + parameterBuilder.Append($"\"{remotePSSessionHelperScript}\""); + parameterBuilder.Append($" -serverName {_deploymentParameters.ServerName}"); + parameterBuilder.Append($" -accountName {_deploymentParameters.ServerAccountName}"); + parameterBuilder.Append($" -accountPassword {_deploymentParameters.ServerAccountPassword}"); + parameterBuilder.Append($" -deployedFolderPath {_deployedFolderPathInFileShare}"); + + if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) + { + parameterBuilder.Append($" -dotnetRuntimePath \"{_deploymentParameters.DotnetRuntimePath}\""); + } + + parameterBuilder.Append($" -executablePath \"{executablePath}\""); + + if (!string.IsNullOrEmpty(executableParameters)) + { + parameterBuilder.Append($" -executableParameters \"{executableParameters}\""); + } + + parameterBuilder.Append($" -serverType {_deploymentParameters.ServerType}"); + parameterBuilder.Append($" -serverAction {serverAction}"); + parameterBuilder.Append($" -applicationBaseUrl {_deploymentParameters.ApplicationBaseUriHint}"); + var environmentVariables = string.Join("`,", _deploymentParameters.EnvironmentVariables.Select(envVariable => $"{envVariable.Key}={envVariable.Value}")); + parameterBuilder.Append($" -environmentVariables \"{environmentVariables}\""); + + var startInfo = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = parameterBuilder.ToString(), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true }; - runScriptsOnRemoteServerProcess.OutputDataReceived += (sender, dataArgs) => + using (var runScriptsOnRemoteServerProcess = new Process() { StartInfo = startInfo }) { - if (!string.IsNullOrEmpty(dataArgs.Data)) + var processExited = new TaskCompletionSource(); + + runScriptsOnRemoteServerProcess.EnableRaisingEvents = true; + runScriptsOnRemoteServerProcess.Exited += (sender, exitedArgs) => { - Logger.LogInformation($"[{_deploymentParameters.ServerName}]: {dataArgs.Data}"); + Logger.LogInformation($"[{_deploymentParameters.ServerName} {serverAction} stdout]: script complete"); + processExited.TrySetResult(null); + }; + + runScriptsOnRemoteServerProcess.StartAndCaptureOutAndErrToLogger(serverAction, Logger); + + await processExited.Task.OrTimeout(TimeSpan.FromMinutes(1)); + runScriptsOnRemoteServerProcess.WaitForExit((int)TimeSpan.FromMinutes(1).TotalMilliseconds); + + if (runScriptsOnRemoteServerProcess.HasExited && runScriptsOnRemoteServerProcess.ExitCode != 0) + { + throw new Exception($"Failed to execute the script on '{_deploymentParameters.ServerName}'."); } - }; - - runScriptsOnRemoteServerProcess.Start(); - runScriptsOnRemoteServerProcess.BeginErrorReadLine(); - runScriptsOnRemoteServerProcess.BeginOutputReadLine(); - runScriptsOnRemoteServerProcess.WaitForExit((int)TimeSpan.FromMinutes(1).TotalMilliseconds); - - if (runScriptsOnRemoteServerProcess.HasExited && runScriptsOnRemoteServerProcess.ExitCode != 0) - { - throw new Exception($"Failed to execute the script on '{_deploymentParameters.ServerName}'."); } } } diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/SelfHostDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/SelfHostDeployer.cs index 31a7023cfb..03600cce47 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/SelfHostDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Deployers/SelfHostDeployer.cs @@ -1,11 +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.Diagnostics; using System.IO; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.Logging; @@ -16,6 +18,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// public class SelfHostDeployer : ApplicationDeployer { + private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?.*)$"); + private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down."; + public Process HostProcess { get; private set; } public SelfHostDeployer(DeploymentParameters deploymentParameters, ILogger logger) @@ -23,129 +28,159 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { } - public override DeploymentResult Deploy() + public override async Task DeployAsync() { - // Start timer - StartTimer(); - - if (DeploymentParameters.PublishApplicationBeforeDeployment) + using (Logger.BeginScope("SelfHost.Deploy")) { - DotnetPublish(); + // Start timer + StartTimer(); + + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + DotnetPublish(); + } + + var hintUrl = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); + + // Launch the host process. + var (actualUrl, hostExitToken) = await StartSelfHostAsync(hintUrl); + + Logger.LogInformation("Application ready at URL: {appUrl}", actualUrl); + + return new DeploymentResult + { + ContentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath, + DeploymentParameters = DeploymentParameters, + ApplicationBaseUri = actualUrl.ToString(), + HostShutdownToken = hostExitToken + }; } - - var uri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); - // Launch the host process. - var hostExitToken = StartSelfHost(uri); - - return new DeploymentResult - { - ContentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath, - DeploymentParameters = DeploymentParameters, - ApplicationBaseUri = uri.ToString(), - HostShutdownToken = hostExitToken - }; } - protected CancellationToken StartSelfHost(Uri uri) + protected async Task<(Uri url, CancellationToken hostExitToken)> StartSelfHostAsync(Uri hintUrl) { - string executableName; - string executableArgs = string.Empty; - string workingDirectory = string.Empty; - if (DeploymentParameters.PublishApplicationBeforeDeployment) + using (Logger.BeginScope("StartSelfHost")) { - workingDirectory = DeploymentParameters.PublishedApplicationRootPath; - var executableExtension = - DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? ".exe" : - DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll" : ""; - var executable = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, DeploymentParameters.ApplicationName + executableExtension); + string executableName; + string executableArgs = string.Empty; + string workingDirectory = string.Empty; + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + workingDirectory = DeploymentParameters.PublishedApplicationRootPath; + var executableExtension = + DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? ".exe" : + DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll" : ""; + var executable = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, DeploymentParameters.ApplicationName + executableExtension); - if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - executableName = "mono"; - executableArgs = executable; - } - else if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) - { - executableName = "dotnet"; - executableArgs = executable; + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + executableName = "mono"; + executableArgs = executable; + } + else if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) + { + executableName = "dotnet"; + executableArgs = executable; + } + else + { + executableName = executable; + } } else { - executableName = executable; + workingDirectory = DeploymentParameters.ApplicationPath; + var targetFramework = DeploymentParameters.TargetFramework ?? (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? "net46" : "netcoreapp2.0"); + + executableName = DotnetCommandName; + executableArgs = $"run --framework {targetFramework} {DotnetArgumentSeparator}"; } + + executableArgs += $" --server.urls {hintUrl} " + + $" --server {(DeploymentParameters.ServerType == ServerType.WebListener ? "Microsoft.AspNetCore.Server.HttpSys" : "Microsoft.AspNetCore.Server.Kestrel")}"; + + Logger.LogInformation($"Executing {executableName} {executableArgs}"); + + var startInfo = new ProcessStartInfo + { + FileName = executableName, + Arguments = executableArgs, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + // Trying a work around for https://github.com/aspnet/Hosting/issues/140. + RedirectStandardInput = true, + WorkingDirectory = workingDirectory + }; + + AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); + + Uri actualUrl = null; + var started = new TaskCompletionSource(); + + HostProcess = new Process() { StartInfo = startInfo }; + HostProcess.EnableRaisingEvents = true; + HostProcess.OutputDataReceived += (sender, dataArgs) => + { + if (string.Equals(dataArgs.Data, ApplicationStartedMessage)) + { + started.TrySetResult(null); + } + else if (!string.IsNullOrEmpty(dataArgs.Data)) + { + var m = NowListeningRegex.Match(dataArgs.Data); + if (m.Success) + { + actualUrl = new Uri(m.Groups["url"].Value); + } + } + }; + var hostExitTokenSource = new CancellationTokenSource(); + HostProcess.Exited += (sender, e) => + { + Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id); + TriggerHostShutdown(hostExitTokenSource); + }; + + try + { + HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger); + } + catch (Exception ex) + { + Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString()); + } + + if (HostProcess.HasExited) + { + Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode); + throw new Exception("Failed to start host"); + } + + Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); + + await started.Task; + + return (url: actualUrl ?? hintUrl, hostExitToken: hostExitTokenSource.Token); } - else - { - workingDirectory = DeploymentParameters.ApplicationPath; - var targetFramework = DeploymentParameters.TargetFramework ?? (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? "net46" : "netcoreapp2.0"); - - executableName = DotnetCommandName; - executableArgs = $"run --framework {targetFramework} {DotnetArgumentSeparator}"; - } - - executableArgs += $" --server.urls {uri} " - + $" --server {(DeploymentParameters.ServerType == ServerType.WebListener ? "Microsoft.AspNetCore.Server.HttpSys" : "Microsoft.AspNetCore.Server.Kestrel")}"; - - Logger.LogInformation($"Executing {executableName} {executableArgs}"); - - var startInfo = new ProcessStartInfo - { - FileName = executableName, - Arguments = executableArgs, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - // Trying a work around for https://github.com/aspnet/Hosting/issues/140. - RedirectStandardInput = true, - WorkingDirectory = workingDirectory - }; - - AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); - - HostProcess = new Process() { StartInfo = startInfo }; - HostProcess.ErrorDataReceived += (sender, dataArgs) => { Logger.LogError(dataArgs.Data ?? string.Empty); }; - HostProcess.OutputDataReceived += (sender, dataArgs) => { Logger.LogInformation(dataArgs.Data ?? string.Empty); }; - HostProcess.EnableRaisingEvents = true; - var hostExitTokenSource = new CancellationTokenSource(); - HostProcess.Exited += (sender, e) => - { - TriggerHostShutdown(hostExitTokenSource); - }; - - try - { - HostProcess.Start(); - HostProcess.BeginErrorReadLine(); - HostProcess.BeginOutputReadLine(); - } - catch (Exception ex) - { - Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString()); - } - - if (HostProcess.HasExited) - { - Logger.LogError("Host process {processName} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.ExitCode); - throw new Exception("Failed to start host"); - } - - Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); - return hostExitTokenSource.Token; } public override void Dispose() { - ShutDownIfAnyHostProcess(HostProcess); - - if (DeploymentParameters.PublishApplicationBeforeDeployment) + using (Logger.BeginScope("SelfHost.Dispose")) { - CleanPublishedOutput(); + ShutDownIfAnyHostProcess(HostProcess); + + if (DeploymentParameters.PublishApplicationBeforeDeployment) + { + CleanPublishedOutput(); + } + + InvokeUserApplicationCleanup(); + + StopTimer(); } - - InvokeUserApplicationCleanup(); - - StopTimer(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Microsoft.AspNetCore.Server.IntegrationTesting.csproj index 51e5864081..8d2504447e 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting/Microsoft.AspNetCore.Server.IntegrationTesting.csproj +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting/Microsoft.AspNetCore.Server.IntegrationTesting.csproj @@ -4,7 +4,7 @@ ASP.NET Core helpers to deploy applications to IIS Express, IIS, WebListener and Kestrel for testing. - 0.3.0 + 0.4.0 net46;netstandard1.3 $(NoWarn);CS1591 true @@ -17,6 +17,7 @@ + diff --git a/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs b/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs index 4d5c75a8a4..abf1cbb426 100644 --- a/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs +++ b/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; +using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.AspNetCore.Server.IntegrationTesting.xunit; using Microsoft.AspNetCore.Testing.xunit; @@ -16,11 +17,12 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows)] [OSSkipCondition(OperatingSystems.MacOSX)] - public void ShutdownTest() + public async Task ShutdownTest() { var logger = new LoggerFactory() .AddConsole() .CreateLogger(nameof(ShutdownTest)); + logger.LogInformation("Started test: {testName}", nameof(ShutdownTest)); var applicationPath = Path.Combine(TestProjectHelpers.GetSolutionRoot(), "test", "Microsoft.AspNetCore.Hosting.TestSites"); @@ -39,10 +41,10 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests using (var deployer = new SelfHostDeployer(deploymentParameters, logger)) { - deployer.Deploy(); + await deployer.DeployAsync(); // Wait for application to start - System.Threading.Thread.Sleep(1000); + await Task.Delay(1000); string output = string.Empty; deployer.HostProcess.OutputDataReceived += (sender, args) => output += args.Data + '\n'; diff --git a/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj b/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj index bbf764d1ce..3fd644197c 100644 --- a/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj +++ b/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj @@ -1,10 +1,11 @@ - + netcoreapp2.0;net46 netcoreapp2.0 + Exe diff --git a/test/Microsoft.AspNetCore.Hosting.TestSites/Properties/launchSettings.json b/test/Microsoft.AspNetCore.Hosting.TestSites/Properties/launchSettings.json new file mode 100644 index 0000000000..b1f1a85f6f --- /dev/null +++ b/test/Microsoft.AspNetCore.Hosting.TestSites/Properties/launchSettings.json @@ -0,0 +1,22 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:49570/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Microsoft.AspNetCore.Hosting.TestSites": { + "commandName": "Project" + } + } +} \ No newline at end of file