forked from dotnet/runtime
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 dotnet#55417 Fix dotnet#44086 Fix dotnet#50397 Fix dotnet#42224 Fix dotnet#35990
- Loading branch information
Showing
5 changed files
with
249 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Diagnostics; | ||
using System.Runtime.InteropServices; | ||
|
||
namespace Microsoft.Extensions.Hosting.Internal | ||
{ | ||
public partial class ConsoleLifetime : IHostLifetime | ||
{ | ||
private PosixSignalRegistration _sigTermRegistration; | ||
|
||
private partial void RegisterShutdownHandlers() | ||
{ | ||
_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => OnSigTerm(context)); | ||
} | ||
|
||
private void OnSigTerm(PosixSignalContext context) | ||
{ | ||
Debug.Assert(context.Signal == PosixSignal.SIGTERM); | ||
|
||
context.Cancel = true; | ||
OnExitSignal(); | ||
} | ||
|
||
private partial void UnregisterShutdownHandlers() | ||
{ | ||
_sigTermRegistration?.Dispose(); | ||
} | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.notnetcoreapp.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// 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.Logging; | ||
|
||
namespace Microsoft.Extensions.Hosting.Internal | ||
{ | ||
public partial class ConsoleLifetime : IHostLifetime | ||
{ | ||
private partial void RegisterShutdownHandlers() | ||
{ | ||
AppDomain.CurrentDomain.ProcessExit += OnProcessExit; | ||
} | ||
|
||
private void OnProcessExit(object sender, EventArgs e) | ||
{ | ||
ApplicationLifetime.StopApplication(); | ||
|
||
if (!_shutdownBlock.WaitOne(HostOptions.ShutdownTimeout)) | ||
{ | ||
Logger.LogInformation("Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks."); | ||
} | ||
|
||
// wait one more time after the above error message, but only for ShutdownTimeout, so it doesn't hang forever | ||
_shutdownBlock.WaitOne(HostOptions.ShutdownTimeout); | ||
|
||
// 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/dotnet/aspnetcore/issues/6526 | ||
|
||
// This only applies to non-net6.0+, since in net6.0 we added a handler specific for SIGTERM, | ||
// so we don't need to reset the ExitCode in ProcessExit anymore. | ||
System.Environment.ExitCode = 0; | ||
} | ||
|
||
private partial void UnregisterShutdownHandlers() | ||
{ | ||
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeExitTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// 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 System.Runtime.InteropServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.DotNet.RemoteExecutor; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Xunit; | ||
|
||
namespace Microsoft.Extensions.Hosting.Tests | ||
{ | ||
public class ConsoleLifetimeExitTests | ||
{ | ||
/// <summary> | ||
/// Tests that a Hosted process that receives SIGTERM/SIGINT completes successfully | ||
/// and the rest of "main" gets executed. | ||
/// </summary> | ||
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] | ||
[PlatformSpecific(TestPlatforms.AnyUnix)] | ||
[InlineData(SIGTERM)] | ||
[InlineData(SIGINT)] | ||
public async Task EnsureSignalContinuesMainMethod(int signal) | ||
{ | ||
using var remoteHandle = RemoteExecutor.Invoke(async () => | ||
{ | ||
await Host.CreateDefaultBuilder() | ||
.ConfigureServices((hostContext, services) => | ||
{ | ||
services.AddHostedService<EnsureSignalContinuesMainMethodWorker>(); | ||
}) | ||
.RunConsoleAsync(); | ||
// adding this delay ensures the "main" method loses in a race with the normal process exit | ||
// and can cause the below message not to be written when the normal process exit isn't canceled by the | ||
// SIGTERM handler | ||
await Task.Delay(20); | ||
Console.WriteLine("Run has completed"); | ||
return 123; | ||
}, new RemoteInvokeOptions() { Start = false, ExpectedExitCode = 123 }); | ||
|
||
remoteHandle.Process.StartInfo.RedirectStandardOutput = true; | ||
remoteHandle.Process.Start(); | ||
|
||
// wait for the host process to start | ||
string line; | ||
while ((line = remoteHandle.Process.StandardOutput.ReadLine()).EndsWith("Started")) | ||
{ | ||
await Task.Delay(20); | ||
} | ||
|
||
// send SIGTERM/SIGINT to the process | ||
kill(remoteHandle.Process.Id, signal); | ||
|
||
remoteHandle.Process.WaitForExit(); | ||
|
||
string processOutput = remoteHandle.Process.StandardOutput.ReadToEnd(); | ||
Assert.Contains("Run has completed", processOutput); | ||
Assert.Equal(123, remoteHandle.Process.ExitCode); | ||
} | ||
|
||
private const int SIGINT = 2; | ||
private const int SIGTERM = 15; | ||
|
||
[DllImport("libc", SetLastError = true)] | ||
private static extern int kill(int pid, int sig); | ||
|
||
private class EnsureSignalContinuesMainMethodWorker : BackgroundService | ||
{ | ||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
while (!stoppingToken.IsCancellationRequested) | ||
{ | ||
try | ||
{ | ||
await Task.Delay(20, stoppingToken); | ||
Console.WriteLine("Started"); | ||
} | ||
catch (OperationCanceledException) | ||
{ | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Tests that calling Environment.Exit from a Hosted app sets the correct exit code. | ||
/// </summary> | ||
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] | ||
// SIGTERM is only handled on net6.0+, so the workaround to "clobber" the exit code is still in place on NetFramework | ||
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] | ||
public void EnsureEnvironmentExitCode() | ||
{ | ||
using var remoteHandle = RemoteExecutor.Invoke(async () => | ||
{ | ||
await Host.CreateDefaultBuilder() | ||
.ConfigureServices((hostContext, services) => | ||
{ | ||
services.AddHostedService<EnsureEnvironmentExitCodeWorker>(); | ||
}) | ||
.RunConsoleAsync(); | ||
}); | ||
|
||
remoteHandle.Process.WaitForExit(); | ||
|
||
Assert.Equal(124, remoteHandle.Process.ExitCode); | ||
} | ||
|
||
private class EnsureEnvironmentExitCodeWorker : BackgroundService | ||
{ | ||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
await Task.Run(() => | ||
{ | ||
Environment.Exit(124); | ||
}); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Tests that calling Environment.Exit from the "main" thread doesn't hang the process forever. | ||
/// </summary> | ||
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] | ||
public void EnsureEnvironmentExitDoesntHang() | ||
{ | ||
using var remoteHandle = RemoteExecutor.Invoke(async () => | ||
{ | ||
await Host.CreateDefaultBuilder() | ||
.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromMilliseconds(100)) | ||
.ConfigureServices((hostContext, services) => | ||
{ | ||
services.AddHostedService<EnsureEnvironmentExitDoesntHangWorker>(); | ||
}) | ||
.RunConsoleAsync(); | ||
}, new RemoteInvokeOptions() { TimeOut = 10_000 }); // give a 10 second time out, so if this does hang, it doesn't hang for the full timeout | ||
|
||
Assert.True(remoteHandle.Process.WaitForExit(10_000), "The hosted process should have exited within 10 seconds"); | ||
|
||
// SIGTERM is only handled on net6.0+, so the workaround to "clobber" the exit code is still in place on NetFramework | ||
int expectedExitCode = PlatformDetection.IsNetFramework ? 0 : 125; | ||
Assert.Equal(expectedExitCode, remoteHandle.Process.ExitCode); | ||
} | ||
|
||
private class EnsureEnvironmentExitDoesntHangWorker : BackgroundService | ||
{ | ||
protected override Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
Environment.Exit(125); | ||
return Task.CompletedTask; | ||
} | ||
} | ||
} | ||
} |