Merge branch 'merge/release/2.2-to-master'

This commit is contained in:
Pavel Krymets 2018-08-01 13:46:05 -07:00
commit 44056697e6
8 changed files with 307 additions and 124 deletions

View File

@ -0,0 +1,127 @@
// 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.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
public class ApplicationPublisher
{
public string ApplicationPath { get; }
public ApplicationPublisher(string applicationPath)
{
ApplicationPath = applicationPath;
}
public static readonly string DotnetCommandName = "dotnet";
public virtual Task<PublishedApplication> Publish(DeploymentParameters deploymentParameters, ILogger logger)
{
var publishDirectory = CreateTempDirectory();
using (logger.BeginScope("dotnet-publish"))
{
if (string.IsNullOrEmpty(deploymentParameters.TargetFramework))
{
throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment");
}
var parameters = $"publish "
+ $" --output \"{publishDirectory.FullName}\""
+ $" --framework {deploymentParameters.TargetFramework}"
+ $" --configuration {deploymentParameters.Configuration}"
+ (deploymentParameters.RestoreOnPublish
? string.Empty
: " --no-restore -p:VerifyMatchingImplicitPackageVersion=false");
// Set VerifyMatchingImplicitPackageVersion to disable errors when Microsoft.NETCore.App's version is overridden externally
// This verification doesn't matter if we are skipping restore during tests.
if (deploymentParameters.ApplicationType == ApplicationType.Standalone)
{
parameters += $" --runtime {GetRuntimeIdentifier(deploymentParameters)}";
}
parameters += $" {deploymentParameters.AdditionalPublishParameters}";
var startInfo = new ProcessStartInfo
{
FileName = DotnetCommandName,
Arguments = parameters,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = deploymentParameters.ApplicationPath,
};
ProcessHelpers.AddEnvironmentVariablesToProcess(startInfo, deploymentParameters.PublishEnvironmentVariables, logger);
var hostProcess = new Process() { StartInfo = startInfo };
logger.LogInformation($"Executing command {DotnetCommandName} {parameters}");
hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", logger);
// A timeout is passed to Process.WaitForExit() for two reasons:
//
// 1. When process output is read asynchronously, WaitForExit() without a timeout blocks until child processes
// are killed, which can cause hangs due to MSBuild NodeReuse child processes started by dotnet.exe.
// With a timeout, WaitForExit() returns when the parent process is killed and ignores child processes.
// https://stackoverflow.com/a/37983587/102052
//
// 2. If "dotnet publish" does hang indefinitely for some reason, tests should fail fast with an error message.
const int timeoutMinutes = 5;
if (hostProcess.WaitForExit(milliseconds: timeoutMinutes * 60 * 1000))
{
if (hostProcess.ExitCode != 0)
{
var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}";
logger.LogError(message);
throw new Exception(message);
}
}
else
{
var message = $"{DotnetCommandName} publish failed to exit after {timeoutMinutes} minutes";
logger.LogError(message);
throw new Exception(message);
}
logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}");
}
return Task.FromResult(new PublishedApplication(publishDirectory.FullName, logger));
}
private static string GetRuntimeIdentifier(DeploymentParameters deploymentParameters)
{
var architecture = deploymentParameters.RuntimeArchitecture;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "win7-" + architecture;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux-" + architecture;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "osx-" + architecture;
}
throw new InvalidOperationException("Unrecognized operation system platform");
}
protected static DirectoryInfo CreateTempDirectory()
{
var tempPath = Path.GetTempPath() + Guid.NewGuid().ToString("N");
var target = new DirectoryInfo(tempPath);
target.Create();
return target;
}
}
}

View File

