| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- // 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.Collections.Generic;
- using System.Diagnostics;
- using System.IO;
- using System.Linq;
- using System.ServiceProcess;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Xml.Linq;
- using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
- using Microsoft.Extensions.Logging;
- using Microsoft.Web.Administration;
- namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
- {
- /// <summary>
- /// Deployer for IIS.
- /// </summary>
- public class IISDeployer : IISDeployerBase
- {
- private const string DetailedErrorsEnvironmentVariable = "ASPNETCORE_DETAILEDERRORS";
- private static readonly TimeSpan _timeout = TimeSpan.FromSeconds(60);
- private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(200);
- private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource();
- private string _configPath;
- private string _debugLogFile;
- public Process HostProcess { get; set; }
- public IISDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
- : base(new IISDeploymentParameters(deploymentParameters), loggerFactory)
- {
- }
- public IISDeployer(IISDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
- : base(deploymentParameters, loggerFactory)
- {
- }
- public override void Dispose()
- {
- Dispose(gracefulShutdown: false);
- }
- public override void Dispose(bool gracefulShutdown)
- {
- Stop();
- TriggerHostShutdown(_hostShutdownToken);
- GetLogsFromFile();
- CleanPublishedOutput();
- InvokeUserApplicationCleanup();
- StopTimer();
- }
- public override Task<DeploymentResult> DeployAsync()
- {
- using (Logger.BeginScope("Deployment"))
- {
- StartTimer();
- if (string.IsNullOrEmpty(DeploymentParameters.ServerConfigTemplateContent))
- {
- DeploymentParameters.ServerConfigTemplateContent = File.ReadAllText("IIS.config");
- }
- // For now, only support using published output
- DeploymentParameters.PublishApplicationBeforeDeployment = true;
- // Move ASPNETCORE_DETAILEDERRORS to web config env variables
- if (IISDeploymentParameters.EnvironmentVariables.ContainsKey(DetailedErrorsEnvironmentVariable))
- {
- IISDeploymentParameters.WebConfigBasedEnvironmentVariables[DetailedErrorsEnvironmentVariable] =
- IISDeploymentParameters.EnvironmentVariables[DetailedErrorsEnvironmentVariable];
- IISDeploymentParameters.EnvironmentVariables.Remove(DetailedErrorsEnvironmentVariable);
- }
- // Do not override settings set on parameters
- if (!IISDeploymentParameters.HandlerSettings.ContainsKey("debugLevel") &&
- !IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile"))
- {
- _debugLogFile = Path.GetTempFileName();
- IISDeploymentParameters.HandlerSettings["debugLevel"] = "file";
- IISDeploymentParameters.HandlerSettings["debugFile"] = _debugLogFile;
- }
- DotnetPublish();
- var contentRoot = DeploymentParameters.PublishedApplicationRootPath;
- RunWebConfigActions(contentRoot);
- var uri = TestUriHelper.BuildTestUri(ServerType.IIS, DeploymentParameters.ApplicationBaseUriHint);
- StartIIS(uri, contentRoot);
- // Warm up time for IIS setup.
- Logger.LogInformation("Successfully finished IIS application directory setup.");
- return Task.FromResult<DeploymentResult>(new IISDeploymentResult(
- LoggerFactory,
- IISDeploymentParameters,
- applicationBaseUri: uri.ToString(),
- contentRoot: contentRoot,
- hostShutdownToken: _hostShutdownToken.Token,
- hostProcess: HostProcess
- ));
- }
- }
- protected override IEnumerable<Action<XElement, string>> GetWebConfigActions()
- {
- yield return WebConfigHelpers.AddOrModifyAspNetCoreSection(
- key: "hostingModel",
- value: DeploymentParameters.HostingModel.ToString());
- yield return (element, _) => {
- var aspNetCore = element
- .Descendants("system.webServer")
- .Single()
- .GetOrAdd("aspNetCore");
- // Expand path to dotnet because IIS process would not inherit PATH variable
- if (aspNetCore.Attribute("processPath")?.Value.StartsWith("dotnet") == true)
- {
- aspNetCore.SetAttributeValue("processPath", DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture));
- }
- };
- yield return WebConfigHelpers.AddOrModifyHandlerSection(
- key: "modules",
- value: DeploymentParameters.AncmVersion.ToString());
- foreach (var action in base.GetWebConfigActions())
- {
- yield return action;
- }
- }
- private void GetLogsFromFile()
- {
- try
- {
- // Handle cases where debug file is redirected by test
- var debugLogLocations = new List<string>();
- if (IISDeploymentParameters.HandlerSettings.ContainsKey("debugFile"))
- {
- 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))
- {
- continue;
- }
- var file = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, debugLogLocation);
- if (File.Exists(file))
- {
- 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;
- }
- }
- // 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);
- }
- }
- }
- public void StartIIS(Uri uri, string contentRoot)
- {
- // Backup currently deployed apphost.config file
- using (Logger.BeginScope("StartIIS"))
- {
- var port = uri.Port;
- if (port == 0)
- {
- throw new NotSupportedException("Cannot set port 0 for IIS.");
- }
- AddTemporaryAppHostConfig(contentRoot, port);
- WaitUntilSiteStarted();
- }
- }
- private void WaitUntilSiteStarted()
- {
- ServiceController serviceController = new ServiceController("w3svc");
- Logger.LogInformation("W3SVC status " + serviceController.Status);
- if (serviceController.Status != ServiceControllerStatus.Running &&
- serviceController.Status != ServiceControllerStatus.StartPending)
- {
- Logger.LogInformation("Starting W3SVC");
- serviceController.Start();
- serviceController.WaitForStatus(ServiceControllerStatus.Running, _timeout);
- }
- RetryServerManagerAction(serverManager =>
- {
- var site = serverManager.Sites.Single();
- var appPool = serverManager.ApplicationPools.Single();
- if (appPool.State != ObjectState.Started && appPool.State != ObjectState.Starting)
- {
- var state = appPool.Start();
- Logger.LogInformation($"Starting pool, state: {state.ToString()}");
- }
- if (site.State != ObjectState.Started && site.State != ObjectState.Starting)
- {
- var state = site.Start();
- Logger.LogInformation($"Starting site, state: {state.ToString()}");
- }
- if (site.State != ObjectState.Started)
- {
- throw new InvalidOperationException("Site not started yet");
- }
- var workerProcess = appPool.WorkerProcesses.SingleOrDefault();
- if (workerProcess == null)
- {
- throw new InvalidOperationException("Site is started but no worked process found");
- }
- HostProcess = Process.GetProcessById(workerProcess.ProcessId);
- // Ensure w3wp.exe is killed if test process termination is non-graceful.
- // Prevents locked files when stop debugging unit test.
- ProcessTracker.Add(HostProcess);
- // cache the process start time for verifying log file name.
- var _ = HostProcess.StartTime;
- Logger.LogInformation("Site has started.");
- });
- }
- private void AddTemporaryAppHostConfig(string contentRoot, int port)
- {
- _configPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("D"));
- var appHostConfigPath = Path.Combine(_configPath, "applicationHost.config");
- Directory.CreateDirectory(_configPath);
- var config = XDocument.Parse(DeploymentParameters.ServerConfigTemplateContent ?? File.ReadAllText("IIS.config"));
- ConfigureAppHostConfig(config.Root, contentRoot, port);
- config.Save(appHostConfigPath);
- RetryServerManagerAction(serverManager =>
- {
- var redirectionConfiguration = serverManager.GetRedirectionConfiguration();
- var redirectionSection = redirectionConfiguration.GetSection("configurationRedirection");
- redirectionSection.Attributes["enabled"].Value = true;
- redirectionSection.Attributes["path"].Value = _configPath;
- serverManager.CommitChanges();
- });
- }
- private void ConfigureAppHostConfig(XElement config, string contentRoot, int port)
- {
- ConfigureModuleAndBinding(config, contentRoot, port);
- // In IISExpress system.webServer/modules in under location element
- config
- .RequiredElement("system.webServer")
- .RequiredElement("modules")
- .GetOrAdd("add", "name", DeploymentParameters.AncmVersion.ToString());
- var pool = config
- .RequiredElement("system.applicationHost")
- .RequiredElement("applicationPools")
- .RequiredElement("add");
- if (DeploymentParameters.EnvironmentVariables.Any())
- {
- var environmentVariables = pool
- .GetOrAdd("environmentVariables");
- foreach (var tuple in DeploymentParameters.EnvironmentVariables)
- {
- environmentVariables
- .GetOrAdd("add", "name", tuple.Key)
- .SetAttributeValue("value", tuple.Value);
- }
- }
- if (DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x86)
- {
- pool.SetAttributeValue("enable32BitAppOnWin64", "true");;
- }
- RunServerConfigActions(config, contentRoot);
- }
- private void Stop()
- {
- try
- {
- RetryServerManagerAction(serverManager =>
- {
- var site = serverManager.Sites.SingleOrDefault();
- if (site == null)
- {
- throw new InvalidOperationException("Site not found");
- }
- if (site.State != ObjectState.Stopped && site.State != ObjectState.Stopping)
- {
- var state = site.Stop();
- Logger.LogInformation($"Stopping site, state: {state.ToString()}");
- }
- var appPool = serverManager.ApplicationPools.SingleOrDefault();
- if (appPool == null)
- {
- throw new InvalidOperationException("Application pool not found");
- }
- if (appPool.State != ObjectState.Stopped && appPool.State != ObjectState.Stopping)
- {
- var state = appPool.Stop();
- Logger.LogInformation($"Stopping pool, state: {state.ToString()}");
- }
- if (site.State != ObjectState.Stopped)
- {
- throw new InvalidOperationException("Site not stopped yet");
- }
- try
- {
- if (appPool.WorkerProcesses != null &&
- appPool.WorkerProcesses.Any(wp =>
- wp.State == WorkerProcessState.Running ||
- wp.State == WorkerProcessState.Stopping))
- {
- throw new InvalidOperationException("WorkerProcess not stopped yet");
- }
- }
- // If WAS was stopped for some reason appPool.WorkerProcesses
- // would throw UnauthorizedAccessException.
- // check if it's the case and continue shutting down deployer
- catch (UnauthorizedAccessException)
- {
- var serviceController = new ServiceController("was");
- if (serviceController.Status != ServiceControllerStatus.Stopped)
- {
- throw;
- }
- }
- if (!HostProcess.HasExited)
- {
- throw new InvalidOperationException("Site is stopped but host process is not");
- }
- Logger.LogInformation($"Site has stopped successfully.");
- });
- }
- finally
- {
- // Undo redirection.config changes unconditionally
- RetryServerManagerAction(serverManager =>
- {
- var redirectionConfiguration = serverManager.GetRedirectionConfiguration();
- var redirectionSection = redirectionConfiguration.GetSection("configurationRedirection");
- redirectionSection.Attributes["enabled"].Value = false;
- serverManager.CommitChanges();
- if (Directory.Exists(_configPath))
- {
- Directory.Delete(_configPath, true);
- }
- });
- }
- }
- private void RetryServerManagerAction(Action<ServerManager> action)
- {
- List<Exception> exceptions = null;
- var sw = Stopwatch.StartNew();
- int retryCount = 0;
- while (sw.Elapsed < _timeout)
- {
- try
- {
- using (var serverManager = new ServerManager())
- {
- action(serverManager);
- }
- return;
- }
- catch (Exception ex)
- {
- if (exceptions == null)
- {
- exceptions = new List<Exception>();
- }
- exceptions.Add(ex);
- }
- retryCount++;
- Thread.Sleep(_retryDelay);
- }
- throw new AggregateException($"Operation did not succeed after {retryCount} retries", exceptions.ToArray());
- }
- }
- }
|