Skip to content

Commit

Permalink
React to SIGTERM exit code change (#1289)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tratcher committed Mar 28, 2019
1 parent 0a27fd3 commit 0bc7c9f
Show file tree
Hide file tree
Showing 30 changed files with 1,784 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/Hosting/Hosting.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hostin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hosting.WindowsServices.Tests", "WindowsServices\test\Microsoft.Extensions.Hosting.WindowsServices.Tests.csproj", "{1A3BC0DA-CB53-4C21-BDD0-2B2CF83655FA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hosting.IntegrationTesting", "IntegrationTesting\src\Microsoft.Extensions.Hosting.IntegrationTesting.csproj", "{A607D5A9-15FB-43D6-9DA3-816CB416C6BF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hosting.FunctionalTests", "test\FunctionalTests\Microsoft.Extensions.Hosting.FunctionalTests.csproj", "{C700A29C-3989-4077-B6FF-A0F08468B0A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Hosting.TestApp", "test\testassets\Microsoft.Extensions.Hosting.TestApp\Microsoft.Extensions.Hosting.TestApp.csproj", "{A5C1CB77-4286-40BD-A87A-8EB13548F755}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -53,6 +59,18 @@ Global
{1A3BC0DA-CB53-4C21-BDD0-2B2CF83655FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A3BC0DA-CB53-4C21-BDD0-2B2CF83655FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A3BC0DA-CB53-4C21-BDD0-2B2CF83655FA}.Release|Any CPU.Build.0 = Release|Any CPU
{A607D5A9-15FB-43D6-9DA3-816CB416C6BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A607D5A9-15FB-43D6-9DA3-816CB416C6BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A607D5A9-15FB-43D6-9DA3-816CB416C6BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A607D5A9-15FB-43D6-9DA3-816CB416C6BF}.Release|Any CPU.Build.0 = Release|Any CPU
{C700A29C-3989-4077-B6FF-A0F08468B0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C700A29C-3989-4077-B6FF-A0F08468B0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C700A29C-3989-4077-B6FF-A0F08468B0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C700A29C-3989-4077-B6FF-A0F08468B0A0}.Release|Any CPU.Build.0 = Release|Any CPU
{A5C1CB77-4286-40BD-A87A-8EB13548F755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5C1CB77-4286-40BD-A87A-8EB13548F755}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5C1CB77-4286-40BD-A87A-8EB13548F755}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5C1CB77-4286-40BD-A87A-8EB13548F755}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
3 changes: 3 additions & 0 deletions src/Hosting/Hosting/src/Internal/ConsoleLifetime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ private void OnProcessExit(object sender, EventArgs e)
{
ApplicationLifetime.StopApplication();
_shutdownBlock.WaitOne();
// On Linux if the shutdown is triggered by SIGTERM then that's signaled with the 143 exit code.
// Suppress that since we shut down gracefully. https://github.com/aspnet/AspNetCore/issues/6526
System.Environment.ExitCode = 0;
}

private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs e)
Expand Down
129 changes: 129 additions & 0 deletions src/Hosting/IntegrationTesting/src/ApplicationPublisher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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.Extensions.Hosting.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}"
+ $" /p:TargetArchitecture={deploymentParameters.RuntimeArchitecture}"
+ " --no-restore";

if (deploymentParameters.ApplicationType == ApplicationType.Standalone)
{
parameters += $" --runtime {GetRuntimeIdentifier(deploymentParameters)}";
}
else
{
// Workaround for https://github.com/aspnet/websdk/issues/422
parameters += " -p:UseAppHost=false";
}

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 "win-" + 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;
}
}
}
18 changes: 18 additions & 0 deletions src/Hosting/IntegrationTesting/src/Common/ApplicationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.

namespace Microsoft.Extensions.Hosting.IntegrationTesting
{
public enum ApplicationType
{
/// <summary>
/// Does not target a specific platform. Requires the matching runtime to be installed.
/// </summary>
Portable,

/// <summary>
/// All dlls are published with the app for x-copy deploy. Net461 requires this because ASP.NET Core is not in the GAC.
/// </summary>
Standalone
}
}
157 changes: 157 additions & 0 deletions src/Hosting/IntegrationTesting/src/Common/DeploymentParameters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// 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.Reflection;