@ -0,0 +1,101 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
public class CachingApplicationPublisher: ApplicationPublisher, IDisposable
{
private readonly Dictionary<DotnetPublishParameters, PublishedApplication> _publishCache = new Dictionary<DotnetPublishParameters, PublishedApplication>();
public CachingApplicationPublisher(string applicationPath) : base(applicationPath)
{
}
public override async Task<PublishedApplication> Publish(DeploymentParameters deploymentParameters, ILogger logger)
{
if (ApplicationPath != deploymentParameters.ApplicationPath)
{
throw new InvalidOperationException("ApplicationPath mismatch");
}
if (deploymentParameters.PublishEnvironmentVariables.Any())
{
throw new InvalidOperationException("DeploymentParameters.PublishEnvironmentVariables not supported");
}
if (!string.IsNullOrEmpty(deploymentParameters.PublishedApplicationRootPath))
{
throw new InvalidOperationException("DeploymentParameters.PublishedApplicationRootPath not supported");
}
if (deploymentParameters.RestoreOnPublish)
{
throw new InvalidOperationException("DeploymentParameters.RestoreOnPublish not supported");
}
var dotnetPublishParameters = new DotnetPublishParameters
{
TargetFramework = deploymentParameters.TargetFramework,
Configuration = deploymentParameters.Configuration,
ApplicationType = deploymentParameters.ApplicationType,
RuntimeArchitecture = deploymentParameters.RuntimeArchitecture
};
if (!_publishCache.TryGetValue(dotnetPublishParameters, out var publishedApplication))
{
publishedApplication = await base.Publish(deploymentParameters, logger);
_publishCache.Add(dotnetPublishParameters, publishedApplication);
}
return new PublishedApplication(CopyPublishedOutput(publishedApplication, logger), logger);
}
private string CopyPublishedOutput(PublishedApplication application, ILogger logger)
{
var target = CreateTempDirectory();
var source = new DirectoryInfo(application.Path);
CopyFiles(source, target, logger);
return target.FullName;
}
public static void CopyFiles(DirectoryInfo source, DirectoryInfo target, ILogger logger)
{
foreach (DirectoryInfo directoryInfo in source.GetDirectories())
{
CopyFiles(directoryInfo, target.CreateSubdirectory(directoryInfo.Name), logger);
}
logger.LogDebug($"Processing {target.FullName}");
foreach (FileInfo fileInfo in source.GetFiles())
{
logger.LogDebug($" Copying {fileInfo.Name}");
var destFileName = Path.Combine(target.FullName, fileInfo.Name);
fileInfo.CopyTo(destFileName);
}
}
public void Dispose()
{
foreach (var publishedApp in _publishCache.Values)
{
publishedApp.Dispose();
}
}
private struct DotnetPublishParameters
{
public string TargetFramework { get; set; }
public string Configuration { get; set; }
public ApplicationType ApplicationType { get; set; }
public RuntimeArchitecture RuntimeArchitecture { get; set; }
}
}
}

View File

@ -99,6 +99,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
}
public ApplicationPublisher ApplicationPublisher { get; set; }
public ServerType ServerType { get; set; }
public RuntimeFlavor RuntimeFlavor { get; set; }

View File

