248 lines
9.3 KiB
C#
248 lines
9.3 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.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Internal;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Microsoft.AspNetCore.Server.IntegrationTesting
|
|
{
|
|
/// <summary>
|
|
/// Abstract base class of all deployers with implementation of some of the common helpers.
|
|
/// </summary>
|
|
public abstract class ApplicationDeployer : IApplicationDeployer
|
|
{
|
|
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();
|
|
|
|
public ApplicationDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
|
|
{
|
|
DeploymentParameters = deploymentParameters;
|
|
LoggerFactory = loggerFactory;
|
|
Logger = LoggerFactory.CreateLogger(GetType().FullName);
|
|
}
|
|
|
|
protected DeploymentParameters DeploymentParameters { get; }
|
|
|
|
protected ILoggerFactory LoggerFactory { get; }
|
|
protected ILogger Logger { get; }
|
|
|
|
public abstract Task<DeploymentResult> DeployAsync();
|
|
|
|
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}";
|
|
|
|
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);
|
|
|
|
hostProcess.WaitForExit();
|
|
|
|
if (hostProcess.ExitCode != 0)
|
|
{
|
|
var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}";
|
|
Logger.LogError(message);
|
|
throw new Exception(message);
|
|
}
|
|
|
|
Logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}");
|
|
}
|
|
}
|
|
|
|
protected void CleanPublishedOutput()
|
|
{
|
|
using (Logger.BeginScope("CleanPublishedOutput"))
|
|
{
|
|
if (DeploymentParameters.PreservePublishedApplicationForDebugging)
|
|
{
|
|
Logger.LogWarning(
|
|
"Skipping deleting the locally published folder as property " +
|
|
$"'{nameof(DeploymentParameters.PreservePublishedApplicationForDebugging)}' is set to 'true'.");
|
|
}
|
|
else
|
|
{
|
|
RetryHelper.RetryOperation(
|
|
() => Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, true),
|
|
e => Logger.LogWarning($"Failed to delete directory : {e.Message}"),
|
|
retryCount: 3,
|
|
retryDelayMilliseconds: 100);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void ShutDownIfAnyHostProcess(Process hostProcess)
|
|
{
|
|
if (hostProcess != null && !hostProcess.HasExited)
|
|
{
|
|
Logger.LogInformation("Attempting to cancel process {0}", hostProcess.Id);
|
|
|
|
// Shutdown the host process.
|
|
hostProcess.KillTree();
|
|
if (!hostProcess.HasExited)
|
|
{
|
|
Logger.LogWarning("Unable to terminate the host process with process Id '{processId}", hostProcess.Id);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Successfully terminated host process with process Id '{processId}'", hostProcess.Id);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Host process already exited or never started successfully.");
|
|
}
|
|
}
|
|
|
|
protected void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, List<KeyValuePair<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;
|
|
}
|
|
}
|
|
|
|
protected void InvokeUserApplicationCleanup()
|
|
{
|
|
using (Logger.BeginScope("UserAdditionalCleanup"))
|
|
{
|
|
if (DeploymentParameters.UserAdditionalCleanup != null)
|
|
{
|
|
// User cleanup.
|
|
try
|
|
{
|
|
DeploymentParameters.UserAdditionalCleanup(DeploymentParameters);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Logger.LogWarning("User cleanup code failed with exception : {exception}", exception.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void TriggerHostShutdown(CancellationTokenSource hostShutdownSource)
|
|
{
|
|
Logger.LogInformation("Host process shutting down.");
|
|
try
|
|
{
|
|
hostShutdownSource.Cancel();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Suppress errors.
|
|
}
|
|
}
|
|
|
|
protected void StartTimer()
|
|
{
|
|
Logger.LogInformation($"Deploying {DeploymentParameters.ToString()}");
|
|
_stopwatch.Start();
|
|
}
|
|
|
|
protected void StopTimer()
|
|
{
|
|
_stopwatch.Stop();
|
|
Logger.LogInformation("[Time]: Total time taken for this test variation '{t}' seconds", _stopwatch.Elapsed.TotalSeconds);
|
|
}
|
|
|
|
public abstract void Dispose();
|
|
|
|
private string GetRuntimeIdentifier()
|
|
{
|
|
var architecture = GetArchitecture();
|
|
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.10.12-" + architecture;
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("Unrecognized operation system platform");
|
|
}
|
|
}
|
|
|
|
private string GetArchitecture()
|
|
{
|
|
switch (RuntimeInformation.OSArchitecture)
|
|
{
|
|
case Architecture.X86:
|
|
return "x86";
|
|
case Architecture.X64:
|
|
return "x64";
|
|
default:
|
|
throw new NotSupportedException($"Unsupported architecture: {RuntimeInformation.OSArchitecture}");
|
|
}
|
|
}
|
|
}
|
|
}
|