diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs index 7fcccbeb8d411f..c5be9291ffd44c 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading; namespace Microsoft.Extensions.Hosting.Internal { @@ -20,7 +21,7 @@ private partial void RegisterShutdownHandlers() Action handler = HandlePosixSignal; _sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, handler); _sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handler); - _sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handler); + _sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OperatingSystem.IsWindows() ? HandleWindowsShutdown : handler); } } @@ -32,6 +33,24 @@ private void HandlePosixSignal(PosixSignalContext context) ApplicationLifetime.StopApplication(); } + private void HandleWindowsShutdown(PosixSignalContext context) + { + // for SIGTERM on Windows we must block this thread until the application is finished + // otherwise the process will be killed immediately on return from this handler + + // don't allow Dispose to unregister handlers, since Windows has a lock that prevents the unregistration while this handler is running + // just leak these, since the process is exiting + _sigIntRegistration = null; + _sigQuitRegistration = null; + _sigTermRegistration = null; + + ApplicationLifetime.StopApplication(); + + // We could wait for a signal here, like Dispose as is done in non-netcoreapp case, but those inevitably could have user + // code that runs after them in the user's Main. Instead we just block this thread completely and let the main routine exit. + Thread.Sleep(HostOptions.ShutdownTimeout); + } + private partial void UnregisterShutdownHandlers() { _sigIntRegistration?.Dispose(); diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeExitTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeExitTests.cs index 0e54db40045e96..3298400036126a 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeExitTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeExitTests.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.IO; +using System.IO.Pipes; +using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -18,14 +21,22 @@ public class ConsoleLifetimeExitTests /// and the rest of "main" gets executed. /// [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [PlatformSpecific(TestPlatforms.AnyUnix)] [InlineData(SIGTERM)] [InlineData(SIGINT)] [InlineData(SIGQUIT)] public async Task EnsureSignalContinuesMainMethod(int signal) { - using var remoteHandle = RemoteExecutor.Invoke(async () => + // simulate signals on Windows by using a pipe to communicate with the remote process + using var messagePipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable); + + using var remoteHandle = RemoteExecutor.Invoke(async (pipeHandleAsString) => { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // kick off a thread to simulate the signal on Windows + _ = Task.Run(() => SimulatePosixSignalWindows(pipeHandleAsString)); + } + await Host.CreateDefaultBuilder() .ConfigureServices((hostContext, services) => { @@ -40,7 +51,7 @@ await Host.CreateDefaultBuilder() Console.WriteLine("Run has completed"); return 123; - }, new RemoteInvokeOptions() { Start = false, ExpectedExitCode = 123 }); + }, messagePipe.GetClientHandleAsString(), new RemoteInvokeOptions() { Start = false, ExpectedExitCode = 123 }); remoteHandle.Process.StartInfo.RedirectStandardOutput = true; remoteHandle.Process.Start(); @@ -53,7 +64,16 @@ await Host.CreateDefaultBuilder() } // send the signal to the process - kill(remoteHandle.Process.Id, signal); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // on Windows, we use the pipe to signal the process + messagePipe.WriteByte((byte)signal); + } + else + { + // on Unix, we send the signal directly + kill(remoteHandle.Process.Id, signal); + } remoteHandle.Process.WaitForExit(); @@ -69,6 +89,69 @@ await Host.CreateDefaultBuilder() [DllImport("libc", SetLastError = true)] private static extern int kill(int pid, int sig); + + private const int CTRL_C_EVENT = 0; + private const int CTRL_BREAK_EVENT = 1; + private const int CTRL_CLOSE_EVENT = 2; + private const int CTRL_LOGOFF_EVENT = 5; + private const int CTRL_SHUTDOWN_EVENT = 6; + + private static unsafe void SimulatePosixSignalWindows(string pipeHandleAsString) + { + try + { + using var readPipe = new AnonymousPipeClientStream(PipeDirection.In, pipeHandleAsString); + + int signal = (int)readPipe.ReadByte(); + + int ctrlType = (int)signal switch + { + SIGINT => CTRL_C_EVENT, + SIGQUIT => CTRL_BREAK_EVENT, + SIGTERM => CTRL_SHUTDOWN_EVENT, + _ => throw new ArgumentOutOfRangeException(nameof(signal), "Unsupported signal") + }; + +#if NETFRAMEWORK + if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT) + { + var handlerMethod = Type.GetType("System.Console, mscorlib")?.GetMethod("BreakEvent", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(handlerMethod); + handlerMethod.Invoke(null, [ctrlType]); + } + else // CTRL_SHUTDOWN_EVENT + { + var handlerField = typeof(AppDomain).GetField("_processExit", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(handlerField); + EventHandler handler = (EventHandler)handlerField.GetValue(AppDomain.CurrentDomain); + Assert.NotNull(handler); + handler.Invoke(AppDomain.CurrentDomain, null); + } +#else + // get the System.Runtime.InteropServices.PosixSignalRegistration.HandlerRoutine private method + var handlerMethod = typeof(PosixSignalRegistration).GetMethod("HandlerRoutine", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(handlerMethod); + + var handlerPtr = handlerMethod.MethodHandle.GetFunctionPointer(); + delegate* unmanaged handler = (delegate* unmanaged)handlerPtr; + + handler(ctrlType); + + if (signal == SIGTERM) + { + // on Windows the OS will kill the process immediately after this + Environment.FailFast("Simulating shutdown"); + } +#endif + } + catch (Exception ex) + { + // Exceptions on this thread will not be observed, nor will they cause the process to exit. + // Use failfast to ensure the process will exit without running any handlers. + Environment.FailFast(ex.ToString()); + } + } + private class EnsureSignalContinuesMainMethodWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj index ea7f4fcbce05a0..cb06f819fbfe5f 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj @@ -4,6 +4,7 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent);$(NetFrameworkCurrent) true true + true true true