Skip to content

Commit

Permalink
Consume PosixSignal in Hosting's ConsoleLifetime (#56057)
Browse files Browse the repository at this point in the history
* Add NetCoreAppCurrent target to Microsoft.Extensions.Hosting

* Handle SIGTERM in Hosting and handle just like SIGINT (CTRL+C)

Don't listen to ProcessExit on net6.0+ in Hosting anymore. This allows for Environment.Exit to not hang the app.
Don't clobber ExitCode during ProcessExit now that SIGTERM is handled separately.

For non-net6.0 targets, only wait for the shutdown timeout, so the process doesn't hang forever.

Fix #55417
Fix #44086
Fix #50397
Fix #42224
Fix #35990

* Remove _shutdownBlock on netcoreappcurrent, as this is no longer waited on
* Change Console.CancelKeyPress to use PosixSignalRegistration SIGINT and SIGQUIT
* Use a no-op lifetime on mobile platforms

* Add docs for shutdown
  • Loading branch information
eerhardt committed Jul 22, 2021
1 parent e60882e commit 36f3f97
Show file tree
Hide file tree
Showing 16 changed files with 481 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Hosting.Systemd;
using Xunit;

namespace Microsoft.Extensions.Hosting
Expand All @@ -19,7 +19,8 @@ public void DefaultsToOffOutsideOfService()
using (host)
{
var lifetime = host.Services.GetRequiredService<IHostLifetime>();
Assert.IsType<ConsoleLifetime>(lifetime);
Assert.NotNull(lifetime);
Assert.IsNotType<SystemdLifetime>(lifetime);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
<Import Project="..\Directory.Build.props" />
<PropertyGroup>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<IncludePlatformAttributes>true</IncludePlatformAttributes>
</PropertyGroup>
</Project>
</Project>
68 changes: 68 additions & 0 deletions src/libraries/Microsoft.Extensions.Hosting/docs/HostShutdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Host Shutdown

A Hosted Service process can be stopped in the following ways:

1. If someone doesn't call `Run` or `WaitForShutdown` and the app exits normally with "Main" completing
2. A crash occurs
3. The app is forcefully shut down, e.g. SIGKILL (i.e. CTRL+Z)
4. When ConsoleLifetime is used it listens for the following signals and attempts to stop the host gracefully
1. SIGINT (i.e. CTRL+C)
2. SIGQUIT (i.e. CTRL+BREAK on Windows, CTRL+`\` on Unix)
3. SIGTERM (sent by other apps, e.g. `docker stop`)
5. The app calls `Environment.Exit(code)`

Scenarios (1), (2), and (3) aren't handled directly by the Hosting code. The owner of the process needs to deal with
them the same as any application.

Scenarios (4) and (5) are handled specially by the built-in Hosting logic. Specifically by the ConsoleLifetime
class. ConsoleLifetime tries to handle the "shutdown" signals listed in (4) and allow for a graceful exit to the
application.

Before .NET 6, there wasn't a way for .NET code to gracefully handle SIGTERM. To work around this limitation,
ConsoleLifetime would subscribe to `AppDomain.CurrentDomain.ProcessExit`. When `ProcessExit` was raised,
ConsoleLifetime would signal the Host to stop, and block the `ProcessExit` thread, waiting for the Host to stop.
This would allow for the clean up code in the application to run - for example, `IHostedService.StopAsync` and code after
`Host.Run` in the Main method.

This caused other issues because SIGTERM wasn't the only way `ProcessExit` was raised. It is also raised by code
in the application calling `Environment.Exit`. `Environment.Exit` isn't a graceful way of shutting down a process.
It raises the `ProcessExit` event and then exits the process. The end of the Main method doesn't get executed. However,
since ConsoleLifetime blocked `ProcessExit` waiting for the Host to shutdown, this behavior also lead to [deadlocks][deadlocks]
due to `Environment.Exit` also blocking waiting for `ProcessExit` to return. Additionally, since SIGTERM handling was attempting
to gracefully shut down the process, ConsoleLifetime would set the ExitCode to `0`, which [clobbered][clobbered] the user's
exit code passed to `Environment.Exit`.

[deadlocks]: https://github.com/dotnet/runtime/issues/50397
[clobbered]: https://github.com/dotnet/runtime/issues/42224

In .NET 6, we added new support to handle [POSIX signals][POSIX signals]. This allows for ConsoleLifetime to specifically
handle SIGTERM gracefully, and no longer get involved when `Environment.Exit` is invoked. For .NET 6+, ConsoleLifetime no longer
has logic to handle scenario (5) above. Apps that call `Environment.Exit`, and need to do clean up logic, can subscribe to
`ProcessExit` themselves. Hosting will no longer attempt to gracefully stop the Host in this scenario.

[POSIX signals]: https://github.com/dotnet/runtime/issues/50527

### Hosting Shutdown Process

The following sequence model shows how the signals in (4) above are handled internally in the Hosting code. It isn't necessary
for the majority of users to understand this process. But for developers that need a deep understanding, this may help them
get started.

After the Host has been started, when a user calls `Run` or `WaitForShutdown`, a handler gets registered for
`ApplicationLifetime.ApplicationStopping`. Execution is paused in `WaitForShutdown`, waiting for the `ApplicationStopping`
event to be raised. This is how the "Main" method doesn't return right away, and the app stays running until
`Run`/`WaitForShutdown` returns.

When a signal is sent to the process, it kicks off the following sequence.

![image](images/HostShutdownSequence.png)

The control flows from `ConsoleLifetime` to `ApplicationLifetime` to raise the `ApplicationStopping` event. This signals
`WaitForShutdownAsync` to unblock the "Main" execution code. In the meantime, the POSIX signal handler returns with
`Cancel = true` since this POSIX signal has been handled.

The "Main" execution code starts executing again and tells the Host to `StopAsync()`, which in turn stops all the Hosted
Services and raises any other stopped events.

Finally, `WaitForShutdown` exits, allowing for any application clean up code to execute, and for the "Main" method
to exit gracefully.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,29 @@ public static partial class HostingHostBuilderExtensions
public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureLogging(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Hosting.HostBuilderContext, Microsoft.Extensions.Logging.ILoggingBuilder> configureLogging) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureLogging(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Logging.ILoggingBuilder> configureLogging) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureServices(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.DependencyInjection.IServiceCollection> configureDelegate) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static System.Threading.Tasks.Task RunConsoleAsync(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Hosting.ConsoleLifetimeOptions> configureOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static System.Threading.Tasks.Task RunConsoleAsync(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static Microsoft.Extensions.Hosting.IHostBuilder UseConsoleLifetime(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static Microsoft.Extensions.Hosting.IHostBuilder UseConsoleLifetime(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Hosting.ConsoleLifetimeOptions> configureOptions) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder UseContentRoot(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, string contentRoot) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder UseDefaultServiceProvider(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.DependencyInjection.ServiceProviderOptions> configure) { throw null; }
Expand All @@ -78,6 +98,11 @@ public partial class ApplicationLifetime : Microsoft.Extensions.Hosting.IApplica
public void NotifyStopped() { }
public void StopApplication() { }
}
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public partial class ConsoleLifetime : Microsoft.Extensions.Hosting.IHostLifetime, System.IDisposable
{
public ConsoleLifetime(Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Hosting.ConsoleLifetimeOptions> options, Microsoft.Extensions.Hosting.IHostEnvironment environment, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Hosting.HostOptions> hostOptions) { }
Expand Down
6 changes: 4 additions & 2 deletions src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.Hosting
/// <summary>
/// A program initialization utility.
/// </summary>
public class HostBuilder : IHostBuilder
public partial class HostBuilder : IHostBuilder
{
private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>();
Expand Down Expand Up @@ -245,7 +245,9 @@ private void CreateServiceProvider()
services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
services.AddSingleton<IHostLifetime, ConsoleLifetime>();

AddLifetime(services);

services.AddSingleton<IHost>(_ =>
{
return new Internal.Host(_appServices,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Internal;

namespace Microsoft.Extensions.Hosting
{
public partial class HostBuilder
{
private static void AddLifetime(ServiceCollection services)
{
if (!OperatingSystem.IsAndroid() && !OperatingSystem.IsBrowser() && !OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst() && !OperatingSystem.IsTvOS())
{
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
}
else
{
services.AddSingleton<IHostLifetime, NullLifetime>();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Internal;

namespace Microsoft.Extensions.Hosting
{
public partial class HostBuilder
{
private static void AddLifetime(ServiceCollection services)
{
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -225,7 +226,12 @@ public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[]
})
.ConfigureLogging((hostingContext, logging) =>
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
bool isWindows =
#if NET6_0_OR_GREATER
OperatingSystem.IsWindows();
#else
RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif
// IMPORTANT: This needs to be added *before* configuration is loaded, this lets
// the defaults be overridden by the configuration.
Expand All @@ -236,7 +242,12 @@ public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[]
}
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
#if NET6_0_OR_GREATER
if (!OperatingSystem.IsBrowser())
#endif
{
logging.AddConsole();
}
logging.AddDebug();
logging.AddEventSourceLogger();
Expand Down Expand Up @@ -274,6 +285,11 @@ public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[]
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(collection => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());
Expand All @@ -286,6 +302,11 @@ public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <param name="configureOptions">The delegate for configuring the <see cref="ConsoleLifetime"/>.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Action<ConsoleLifetimeOptions> configureOptions)
{
return hostBuilder.ConfigureServices(collection =>
Expand All @@ -301,6 +322,11 @@ public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Act
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the console.</param>
/// <returns>A <see cref="Task"/> that only completes when the token is triggered or shutdown is triggered.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken);
Expand All @@ -313,6 +339,11 @@ public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationTo
/// <param name="configureOptions">The delegate for configuring the <see cref="ConsoleLifetime"/>.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the console.</param>
/// <returns>A <see cref="Task"/> that only completes when the token is triggered or shutdown is triggered.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static Task RunConsoleAsync(this IHostBuilder hostBuilder, Action<ConsoleLifetimeOptions> configureOptions, CancellationToken cancellationToken = default)
{
return hostBuilder.UseConsoleLifetime(configureOptions).Build().RunAsync(cancellationToken);
Expand Down
Loading

0 comments on commit 36f3f97

Please sign in to comment.