Allow running some IIS Express variants without publishing #1431

This commit is contained in:
Chris Ross (ASP.NET) 2018-05-25 15:19:25 -07:00
parent 23e3ab6555
commit 8d55a447d4
9 changed files with 1229 additions and 91 deletions

View File

@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
public string ServerConfigLocation { get; set; }
public string SiteName { get; set; }
public string SiteName { get; set; } = "HttpTestSite";
public string ApplicationPath { get; set; }

View File

@ -5,7 +5,7 @@ using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
internal static class DotNetCommands
{

View File

@ -161,6 +161,22 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
}
protected string GetDotNetExeForArchitecture()
{
var executableName = DotnetCommandName;
// We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version.
if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture))
{
executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture);
if (!File.Exists(executableName))
{
throw new Exception($"Unable to find '{executableName}'.'");
}
}
return executableName;
}
protected void ShutDownIfAnyHostProcess(Process hostProcess)
{
if (hostProcess != null && !hostProcess.HasExited)

View File

@ -3,9 +3,9 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@ -42,15 +42,52 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
// Start timer
StartTimer();
// For now we always auto-publish. Otherwise we'll have to write our own local web.config for the HttpPlatformHandler
DotnetPublish();
// For an unpublished application the dllroot points pre-built dlls like projectdir/bin/debug/net461/
// and contentRoot points to the project directory so you get things like static assets.
// For a published app both point to the publish directory.
var dllRoot = CheckIfPublishIsRequired();
var contentRoot = string.Empty;
if (DeploymentParameters.PublishApplicationBeforeDeployment)
{
DotnetPublish();
contentRoot = DeploymentParameters.PublishedApplicationRootPath;
dllRoot = contentRoot;
}
else
{
// Core+Standalone always publishes. This must be Clr+Standalone or Core+Portable.
// Update processPath and arguments for our current scenario
contentRoot = DeploymentParameters.ApplicationPath;
var contentRoot = DeploymentParameters.PublishedApplicationRootPath;
var executableExtension = DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll" : ".exe";
var entryPoint = Path.Combine(dllRoot, DeploymentParameters.ApplicationName + executableExtension);
var executableName = string.Empty;
var executableArgs = string.Empty;
if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable)
{
executableName = GetDotNetExeForArchitecture();
executableArgs = entryPoint;
}
else
{
executableName = entryPoint;
}
Logger.LogInformation("Executing: {exe} {args}", executableName, executableArgs);
DeploymentParameters.EnvironmentVariables["LAUNCHER_PATH"] = executableName;
DeploymentParameters.EnvironmentVariables["LAUNCHER_ARGS"] = executableArgs;
// CurrentDirectory will point to bin/{config}/{tfm}, but the config and static files aren't copied, point to the app base instead.
Logger.LogInformation("ContentRoot: {path}", DeploymentParameters.ApplicationPath);
DeploymentParameters.EnvironmentVariables["ASPNETCORE_CONTENTROOT"] = DeploymentParameters.ApplicationPath;
}
var testUri = TestUriHelper.BuildTestUri(ServerType.IISExpress, DeploymentParameters.ApplicationBaseUriHint);
// Launch the host process.
var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot);
var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot, dllRoot);
Logger.LogInformation("Application ready at URL: {appUrl}", actualUri);
@ -64,7 +101,43 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
}
private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(Uri uri, string contentRoot)
private string CheckIfPublishIsRequired()
{
var targetFramework = DeploymentParameters.TargetFramework;
// IISIntegration uses this layout
var dllRoot = Path.Combine(DeploymentParameters.ApplicationPath, "bin", DeploymentParameters.RuntimeArchitecture.ToString(),
DeploymentParameters.Configuration, targetFramework);
if (!Directory.Exists(dllRoot))
{
// Most repos use this layout
dllRoot = Path.Combine(DeploymentParameters.ApplicationPath, "bin", DeploymentParameters.Configuration, targetFramework);
if (!Directory.Exists(dllRoot))
{
// The bits we need weren't pre-compiled, compile on publish
DeploymentParameters.PublishApplicationBeforeDeployment = true;
}
else if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr
&& DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x86)
{
// x64 is the default. Publish to rebuild for the right bitness
DeploymentParameters.PublishApplicationBeforeDeployment = true;
}
}
if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr
&& DeploymentParameters.ApplicationType == ApplicationType.Standalone)
{
// Publish is always required to get the correct standalone files in the output directory
DeploymentParameters.PublishApplicationBeforeDeployment = true;
}
return dllRoot;
}
private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(Uri uri, string contentRoot, string dllRoot)
{
using (Logger.BeginScope("StartIISExpress"))
{
@ -74,60 +147,17 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
port = (uri.Scheme == "https") ? TestPortHelper.GetNextSSLPort() : TestPortHelper.GetNextPort();
}
Logger.LogInformation("Attempting to start IIS Express on port: {port}", port);
PrepareConfig(contentRoot, dllRoot, port);
var parameters = string.IsNullOrEmpty(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();
for (var attempt = 0; attempt < MaximumAttempts; attempt++)
{
Logger.LogInformation("Attempting to start IIS Express on port: {port}", port);
if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigTemplateContent))
{
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.
serverConfig = ModifyANCMPathInConfig(replaceFlag: "[ANCMPath]", dllName: "aspnetcore.dll", serverConfig, contentRoot);
serverConfig = ModifyANCMPathInConfig(replaceFlag: "[ANCMV2Path]", dllName: "aspnetcorev2.dll", serverConfig, contentRoot);
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();
if (serverConfig.Contains("[HostingModel]"))
{
var hostingModel = DeploymentParameters.HostingModel.ToString();
serverConfig.Replace("[HostingModel]", hostingModel);
Logger.LogDebug("Writing HostingModel '{hostingModel}' to config", hostingModel);
}
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);
}
if (DeploymentParameters.HostingModel == HostingModel.InProcess)
{
ModifyAspNetCoreSectionInWebConfig(key: "hostingModel", value: "inprocess");
}
ModifyHandlerSectionInWebConfig(key: "modules", value: DeploymentParameters.AncmVersion.ToString());
ModifyDotNetExePathInWebConfig();
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
@ -197,7 +227,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
// just in case we missed one -anurse
if (!await started.Task.TimeoutAfter(TimeSpan.FromMinutes(10)))
{
Logger.LogInformation("iisexpress Process {pid} failed to bind to port {port}, trying again", _hostProcess.Id, port);
Logger.LogInformation("iisexpress Process {pid} failed to bind to port {port}, trying again", process.Id, port);
// Wait for the process to exit and try again
process.WaitForExit(30 * 1000);
@ -217,15 +247,69 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
}
private string ModifyANCMPathInConfig(string replaceFlag, string dllName, string serverConfig, string contentRoot)
private void PrepareConfig(string contentRoot, string dllRoot, int port)
{
// Config is required. If not present then fall back to one we carry with us.
if (string.IsNullOrEmpty(DeploymentParameters.ServerConfigTemplateContent))
{
using (var stream = GetType().Assembly.GetManifestResourceStream("Microsoft.AspNetCore.Server.IntegrationTesting.Http.config"))
using (var reader = new StreamReader(stream))
{
DeploymentParameters.ServerConfigTemplateContent = reader.ReadToEnd();
}
}
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.
serverConfig = ModifyANCMPathInConfig(replaceFlag: "[ANCMPath]", dllName: "aspnetcore.dll", serverConfig, dllRoot);
serverConfig = ModifyANCMPathInConfig(replaceFlag: "[ANCMV2Path]", dllName: "aspnetcorev2.dll", serverConfig, dllRoot);
serverConfig = ReplacePlaceholder(serverConfig, "[PORT]", port.ToString(CultureInfo.InvariantCulture));
serverConfig = ReplacePlaceholder(serverConfig, "[ApplicationPhysicalPath]", contentRoot);
if (DeploymentParameters.PublishApplicationBeforeDeployment)
{
// For published apps, prefer the content in the web.config, but update it.
ModifyAspNetCoreSectionInWebConfig(key: "hostingModel",
value: DeploymentParameters.HostingModel == HostingModel.InProcess ? "inprocess" : "");
ModifyHandlerSectionInWebConfig(key: "modules", value: DeploymentParameters.AncmVersion.ToString());
ModifyDotNetExePathInWebConfig();
serverConfig = RemoveRedundantElements(serverConfig);
}
else
{
// The elements normally in the web.config are in the applicationhost.config for unpublished apps.
serverConfig = ReplacePlaceholder(serverConfig, "[HostingModel]", DeploymentParameters.HostingModel.ToString());
serverConfig = ReplacePlaceholder(serverConfig, "[AspNetCoreModule]", DeploymentParameters.AncmVersion.ToString());
}
DeploymentParameters.ServerConfigLocation = Path.GetTempFileName();
Logger.LogDebug("Saving Config to {configPath}", DeploymentParameters.ServerConfigLocation);
File.WriteAllText(DeploymentParameters.ServerConfigLocation, serverConfig);
}
private string ReplacePlaceholder(string content, string field, string value)
{
if (content.Contains(field))
{
content = content.Replace(field, value);
Logger.LogDebug("Writing {field} '{value}' to config", field, value);
}
return content;
}
private string ModifyANCMPathInConfig(string replaceFlag, string dllName, string serverConfig, string dllRoot)
{
if (serverConfig.Contains(replaceFlag))
{
var arch = DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x64 ? $@"x64\{dllName}" : $@"x86\{dllName}";
var ancmFile = Path.Combine(contentRoot, arch);
var ancmFile = Path.Combine(dllRoot, arch);
if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile)))
{
ancmFile = Path.Combine(contentRoot, dllName);
ancmFile = Path.Combine(dllRoot, dllName);
if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile)))
{
throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmFile);
@ -263,7 +347,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
ShutDownIfAnyHostProcess(_hostProcess);
if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation)
if (!string.IsNullOrEmpty(DeploymentParameters.ServerConfigLocation)
&& File.Exists(DeploymentParameters.ServerConfigLocation))
{
// Delete the temp applicationHostConfig that we created.
@ -317,7 +401,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
// Transforms the web.config file to set attributes like hostingModel="inprocess" element
private void ModifyAspNetCoreSectionInWebConfig(string key, string value)
{
var webConfigFile = $"{DeploymentParameters.PublishedApplicationRootPath}/web.config";
var webConfigFile = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, "web.config");
var config = XDocument.Load(webConfigFile);
var element = config.Descendants("aspNetCore").FirstOrDefault();
element.SetAttributeValue(key, value);
@ -326,11 +410,27 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
private void ModifyHandlerSectionInWebConfig(string key, string value)
{
var webConfigFile = $"{DeploymentParameters.PublishedApplicationRootPath}/web.config";
var webConfigFile = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, "web.config");
var config = XDocument.Load(webConfigFile);
var element = config.Descendants("handlers").FirstOrDefault().Descendants("add").FirstOrDefault();
element.SetAttributeValue(key, value);
config.Save(webConfigFile);
config.Save(webConfigFile);
}
// These elements are duplicated in the web.config if you publish. Remove them from the host.config.
private string RemoveRedundantElements(string serverConfig)
{
var hostConfig = XDocument.Parse(serverConfig);
var coreElement = hostConfig.Descendants("aspNetCore").FirstOrDefault();
coreElement?.Remove();
var handlersElement = hostConfig.Descendants("handlers").First();
var handlerElement = handlersElement.Descendants("add")
.Where(x => x.Attribute("name").Value == "aspNetCore").FirstOrDefault();
handlerElement?.Remove();
return hostConfig.ToString();
}
}
}