@ -20,12 +20,10 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
public static readonly string DotnetCommandName = "dotnet";
// This is the argument that separates the dotnet arguments for the args being passed to the
// app being run when running dotnet run
public static readonly string DotnetArgumentSeparator = "--";
private readonly Stopwatch _stopwatch = new Stopwatch();
private PublishedApplication _publishedApplication;
public ApplicationDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
{
DeploymentParameters = deploymentParameters;
@ -82,78 +80,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
protected void DotnetPublish(string publishRoot = null)
{
using (Logger.BeginScope("dotnet-publish"))
{
if (string.IsNullOrEmpty(DeploymentParameters.TargetFramework))
{
throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment");
}
DeploymentParameters.PublishedApplicationRootPath = publishRoot ?? Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var parameters = $"publish "
+ $" --output \"{DeploymentParameters.PublishedApplicationRootPath}\""
+ $" --framework {DeploymentParameters.TargetFramework}"
+ $" --configuration {DeploymentParameters.Configuration}"
+ (DeploymentParameters.RestoreOnPublish
? string.Empty
: " --no-restore -p:VerifyMatchingImplicitPackageVersion=false");
// Set VerifyMatchingImplicitPackageVersion to disable errors when Microsoft.NETCore.App's version is overridden externally
// This verification doesn't matter if we are skipping restore during tests.
if (DeploymentParameters.ApplicationType == ApplicationType.Standalone)
{
parameters += $" --runtime {GetRuntimeIdentifier()}";
}
parameters += $" {DeploymentParameters.AdditionalPublishParameters}";
var startInfo = new ProcessStartInfo
{
FileName = DotnetCommandName,
Arguments = parameters,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = DeploymentParameters.ApplicationPath,
};
AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.PublishEnvironmentVariables);
var hostProcess = new Process() { StartInfo = startInfo };
Logger.LogInformation($"Executing command {DotnetCommandName} {parameters}");
hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", Logger);
// A timeout is passed to Process.WaitForExit() for two reasons:
//
// 1. When process output is read asynchronously, WaitForExit() without a timeout blocks until child processes
// are killed, which can cause hangs due to MSBuild NodeReuse child processes started by dotnet.exe.
// With a timeout, WaitForExit() returns when the parent process is killed and ignores child processes.
// https://stackoverflow.com/a/37983587/102052
//
// 2. If "dotnet publish" does hang indefinitely for some reason, tests should fail fast with an error message.
const int timeoutMinutes = 5;
if (hostProcess.WaitForExit(milliseconds: timeoutMinutes * 60 * 1000))
{
if (hostProcess.ExitCode != 0)
{
var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}";
Logger.LogError(message);
throw new Exception(message);
}
}
else
{
var message = $"{DotnetCommandName} publish failed to exit after {timeoutMinutes} minutes";
Logger.LogError(message);
throw new Exception(message);
}
Logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}");
}
var publisher = DeploymentParameters.ApplicationPublisher ?? new ApplicationPublisher(DeploymentParameters.ApplicationPath);
_publishedApplication = publisher.Publish(DeploymentParameters, Logger).GetAwaiter().GetResult();
DeploymentParameters.PublishedApplicationRootPath = _publishedApplication.Path;
}
protected void CleanPublishedOutput()
@ -168,11 +97,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
else
{
RetryHelper.RetryOperation(
() => Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, true),
e => Logger.LogWarning($"Failed to delete directory : {e.Message}"),
retryCount: 3,
retryDelayMilliseconds: 100);
_publishedApplication.Dispose();
}
}
}
@ -219,26 +144,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
protected void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, IDictionary<string, string> environmentVariables)
{
var environment = startInfo.Environment;
SetEnvironmentVariable(environment, "ASPNETCORE_ENVIRONMENT", DeploymentParameters.EnvironmentName);
foreach (var environmentVariable in environmentVariables)
{
SetEnvironmentVariable(environment, environmentVariable.Key, environmentVariable.Value);
}
}
protected void SetEnvironmentVariable(IDictionary<string, string> environment, string name, string value)
{
if (value == null)
{
Logger.LogInformation("Removing environment variable {name}", name);
environment.Remove(name);
}
else
{
Logger.LogInformation("SET {name}={value}", name, value);
environment[name] = value;
}
ProcessHelpers.SetEnvironmentVariable(environment, "ASPNETCORE_ENVIRONMENT", DeploymentParameters.EnvironmentName, Logger);
ProcessHelpers.AddEnvironmentVariablesToProcess(startInfo, environmentVariables, Logger);
}
protected void InvokeUserApplicationCleanup()
@ -286,26 +193,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
public abstract void Dispose();
private string GetRuntimeIdentifier()
{
var architecture = DeploymentParameters.RuntimeArchitecture;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "win7-" + architecture;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux-" + architecture;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "osx-" + architecture;
}
else
{
throw new InvalidOperationException("Unrecognized operation system platform");
}
}
}
}

View File

@ -0,0 +1,36 @@
// 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.Collections.Generic;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
internal class ProcessHelpers
{
public static void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, IDictionary<string, string> environmentVariables, ILogger logger)
{
var environment = startInfo.Environment;
foreach (var environmentVariable in environmentVariables)
{
SetEnvironmentVariable(environment, environmentVariable.Key, environmentVariable.Value, logger);
}
}
public static void SetEnvironmentVariable(IDictionary<string, string> environment, string name, string value, ILogger logger)
{
if (value == null)
{
logger.LogInformation("Removing environment variable {name}", name);
environment.Remove(name);
}
else
{
logger.LogInformation("SET {name}={value}", name, value);
environment[name] = value;
}
}
}
}

View File

@ -0,0 +1,31 @@
// 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.IO;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
public class PublishedApplication: IDisposable
{
private readonly ILogger _logger;
public string Path { get; }
public PublishedApplication(string path, ILogger logger)
{
_logger = logger;
Path = path;
}
public void Dispose()
{
RetryHelper.RetryOperation(
() => Directory.Delete(Path, true),
e => _logger.LogWarning($"Failed to delete directory : {e.Message}"),
retryCount: 3,
retryDelayMilliseconds: 100);
}
}
}

View File

@ -252,7 +252,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
}
var archSkip = skip ?? SkipIfArchitectureNotSupportedOnCurrentSystem(arch);
if (server == ServerType.IISExpress)
if (server == ServerType.IISExpress || server == ServerType.IIS)
{
VaryByAncmVersion(variants, server, tfm, type, arch, archSkip);
}

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
// For debug and test explorer view
var description = $"Server: {Server}, TFM: {Tfm}, Type: {ApplicationType}, Arch: {Architecture}";
if (Server == ServerType.IISExpress)
if (Server == ServerType.IISExpress || Server == ServerType.IIS)
{
var version = AncmVersion == AncmVersion.AspNetCoreModule ? "V1" : "V2";
description += $", ANCM: {version}, Host: {HostingModel}";