diff --git a/src/Microsoft.AspNetCore.Server.Testing/Common/DeploymentParameters.cs b/src/Microsoft.AspNetCore.Server.Testing/Common/DeploymentParameters.cs index 55df9abe48..9150e71f73 100644 --- a/src/Microsoft.AspNetCore.Server.Testing/Common/DeploymentParameters.cs +++ b/src/Microsoft.AspNetCore.Server.Testing/Common/DeploymentParameters.cs @@ -61,9 +61,9 @@ namespace Microsoft.AspNetCore.Server.Testing public string EnvironmentName { get; set; } - public string ApplicationHostConfigTemplateContent { get; set; } + public string ServerConfigTemplateContent { get; set; } - public string ApplicationHostConfigLocation { get; set; } + public string ServerConfigLocation { get; set; } public string SiteName { get; set; } diff --git a/src/Microsoft.AspNetCore.Server.Testing/Common/ServerType.cs b/src/Microsoft.AspNetCore.Server.Testing/Common/ServerType.cs index e54e8f99f6..d3dec2bdf3 100644 --- a/src/Microsoft.AspNetCore.Server.Testing/Common/ServerType.cs +++ b/src/Microsoft.AspNetCore.Server.Testing/Common/ServerType.cs @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Server.Testing IISExpress, IIS, WebListener, - Kestrel + Kestrel, + Nginx } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Testing/Deployers/ApplicationDeployerFactory.cs b/src/Microsoft.AspNetCore.Server.Testing/Deployers/ApplicationDeployerFactory.cs index 0f2492648c..4576902281 100644 --- a/src/Microsoft.AspNetCore.Server.Testing/Deployers/ApplicationDeployerFactory.cs +++ b/src/Microsoft.AspNetCore.Server.Testing/Deployers/ApplicationDeployerFactory.cs @@ -40,6 +40,8 @@ namespace Microsoft.AspNetCore.Server.Testing case ServerType.WebListener: case ServerType.Kestrel: return new SelfHostDeployer(deploymentParameters, logger); + case ServerType.Nginx: + return new NginxDeployer(deploymentParameters, logger); 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.Testing/Deployers/IISExpressDeployer.cs b/src/Microsoft.AspNetCore.Server.Testing/Deployers/IISExpressDeployer.cs index 0f491e562f..d0c1b4033d 100644 --- a/src/Microsoft.AspNetCore.Server.Testing/Deployers/IISExpressDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.Testing/Deployers/IISExpressDeployer.cs @@ -50,19 +50,19 @@ namespace Microsoft.AspNetCore.Server.Testing private CancellationToken StartIISExpress(Uri uri) { - if (!string.IsNullOrWhiteSpace(DeploymentParameters.ApplicationHostConfigTemplateContent)) + if (!string.IsNullOrWhiteSpace(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. - DeploymentParameters.ApplicationHostConfigTemplateContent = - DeploymentParameters.ApplicationHostConfigTemplateContent + DeploymentParameters.ServerConfigTemplateContent = + DeploymentParameters.ServerConfigTemplateContent .Replace("[ApplicationPhysicalPath]", DeploymentParameters.ApplicationPath) .Replace("[PORT]", uri.Port.ToString()); - DeploymentParameters.ApplicationHostConfigLocation = Path.GetTempFileName(); + DeploymentParameters.ServerConfigLocation = Path.GetTempFileName(); - File.WriteAllText(DeploymentParameters.ApplicationHostConfigLocation, DeploymentParameters.ApplicationHostConfigTemplateContent); + File.WriteAllText(DeploymentParameters.ServerConfigLocation, DeploymentParameters.ServerConfigTemplateContent); } var webroot = DeploymentParameters.ApplicationPath; @@ -71,9 +71,9 @@ namespace Microsoft.AspNetCore.Server.Testing webroot = Path.Combine(webroot, "wwwroot"); } - var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ApplicationHostConfigLocation) ? + var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) ? string.Format("/port:{0} /path:\"{1}\" /trace:error", uri.Port, webroot) : - string.Format("/site:{0} /config:{1} /trace:error", DeploymentParameters.SiteName, DeploymentParameters.ApplicationHostConfigLocation); + string.Format("/site:{0} /config:{1} /trace:error", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); var iisExpressPath = GetIISExpressPath(); @@ -140,18 +140,18 @@ namespace Microsoft.AspNetCore.Server.Testing { ShutDownIfAnyHostProcess(_hostProcess); - if (!string.IsNullOrWhiteSpace(DeploymentParameters.ApplicationHostConfigLocation) - && File.Exists(DeploymentParameters.ApplicationHostConfigLocation)) + if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) + && File.Exists(DeploymentParameters.ServerConfigLocation)) { // Delete the temp applicationHostConfig that we created. try { - File.Delete(DeploymentParameters.ApplicationHostConfigLocation); + File.Delete(DeploymentParameters.ServerConfigLocation); } catch (Exception exception) { // Ignore delete failures - just write a log. - Logger.LogWarning("Failed to delete '{config}'. Exception : {exception}", DeploymentParameters.ApplicationHostConfigLocation, exception.Message); + Logger.LogWarning("Failed to delete '{config}'. Exception : {exception}", DeploymentParameters.ServerConfigLocation, exception.Message); } } diff --git a/src/Microsoft.AspNetCore.Server.Testing/Deployers/NginxDeployer.cs b/src/Microsoft.AspNetCore.Server.Testing/Deployers/NginxDeployer.cs new file mode 100644 index 0000000000..bb85c9a33b --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Testing/Deployers/NginxDeployer.cs @@ -0,0 +1,138 @@ +// 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 Microsoft.AspNetCore.Server.Testing.Common; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Testing +{ + /// + /// Deployer for Kestrel on Nginx. + /// + public class NginxDeployer : SelfHostDeployer + { + private string _configFile; + private readonly int _waitTime = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; + + public NginxDeployer(DeploymentParameters deploymentParameters, ILogger logger) + : base(deploymentParameters, logger) + { + } + + public override DeploymentResult Deploy() + { + _configFile = Path.GetTempFileName(); + var uri = new Uri(DeploymentParameters.ApplicationBaseUriHint); + + var redirectUri = $"http://localhost:{TestUriHelper.FindFreePort()}"; + + var exitToken = StartSelfHost(new Uri(redirectUri)); + + 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(() => + { + return httpClient.GetAsync(redirectUri); + }, Logger).Result; + + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Deploy failed"); + } + } + + return new DeploymentResult + { + WebRootLocation = 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("[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 + { + 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) => + { + 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"); + } + } + } + + public override void 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.Start(); + runNginx.WaitForExit(_waitTime); + } + + File.Delete(_configFile); + } + + base.Dispose(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Testing/Deployers/SelfHostDeployer.cs b/src/Microsoft.AspNetCore.Server.Testing/Deployers/SelfHostDeployer.cs index f28e5dde98..8a43604e3a 100644 --- a/src/Microsoft.AspNetCore.Server.Testing/Deployers/SelfHostDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.Testing/Deployers/SelfHostDeployer.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.Testing }; } - private CancellationToken StartSelfHost(Uri uri) + protected CancellationToken StartSelfHost(Uri uri) { string executableName; string executableArgs = string.Empty;