Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can a .NET Core application on Windows trap a SIGTERM event?

I'm curious if a console app written in .NET Core running on Windows can intercept a SIGKILL event and basically know it's being terminated. Here's the code I'm trying:

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine($"Hello from PID {Process.GetCurrentProcess().Id}, press CTRL+C to exit.");

        AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()).Unloading += context =>
        {
            Console.WriteLine("SIGTERM received, exiting program...");
        };

        Console.CancelKeyPress += (s, e) =>
        {
            Console.WriteLine("SIGINT received, exiting program...");
            Environment.Exit(0);
        };

        try
        {
            await Task.Delay(Timeout.Infinite);
        }
        finally
        {
            Console.WriteLine("Finally executed..");
        }

    }

When I run the program from the command line, I can terminate it with the CTRL+C key combination. This will fire CancelKeyPress as well as Unloading. If I terminate the program other ways (Using the Windows Task Manager "End Process" function, or the Stop-Process PowerShell command), the process simply ends without any output written to the console.

This is part of a larger goal, which is to trap a Docker container shutting down. On Windows, the docker stop command will kill the main process using SIGTERM. This behavior is configurable on Linux Docker using the --stop-signal or STOPSIGNAL features, but those things have not been implemented on Windows.

like image 729
Mike Christensen Avatar asked Mar 04 '20 01:03

Mike Christensen


2 Answers

On Windows, the docker stop command will kill the main process using SIGTERM.

As of version 1709 (for both host and base image), Windows containers using the base image microsoft/windowsservercore or microsoft/nanoserver will send CTRL_CLOSE_EVENT to the main process only.

As of version 1803, Windows containers using those base images will send CTRL_SHUTDOWN_EVENT to all processes running in the container.

So, depending on your versions, Docker may send CTRL_CLOSE_EVENT or CTRL_SHUTDOWN_EVENT.

These are different than the Ctrl-C signal, which is CTRL_C_EVENT. .NET's Console.CancelKeyPress only handles CTRL_C_EVENT (or the closely-related CTRL_BREAK_EVENT), so it will not be called for CTRL_CLOSE_EVENT or CTRL_SHUTDOWN_EVENT.

If I terminate the program other ways (Using the Windows Task Manager "End Process" function, or the Stop-Process PowerShell command), the process simply ends without any output written to the console.

It is not possible to handle all kill signals. I believe Task Manager these days tries to do a "gentle" close which in this case is probably sending CTRL_CLOSE_EVENT, but it is also entirely possible to just TerminateProcess a process, which kills it immediately without any signals or anything.

But recognizing a Docker-initiated shutdown and responding to it is possible, since it sends a nice signal first and only terminates after a timeout.

The .NET Runtime will recognize CTRL_CLOSE_EVENT as a process exit request, but since newer versions of Docker for Windows have switched to CTRL_SHUTDOWN_EVENT, I believe that .NET will simply not do a graceful shutdown for Windows containers anymore.

The only workaround I'm aware of is to install your own Console control event handler. I recommend handling CTRL_C_EVENT, CTRL_CLOSE_EVENT, and CTRL_SHUTDOWN_EVENT all as just a generic "shutdown" signal.

like image 180
Stephen Cleary Avatar answered Oct 18 '22 08:10

Stephen Cleary


Figured I'd post the complete working code of a .NET Core console app which, when run in a Docker container (using docker run -d) can respond to a docker stop command. Please use this only for good!

using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace DockerTest
{
    class Program
    {
        private enum ConsoleControlEvent : uint { CTRL_C_EVENT = 0, CTRL_CLOSE_EVENT = 2, CTRL_SHUTDOWN_EVENT = 6 }

        private static void ConsoleCtrlHandler(ConsoleControlEvent controlType)
        {
            if (controlType == ConsoleControlEvent.CTRL_C_EVENT || controlType == ConsoleControlEvent.CTRL_CLOSE_EVENT ||
                controlType == ConsoleControlEvent.CTRL_SHUTDOWN_EVENT)
            {
                Console.WriteLine("Docker container is shutting down..");
                Environment.Exit(0);
            }
        }

        [UnmanagedFunctionPointer(CallingConvention.Winapi)]
        private delegate void SetConsoleCtrlHandler_HandlerRoutine(ConsoleControlEvent controlType);

        [DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)]
        private static extern bool SetConsoleCtrlHandler(SetConsoleCtrlHandler_HandlerRoutine handler,
            [MarshalAs(UnmanagedType.Bool)] bool add);

        static void Main()
        {
            Console.CancelKeyPress += (_, args) => { };
            if (!SetConsoleCtrlHandler(ConsoleCtrlHandler, add: true))
                throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());

            Console.WriteLine("Waiting to be stopped.");
            Thread.Sleep(-1);
        }
    }
}

And here's the proof:

enter image description here

Most of the thanks goes to @Stephen Cleary, so go upvote his answer. I'm no hero.

like image 30
Mike Christensen Avatar answered Oct 18 '22 10:10

Mike Christensen