Skip to content

Conversation

ericstj
Copy link
Member

@ericstj ericstj commented Jun 13, 2025

Fixes #115206

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.

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes an issue where the process is killed immediately after handling CTRL_SHUTDOWN_EVENT on Windows by introducing a delay in the shutdown handler. Key changes include enabling unsafe blocks in the test project, modifying the ConsoleLifetimeExitTests to simulate Windows signals via a pipe, and updating the Windows SIGTERM handler in ConsoleLifetime to delay its return.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj Enabled unsafe blocks for tests
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/ConsoleLifetimeExitTests.cs Updated signal simulation logic for Windows using an anonymous pipe and adjusted RemoteExecutor parameters
src/libraries/Microsoft.Extensions.Hosting/src/Internal/ConsoleLifetime.netcoreapp.cs Modified SIGTERM registration to use a dedicated Windows shutdown handler that delays exit via Thread.Sleep
Comments suppressed due to low confidence (2)

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

  • Review the use of the array initializer [ctrlType] in the method invocation. Although valid under new C# syntax, please double-check that it compiles as intended and produces the expected invocation behavior.
handlerMethod.Invoke(null, [ctrlType]);

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

  • The shutdown handler invokes Thread.Sleep using HostOptions.ShutdownTimeout to delay returning, but the test expects an exit code of 123. Verify that the use of Environment.FailFast in the signal simulation (for SIGTERM) does not conflict with the expected process exit behavior in a Windows environment.
Thread.Sleep(HostOptions.ShutdownTimeout);

Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-hosting
See info in area-owners.md if you want to be subscribed.

@ericstj ericstj force-pushed the hostingWindowsShutdown branch from cdd3e3a to 4e05c94 Compare June 13, 2025 20:17
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.
@ericstj ericstj force-pushed the hostingWindowsShutdown branch from 4e05c94 to 2c3c15b Compare June 13, 2025 20:19
@jkotas
Copy link
Member

jkotas commented Jun 14, 2025

The change in the shipping code looks good to me.

@ericstj ericstj requested review from a team and jozkee June 17, 2025 23:37
@ericstj ericstj merged commit 35584ab into dotnet:main Jul 11, 2025
85 of 87 checks passed
@AustinWise
Copy link
Contributor

Should you also use GC.SuppressFinalize on the PosixSignalRegistration? They are finalizable, so they could block the finalizer thread. This in turn would block the main thread from exiting, as the process exit event is delivered via the finalizer thread. See here and here.

proof of concept program

This shows the finalization of PosixSignalRegistration can block the processes from shutting down. Tested on .NET 9.0.7.

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

static class Program
{
    static readonly ManualResetEvent s_exiting = new ManualResetEvent(false);
    static PosixSignalRegistration? s_sigTermReg;
    static PosixSignalRegistration? s_sigIntReg;
    static PosixSignalRegistration? s_sigQuitReg;

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void CreateReg()
    {
        s_sigTermReg = PosixSignalRegistration.Create(PosixSignal.SIGTERM, HandleSigTerm);
        s_sigIntReg = PosixSignalRegistration.Create(PosixSignal.SIGINT, HandleSigTerm);
        s_sigQuitReg = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, HandleSigTerm);
    }

    static void Main(string[] args)
    {
        Console.WriteLine($"started {Environment.ProcessId}");
        CreateReg();
        s_exiting.WaitOne();
        Thread.Sleep(100);
        Console.WriteLine("exiting main");

        // Make sure the PosixSignalRegistration are ready to finialize
        for (int i = 0; i < 10; i++)
        {
            GC.Collect();
        }
        // Give the finializer thread some time to start and get blocked.
        Thread.Sleep(100);
    }

    static void HandleSigTerm(PosixSignalContext ctx)
    {
        Console.WriteLine($"Got {ctx.Signal}");
        s_sigTermReg = null;
        s_sigIntReg = null;
        s_sigQuitReg = null;
        s_exiting.Set();
        Thread.Sleep(Timeout.Infinite);
    }
}

Re: writing a more realistic unit test on Windows, you could use the GenerateConsoleCtrlEvent function with CTRL_BREAK_EVENT to trigger the SIGQUIT handler. The recently added CreateNewProcessGroup property should make this straightforward.

@jkotas
Copy link
Member

jkotas commented Jul 13, 2025

@AustinWise Good catch!

Should you also use GC.SuppressFinalize on the PosixSignalRegistration?

Keeping these registrations alive to prevent their finalization would be more appropriate. Calling GC.SuppressFinalize on a type that you do not own is not the best practice.

Even with the finalization suppressed, there seems to be an opportunity for dead locks: If ConsoleLifetime.Dispose gets called at the same as the signal is delivered, ConsoleLifetime.Dispose will deadlock on the Windows lock trying to unregister the handlers. We may need some sort of handshake between ConsoleLifetime.Dispose and the signal handler to prevent this.

@jkotas
Copy link
Member

jkotas commented Jul 17, 2025

I have opened #117753 on the potential deadlocks so that it does not get lost.

@github-actions github-actions bot locked and limited conversation to collaborators Aug 17, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IHostedService.StopAsync not invoked with 8.0-windowsservercore-ltsc2019 image

4 participants