Skip to content

Commit 35584ab

Browse files
authored
Do not return from CTRL_SHUTDOWN_EVENT (#116652)
* Do not return from CTRL_SHUTDOWN_EVENT Windows will kill the process after it completes handling CTRL_SHUTDOWN_EVENT To avoid this, we add a delay to our handler. This allows the process to exit before the handler returns. We use HostOptions.ShutdownTimeout for the delay. This does mean that other SIGTERM handlers will not run, but that's a tradeoff we have to make to ensure the process isn't killed. * Update ConsoleLifetime.netcoreapp.cs
1 parent 4aec7a5 commit 35584ab

File tree

3 files changed

+108
-5
lines changed

3 files changed

+108
-5
lines changed

src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Diagnostics;
66
using System.Runtime.InteropServices;
7+
using System.Threading;
78

89
namespace Microsoft.Extensions.Hosting.Internal
910
{
@@ -20,7 +21,7 @@ private partial void RegisterShutdownHandlers()
2021
Action<PosixSignalContext> handler = HandlePosixSignal;
2122
_sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, handler);
2223
_sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handler);
23-
_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handler);
24+
_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OperatingSystem.IsWindows() ? HandleWindowsShutdown : handler);
2425
}
2526
}
2627

@@ -32,6 +33,24 @@ private void HandlePosixSignal(PosixSignalContext context)
3233
ApplicationLifetime.StopApplication();
3334
}
3435

