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.
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.
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:
Most of the thanks goes to @Stephen Cleary, so go upvote his answer. I'm no hero.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With