View File

@ -39,37 +39,37 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
$" Supported server types are {nameof(ServerType.Kestrel)}, {nameof(ServerType.IIS)} and {nameof(ServerType.HttpSys)}");
}
if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerName))
if (string.IsNullOrEmpty(_deploymentParameters.ServerName))
{
throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerName}' for {nameof(RemoteWindowsDeploymentParameters.ServerName)}");
}
if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerAccountName))
if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountName))
{
throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountName}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountName)}." +
" Account credentials are required to enable creating a powershell session to the remote server.");
}
if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerAccountPassword))
if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountPassword))
{
throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountPassword}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountPassword)}." +
" Account credentials are required to enable creating a powershell session to the remote server.");
}
if (_deploymentParameters.ApplicationType == ApplicationType.Portable
&& string.IsNullOrWhiteSpace(_deploymentParameters.DotnetRuntimePath))
&& string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath))
{
throw new ArgumentException($"Invalid value '{_deploymentParameters.DotnetRuntimePath}' for {nameof(RemoteWindowsDeploymentParameters.DotnetRuntimePath)}. " +
"It must be non-empty for portable apps.");
}
if (string.IsNullOrWhiteSpace(_deploymentParameters.RemoteServerFileSharePath))
if (string.IsNullOrEmpty(_deploymentParameters.RemoteServerFileSharePath))
{
throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.RemoteServerFileSharePath)}." +
" . A file share is required to copy the application's published output.");
}
if (string.IsNullOrWhiteSpace(_deploymentParameters.ApplicationBaseUriHint))
if (string.IsNullOrEmpty(_deploymentParameters.ApplicationBaseUriHint))
{
throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.ApplicationBaseUriHint)}.");
}

View File

@ -194,22 +194,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
}
private string GetDotNetExeForArchitecture()
{
var executableName = DotnetCommandName;
// We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version.
if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture))
{
executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture);
if (!File.Exists(executableName))
{
throw new Exception($"Unable to find '{executableName}'.'");
}
}
return executableName;
}
public override void Dispose()
{
using (Logger.BeginScope("SelfHost.Dispose"))

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,10 @@
<EnableApiCheck>false</EnableApiCheck>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Http.config" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Deployers\RemoteWindowsDeployer\RemotePSSessionHelper.ps1;Deployers\RemoteWindowsDeployer\StartServer.ps1;Deployers\RemoteWindowsDeployer\StopServer.ps1" Exclude="bin\**;obj\**;**\*.xproj;packages\**;@(EmbeddedResource)" />
</ItemGroup>

View File

@ -305,7 +305,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
if (hostingModel == HostingModel.InProcess)
{
// Not supported
if (Tfm.Matches(Tfm.Net461, tfm) || version == AncmVersion.AspNetCoreModule)
if (Tfm.Matches(Tfm.Net461, tfm) || Tfm.Matches(Tfm.NetCoreApp20, tfm) || version == AncmVersion.AspNetCoreModule)
{
continue;
}