| File: Deployers\NginxDeployer.cs | Web Access |
| Project: src\aspnetcore\src\Hosting\Server.IntegrationTesting\src\Microsoft.AspNetCore.Server.IntegrationTesting.csproj (Microsoft.AspNetCore.Server.IntegrationTesting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Sockets; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.IntegrationTesting; /// <summary> /// Deployer for Kestrel on Nginx. /// </summary> public class NginxDeployer : SelfHostDeployer { private string _configFile; private readonly int _waitTime = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; private Socket _portSelector; public NginxDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) : base(deploymentParameters, loggerFactory) { } public override async Task<DeploymentResult> DeployAsync() { using (Logger.BeginScope("Deploy")) { _configFile = Path.GetTempFileName(); var uri = string.IsNullOrEmpty(DeploymentParameters.ApplicationBaseUriHint) ? new Uri("http://localhost:0") : new Uri(DeploymentParameters.ApplicationBaseUriHint); if (uri.Port == 0) { var builder = new UriBuilder(uri); if (OperatingSystem.IsLinux()) { // This works with nginx 1.9.1 and later using the reuseport flag, available on Ubuntu 16.04. // Keep it open so nobody else claims the port _portSelector = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _portSelector.Bind(new IPEndPoint(IPAddress.Loopback, 0)); builder.Port = ((IPEndPoint)_portSelector.LocalEndPoint).Port; } else { builder.Port = TestPortHelper.GetNextPort(); } uri = builder.Uri; } var redirectUri = TestUriHelper.BuildTestUri(ServerType.Nginx); if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Standalone) { // Publish is required to get the correct files in the output directory DeploymentParameters.PublishApplicationBeforeDeployment = true; } if (DeploymentParameters.PublishApplicationBeforeDeployment) { DotnetPublish(); } var (appUri, exitToken) = await StartSelfHostAsync(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()) { httpClient.Timeout = TimeSpan.FromSeconds(200); var response = await RetryHelper.RetryRequest(() => { return httpClient.GetAsync(redirectUri); }, Logger, exitToken); if (!response.IsSuccessStatusCode) { throw new InvalidOperationException("Deploy failed"); } } return new DeploymentResult( LoggerFactory, DeploymentParameters, applicationBaseUri: uri.ToString(), contentRoot: DeploymentParameters.ApplicationPath, hostShutdownToken: exitToken); } } private static string GetUserName() { var retVal = Environment.GetEnvironmentVariable("LOGNAME") ?? Environment.GetEnvironmentVariable("USER") ?? Environment.GetEnvironmentVariable("USERNAME"); if (!string.IsNullOrEmpty(retVal)) { return retVal; } if (!OperatingSystem.IsWindows()) { using (var process = new Process { StartInfo = { FileName = "whoami", RedirectStandardOutput = true, } }) { process.Start(); process.WaitForExit(10_000); return process.StandardOutput.ReadToEnd(); } } return null; } private void SetupNginx(string redirectUri, Uri originalUri) { using (Logger.BeginScope("SetupNginx")) { var userName = GetUserName() ?? throw new InvalidOperationException("Could not identify the current username"); // 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]", userName) .Replace("[errorlog]", errorLog) .Replace("[accesslog]", accessLog) .Replace("[listenPort]", originalUri.Port.ToString(CultureInfo.InvariantCulture) + (_portSelector != null ? " reuseport" : "")) .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)) { Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", DeploymentParameters.ServerConfigTemplateContent); } 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.StartAndCaptureOutAndErrToLogger("nginx start", Logger); runNginx.WaitForExit(_waitTime); if (runNginx.ExitCode != 0) { throw new InvalidOperationException("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 (File.Exists(_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); } _portSelector?.Dispose(); base.Dispose(); } } }