349 lines
14 KiB
C#
349 lines
14 KiB
C#
// 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.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using System.Xml.Linq;
|
|
using Microsoft.Extensions.CommandLineUtils;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Web.Administration;
|
|
|
|
namespace Microsoft.AspNetCore.Server.IntegrationTesting
|
|
{
|
|
/// <summary>
|
|
/// Represents the IIS website registered in the global applicationHost.config
|
|
/// </summary>
|
|
internal class IISApplication
|
|
{
|
|
private static readonly TimeSpan _timeout = TimeSpan.FromSeconds(5);
|
|
private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(200);
|
|
private readonly ServerManager _serverManager = new ServerManager();
|
|
private readonly DeploymentParameters _deploymentParameters;
|
|
private readonly ILogger _logger;
|
|
private readonly string _ancmVersion;
|
|
private readonly string _ancmDllName;
|
|
private readonly string _apphostConfigBackupPath;
|
|
private static readonly string _apphostConfigPath = Path.Combine(
|
|
Environment.SystemDirectory,
|
|
"inetsrv",
|
|
"config",
|
|
"applicationhost.config");
|
|
|
|
public IISApplication(DeploymentParameters deploymentParameters, ILogger logger)
|
|
{
|
|
_deploymentParameters = deploymentParameters;
|
|
_logger = logger;
|
|
_ancmVersion = deploymentParameters.AncmVersion.ToString();
|
|
_ancmDllName = deploymentParameters.AncmVersion == AncmVersion.AspNetCoreModuleV2 ? "aspnetcorev2.dll" : "aspnetcore.dll";
|
|
WebSiteName = CreateTestSiteName();
|
|
AppPoolName = $"{WebSiteName}Pool";
|
|
_apphostConfigBackupPath = Path.Combine(
|
|
Environment.SystemDirectory,
|
|
"inetsrv",
|
|
"config",
|
|
$"applicationhost.config.{WebSiteName}backup");
|
|
}
|
|
|
|
public string WebSiteName { get; }
|
|
|
|
public string AppPoolName { get; }
|
|
|
|
public async Task 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();
|
|
|
|
ConfigureAppPool(contentRoot);
|
|
|
|
ConfigureSite(contentRoot, port);
|
|
|
|
ConfigureAppHostConfig(contentRoot);
|
|
|
|
if (_deploymentParameters.ApplicationType == ApplicationType.Portable)
|
|
{
|
|
ModifyAspNetCoreSectionInWebConfig("processPath", DotNetMuxer.MuxerPathOrDefault());
|
|
}
|
|
|
|
_serverManager.CommitChanges();
|
|
|
|
await WaitUntilSiteStarted();
|
|
}
|
|
}
|
|
|
|
private void ModifyAspNetCoreSectionInWebConfig(string key, string value)
|
|
{
|
|
var webConfigFile = Path.Combine(_deploymentParameters.PublishedApplicationRootPath, "web.config");
|
|
var config = XDocument.Load(webConfigFile);
|
|
var element = config.Descendants("aspNetCore").FirstOrDefault();
|
|
element.SetAttributeValue(key, value);
|
|
config.Save(webConfigFile);
|
|
}
|
|
|
|
private async Task WaitUntilSiteStarted()
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
while (sw.Elapsed < _timeout)
|
|
{
|
|
try
|
|
{
|
|
var site = _serverManager.Sites.FirstOrDefault(s => s.Name.Equals(WebSiteName));
|
|
if (site.State == ObjectState.Started)
|
|
{
|
|
_logger.LogInformation($"Site {WebSiteName} has started.");
|
|
return;
|
|
}
|
|
else if (site.State != ObjectState.Starting)
|
|
{
|
|
_logger.LogInformation($"Site hasn't started with state: {site.State.ToString()}");
|
|
var state = site.Start();
|
|
_logger.LogInformation($"Tried to start site, state: {state.ToString()}");
|
|
}
|
|
}
|
|
catch (COMException comException)
|
|
{
|
|
// Accessing the site.State property while the site
|
|
// is starting up returns the COMException
|
|
// The object identifier does not represent a valid object.
|
|
// (Exception from HRESULT: 0x800710D8)
|
|
// This also means the site is not started yet, so catch and retry
|
|
// after waiting.
|
|
_logger.LogWarning($"ComException: {comException.Message}");
|
|
}
|
|
|
|
await Task.Delay(_retryDelay);
|
|
}
|
|
|
|
throw new TimeoutException($"IIS failed to start site.");
|
|
}
|
|
|
|
public async Task StopAndDeleteAppPool()
|
|
{
|
|
if (string.IsNullOrEmpty(WebSiteName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RestoreAppHostConfig();
|
|
|
|
_serverManager.CommitChanges();
|
|
|
|
await WaitUntilSiteStopped();
|
|
}
|
|
|
|
private async Task WaitUntilSiteStopped()
|
|
{
|
|
var site = _serverManager.Sites.Where(element => element.Name == WebSiteName).FirstOrDefault();
|
|
if (site == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
while (sw.Elapsed < _timeout)
|
|
{
|
|
try
|
|
{
|
|
if (site.State == ObjectState.Stopped)
|
|
{
|
|
_logger.LogInformation($"Site {WebSiteName} has stopped successfully.");
|
|
return;
|
|
}
|
|
}
|
|
catch (COMException)
|
|
{
|
|
// Accessing the site.State property while the site
|
|
// is shutdown down returns the COMException
|
|
return;
|
|
}
|
|
|
|
_logger.LogWarning($"IIS has not stopped after {sw.Elapsed.TotalMilliseconds}");
|
|
await Task.Delay(_retryDelay);
|
|
}
|
|
|
|
throw new TimeoutException($"IIS failed to stop site {site}.");
|
|
}
|
|
|
|
private void AddTemporaryAppHostConfig()
|
|
{
|
|
RetryFileOperation(() => File.Move(_apphostConfigPath, _apphostConfigBackupPath),
|
|
e => _logger.LogWarning($"Failed to backup apphost.config: {e.Message}"));
|
|
|
|
_logger.LogInformation($"Backed up {_apphostConfigPath} to {_apphostConfigBackupPath}");
|
|
|
|
RetryFileOperation(
|
|
() => File.WriteAllText(_apphostConfigPath, _deploymentParameters.ServerConfigTemplateContent ?? File.ReadAllText("IIS.config")),
|
|
e => _logger.LogWarning($"Failed to copy IIS.config to apphost.config: {e.Message}"));
|
|
|
|
_logger.LogInformation($"Copied contents of IIS.config to {_apphostConfigPath}");
|
|
}
|
|
|
|
private void RestoreAppHostConfig()
|
|
{
|
|
if (File.Exists(_apphostConfigPath))
|
|
{
|
|
RetryFileOperation(
|
|
() => File.Delete(_apphostConfigPath),
|
|
e => _logger.LogWarning($"Failed to delete file : {e.Message}"));
|
|
}
|
|
|
|
RetryFileOperation(
|
|
() => File.Move(_apphostConfigBackupPath, _apphostConfigPath),
|
|
e => _logger.LogError($"Failed to backup apphost.config: {e.Message}"));
|
|
|
|
_logger.LogInformation($"Restored {_apphostConfigPath}.");
|
|
}
|
|
|
|
private void RetryFileOperation(Action retryBlock, Action<Exception> exceptionBlock)
|
|
{
|
|
RetryHelper.RetryOperation(retryBlock,
|
|
exceptionBlock,
|
|
retryCount: 10,
|
|
retryDelayMilliseconds: 100);
|
|
}
|
|
|
|
private ApplicationPool ConfigureAppPool(string contentRoot)
|
|
{
|
|
try
|
|
{
|
|
var pool = _serverManager.ApplicationPools.Add(AppPoolName);
|
|
pool.ProcessModel.IdentityType = ProcessModelIdentityType.LocalSystem;
|
|
pool.ManagedRuntimeVersion = string.Empty;
|
|
|
|
AddEnvironmentVariables(contentRoot, pool);
|
|
|
|
_logger.LogInformation($"Configured AppPool {AppPoolName}");
|
|
return pool;
|
|
}
|
|
catch (COMException comException)
|
|
{
|
|
_logger.LogError(File.ReadAllText(_apphostConfigPath));
|
|
throw comException;
|
|
}
|
|
}
|
|
|
|
private void AddEnvironmentVariables(string contentRoot, ApplicationPool pool)
|
|
{
|
|
try
|
|
{
|
|
var envCollection = pool.GetCollection("environmentVariables");
|
|
|
|
foreach (var tuple in _deploymentParameters.EnvironmentVariables)
|
|
{
|
|
AddEnvironmentVariableToAppPool(envCollection, tuple.Key, tuple.Value);
|
|
}
|
|
AddEnvironmentVariableToAppPool(envCollection, "ASPNETCORE_MODULE_DEBUG_FILE", $"{WebSiteName}.txt");
|
|
}
|
|
catch (COMException comException)
|
|
{
|
|
_logger.LogInformation($"Could not add environment variables to worker process: {comException.Message}");
|
|
}
|
|
}
|
|
|
|
private static void AddEnvironmentVariableToAppPool(ConfigurationElementCollection envCollection, string key, string value)
|
|
{
|
|
var addElement = envCollection.CreateElement("add");
|
|
addElement["name"] = key;
|
|
addElement["value"] = value;
|
|
envCollection.Add(addElement);
|
|
}
|
|
|
|
private Site ConfigureSite(string contentRoot, int port)
|
|
{
|
|
var site = _serverManager.Sites.Add(WebSiteName, contentRoot, port);
|
|
site.Applications.Single().ApplicationPoolName = AppPoolName;
|
|
_logger.LogInformation($"Configured Site {WebSiteName} with AppPool {AppPoolName}");
|
|
return site;
|
|
}
|
|
|
|
private Configuration ConfigureAppHostConfig(string dllRoot)
|
|
{
|
|
var config = _serverManager.GetApplicationHostConfiguration();
|
|
|
|
SetGlobalModuleSection(config, dllRoot);
|
|
|
|
SetModulesSection(config);
|
|
|
|
return config;
|
|
}
|
|
|
|
private void SetGlobalModuleSection(Configuration config, string dllRoot)
|
|
{
|
|
var ancmFile = GetAncmLocation(dllRoot);
|
|
|
|
var globalModulesSection = config.GetSection("system.webServer/globalModules");
|
|
var globalConfigElement = globalModulesSection
|
|
.GetCollection()
|
|
.Where(element => (string)element["name"] == _ancmVersion)
|
|
.FirstOrDefault();
|
|
|
|
if (globalConfigElement == null)
|
|
{
|
|
_logger.LogInformation($"Could not find {_ancmVersion} section in global modules; creating section.");
|
|
var addElement = globalModulesSection.GetCollection().CreateElement("add");
|
|
addElement["name"] = _ancmVersion;
|
|
addElement["image"] = ancmFile;
|
|
globalModulesSection.GetCollection().Add(addElement);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation($"Replacing {_ancmVersion} section in global modules with {ancmFile}");
|
|
globalConfigElement["image"] = ancmFile;
|
|
}
|
|
}
|
|
|
|
private void SetModulesSection(Configuration config)
|
|
{
|
|
var modulesSection = config.GetSection("system.webServer/modules");
|
|
var moduleConfigElement = modulesSection.GetCollection().Where(element => (string)element["name"] == _ancmVersion).FirstOrDefault();
|
|
if (moduleConfigElement == null)
|
|
{
|
|
_logger.LogInformation($"Could not find {_ancmVersion} section in modules; creating section.");
|
|
var moduleElement = modulesSection.GetCollection().CreateElement("add");
|
|
moduleElement["name"] = _ancmVersion;
|
|
modulesSection.GetCollection().Add(moduleElement);
|
|
}
|
|
}
|
|
|
|
private string CreateTestSiteName()
|
|
{
|
|
if (!string.IsNullOrEmpty(_deploymentParameters.SiteName))
|
|
{
|
|
return $"{_deploymentParameters.SiteName}{DateTime.Now.ToString("yyyyMMddHHmmss")}";
|
|
}
|
|
else
|
|
{
|
|
return $"testsite{DateTime.Now.ToString("yyyyMMddHHmmss")}";
|
|
}
|
|
}
|
|
|
|
private string GetAncmLocation(string dllRoot)
|
|
{
|
|
var arch = _deploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x64 ? $@"x64\{_ancmDllName}" : $@"x86\{_ancmDllName}";
|
|
var ancmFile = Path.Combine(dllRoot, arch);
|
|
if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile)))
|
|
{
|
|
ancmFile = Path.Combine(dllRoot, _ancmDllName);
|
|
if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile)))
|
|
{
|
|
throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmFile);
|
|
}
|
|
}
|
|
|
|
return ancmFile;
|
|
}
|
|
}
|
|
}
|