namespace Microsoft.Extensions.Hosting.IntegrationTesting
{
/// <summary>
/// Parameters to control application deployment.
/// </summary>
public class DeploymentParameters
{
public DeploymentParameters()
{
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
{
Configuration = configAttribute.Configuration;
}
}

public DeploymentParameters(TestVariant variant)
{
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
{
Configuration = configAttribute.Configuration;
}

TargetFramework = variant.Tfm;
ApplicationType = variant.ApplicationType;
RuntimeArchitecture = variant.Architecture;
}

/// <summary>
/// Creates an instance of <see cref="DeploymentParameters"/>.
/// </summary>
/// <param name="applicationPath">Source code location of the target location to be deployed.</param>
/// <param name="runtimeFlavor">Flavor of the clr to run against.</param>
/// <param name="runtimeArchitecture">Architecture of the runtime to be used.</param>
public DeploymentParameters(
string applicationPath,
RuntimeFlavor runtimeFlavor,
RuntimeArchitecture runtimeArchitecture)
{
if (string.IsNullOrEmpty(applicationPath))
{
throw new ArgumentException("Value cannot be null.", nameof(applicationPath));
}

if (!Directory.Exists(applicationPath))
{
throw new DirectoryNotFoundException(string.Format("Application path {0} does not exist.", applicationPath));
}

ApplicationPath = applicationPath;
ApplicationName = new DirectoryInfo(ApplicationPath).Name;
RuntimeFlavor = runtimeFlavor;

var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
{
Configuration = configAttribute.Configuration;
}
}

public DeploymentParameters(DeploymentParameters parameters)
{
foreach (var propertyInfo in typeof(DeploymentParameters).GetProperties())
{
if (propertyInfo.CanWrite)
{
propertyInfo.SetValue(this, propertyInfo.GetValue(parameters));
}
}

foreach (var kvp in parameters.EnvironmentVariables)
{
EnvironmentVariables.Add(kvp);
}

foreach (var kvp in parameters.PublishEnvironmentVariables)
{
PublishEnvironmentVariables.Add(kvp);
}
}

public ApplicationPublisher ApplicationPublisher { get; set; }

public RuntimeFlavor RuntimeFlavor { get; set; }

public RuntimeArchitecture RuntimeArchitecture { get; set; } = RuntimeArchitecture.x64;

public string EnvironmentName { get; set; }

public string ApplicationPath { get; set; }

/// <summary>
/// Gets or sets the name of the application. This is used to execute the application when deployed.
/// Defaults to the file name of <see cref="ApplicationPath"/>.
/// </summary>
public string ApplicationName { get; set; }

public string TargetFramework { get; set; }

/// <summary>
/// Configuration under which to build (ex: Release or Debug)
/// </summary>
public string Configuration { get; set; } = "Debug";

/// <summary>
/// Space separated command line arguments to be passed to dotnet-publish
/// </summary>
public string AdditionalPublishParameters { get; set; }

/// <summary>
/// To publish the application before deployment.
/// </summary>
public bool PublishApplicationBeforeDeployment { get; set; }

public bool PreservePublishedApplicationForDebugging { get; set; } = false;

public bool StatusMessagesEnabled { get; set; } = true;

public ApplicationType ApplicationType { get; set; }

public string PublishedApplicationRootPath { get; set; }

/// <summary>
/// Environment variables to be set before starting the host.
/// Not applicable for IIS Scenarios.
/// </summary>
public IDictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>();

/// <summary>
/// Environment variables used when invoking dotnet publish.
/// </summary>
public IDictionary<string, string> PublishEnvironmentVariables { get; } = new Dictionary<string, string>();

/// <summary>
/// For any application level cleanup to be invoked after performing host cleanup.
/// </summary>
public Action<DeploymentParameters> UserAdditionalCleanup { get; set; }

public override string ToString()
{
return string.Format(
"[Variation] :: Runtime={0}, Arch={1}, Publish={2}",
RuntimeFlavor,
RuntimeArchitecture,
PublishApplicationBeforeDeployment);
}
}
}
47 changes: 47 additions & 0 deletions src/Hosting/IntegrationTesting/src/Common/DeploymentResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.Net.Http;
using System.Threading;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.Hosting.IntegrationTesting
{
/// <summary>
/// Result of a deployment.
/// </summary>
public class DeploymentResult
{
private readonly ILoggerFactory _loggerFactory;

/// <summary>
/// The folder where the application is hosted. This path can be different from the
/// original application source location if published before deployment.
/// </summary>
public string ContentRoot { get; }

/// <summary>
/// Original deployment parameters used for this deployment.
/// </summary>
public DeploymentParameters DeploymentParameters { get; }

/// <summary>
/// Triggered when the host process dies or pulled down.
/// </summary>
public CancellationToken HostShutdownToken { get; }

public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters)
: this(loggerFactory, deploymentParameters: deploymentParameters, contentRoot: string.Empty, hostShutdownToken: CancellationToken.None)
{ }

public DeploymentResult(ILoggerFactory loggerFactory, DeploymentParameters deploymentParameters, string contentRoot, CancellationToken hostShutdownToken)
{
_loggerFactory = loggerFactory;

ContentRoot = contentRoot;
DeploymentParameters = deploymentParameters;
HostShutdownToken = hostShutdownToken;
}
}
}
Loading

0 comments on commit 0bc7c9f

Please sign in to comment.