36+
private void HandleWindowsShutdown(PosixSignalContext context)
37+
{
38+
// for SIGTERM on Windows we must block this thread until the application is finished
39+
// otherwise the process will be killed immediately on return from this handler
40+
41+
// don't allow Dispose to unregister handlers, since Windows has a lock that prevents the unregistration while this handler is running
42+
// just leak these, since the process is exiting
43+
_sigIntRegistration = null;
44+
_sigQuitRegistration = null;
45+
_sigTermRegistration = null;
46+
47+
ApplicationLifetime.StopApplication();
48+
49+
// We could wait for a signal here, like Dispose as is done in non-netcoreapp case, but those inevitably could have user
50+
// code that runs after them in the user's Main. Instead we just block this thread completely and let the main routine exit.
51+
Thread.Sleep(HostOptions.ShutdownTimeout);
52+
}
53+
3554
private partial void UnregisterShutdownHandlers()
3655
{
3756
_sigIntRegistration?.Dispose();

src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeExitTests.cs

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.IO;
6+
using System.IO.Pipes;
7+
using System.Reflection;
58
using System.Runtime.InteropServices;
69
using System.Threading;
710
using System.Threading.Tasks;
@@ -18,14 +21,22 @@ public class ConsoleLifetimeExitTests
1821
/// and the rest of "main" gets executed.
1922
/// </summary>
2023
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
21-
[PlatformSpecific(TestPlatforms.AnyUnix)]
2224
[InlineData(SIGTERM)]
2325
[InlineData(SIGINT)]
2426
[InlineData(SIGQUIT)]
2527
public async Task EnsureSignalContinuesMainMethod(int signal)
2628
{
27-
using var remoteHandle = RemoteExecutor.Invoke(async () =>
29+
// simulate signals on Windows by using a pipe to communicate with the remote process
30+
using var messagePipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable);
31+
32+
using var remoteHandle = RemoteExecutor.Invoke(async (pipeHandleAsString) =>
2833
{
34+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
35+
{
36+
// kick off a thread to simulate the signal on Windows
37+
_ = Task.Run(() => SimulatePosixSignalWindows(pipeHandleAsString));
38+
}
39+
2940
await Host.CreateDefaultBuilder()
3041
.ConfigureServices((hostContext, services) =>
3142
{
@@ -40,7 +51,7 @@ await Host.CreateDefaultBuilder()
4051

4152
Console.WriteLine("Run has completed");
4253
return 123;
43-
}, new RemoteInvokeOptions() { Start = false, ExpectedExitCode = 123 });
54+
}, messagePipe.GetClientHandleAsString(), new RemoteInvokeOptions() { Start = false, ExpectedExitCode = 123 });
4455

4556
remoteHandle.Process.StartInfo.RedirectStandardOutput = true;
4657
remoteHandle.Process.Start();
@@ -53,7 +64,16 @@ await Host.CreateDefaultBuilder()
5364
}
5465

5566
// send the signal to the process
56-
kill(remoteHandle.Process.Id, signal);
67+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
68+
{
69+
// on Windows, we use the pipe to signal the process
70+
messagePipe.WriteByte((byte)signal);
71+
}
72+
else
73+
{
74+
// on Unix, we send the signal directly
75+
kill(remoteHandle.Process.Id, signal);
76+
}
5777

5878
remoteHandle.Process.WaitForExit();
5979

@@ -69,6 +89,69 @@ await Host.CreateDefaultBuilder()
6989
[DllImport("libc", SetLastError = true)]
7090
private static extern int kill(int pid, int sig);
7191

92+
93+
private const int CTRL_C_EVENT = 0;
94+
private const int CTRL_BREAK_EVENT = 1;
95+
private const int CTRL_CLOSE_EVENT = 2;
96+
private const int CTRL_LOGOFF_EVENT = 5;
97+
private const int CTRL_SHUTDOWN_EVENT = 6;
98+
99+
private static unsafe void SimulatePosixSignalWindows(string pipeHandleAsString)
100+
{
101+
try
102+
{
103+
using var readPipe = new AnonymousPipeClientStream(PipeDirection.In, pipeHandleAsString);
104+
105+
int signal = (int)readPipe.ReadByte();
106+
107+
int ctrlType = (int)signal switch
108+
{
109+
SIGINT => CTRL_C_EVENT,
110+
SIGQUIT => CTRL_BREAK_EVENT,
111+
SIGTERM => CTRL_SHUTDOWN_EVENT,
112+
_ => throw new ArgumentOutOfRangeException(nameof(signal), "Unsupported signal")
113+
};
114+
115+
#if NETFRAMEWORK
116+
if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT)
117+
{
118+
var handlerMethod = Type.GetType("System.Console, mscorlib")?.GetMethod("BreakEvent", BindingFlags.NonPublic | BindingFlags.Static);
119+
Assert.NotNull(handlerMethod);
120+
handlerMethod.Invoke(null, [ctrlType]);
121+
}
122+
else // CTRL_SHUTDOWN_EVENT
123+
{
124+
var handlerField = typeof(AppDomain).GetField("_processExit", BindingFlags.NonPublic | BindingFlags.Instance);
125+
Assert.NotNull(handlerField);
126+
EventHandler handler = (EventHandler)handlerField.GetValue(AppDomain.CurrentDomain);
127+
Assert.NotNull(handler);
128+
handler.Invoke(AppDomain.CurrentDomain, null);
129+
}
130+
#else
131+
// get the System.Runtime.InteropServices.PosixSignalRegistration.HandlerRoutine private method
132+
var handlerMethod = typeof(PosixSignalRegistration).GetMethod("HandlerRoutine", BindingFlags.NonPublic | BindingFlags.Static);
133+
Assert.NotNull(handlerMethod);
134+
135+
var handlerPtr = handlerMethod.MethodHandle.GetFunctionPointer();
136+
delegate* unmanaged<int, int> handler = (delegate* unmanaged<int, int>)handlerPtr;
137+
138+
handler(ctrlType);
139+
140+
if (signal == SIGTERM)
141+
{
142+
// on Windows the OS will kill the process immediately after this
143+
Environment.FailFast("Simulating shutdown");
144+
}
145+
#endif
146+
}
147+
catch (Exception ex)
148+
{
149+
// Exceptions on this thread will not be observed, nor will they cause the process to exit.
150+
// Use failfast to ensure the process will exit without running any handlers.
151+
Environment.FailFast(ex.ToString());
152+
}
153+
}
154+
72155
private class EnsureSignalContinuesMainMethodWorker : BackgroundService
73156
{
74157
protected override async Task ExecuteAsync(CancellationToken stoppingToken)

src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
55
<EnableDefaultItems>true</EnableDefaultItems>
66
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
78
<AutoGenerateBindingRedirects Condition="$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) == '.NETFramework'">true</AutoGenerateBindingRedirects>
89
<EventSourceSupport Condition="'$(TestNativeAot)' == 'true'">true</EventSourceSupport>
910
</PropertyGroup>

0 commit comments

Comments
 (0)