Handling SIGTERM in python on Windows

You have to be well awake to follow this post
2019-01-15 python windows

The truth

Let’s be clear upfront. There’s no real SIGTERM on Windows, it’s all lies.

SIGTERM is super useful on posix to tell a process to shutdown, while still giving it a chance to shutdown properly for a small amount of time. If it takes too long, then you SIGKILL it if it takes too much time.

With Win32 GUI applications that use a message loop, you can send a WM_QUIT and this works great, but for CLI applications, this is trickier.

Fake it until you make it

After reading MSDN for a few hours, you discover there’s CTRL_BREAK_EVENT that you can ‘kinda’ use as a signal. You can send this event to a console application via GenerateConsoleCtrlEvent(). Yay!

Albeit, you don’t send it to a process, you send it to a process group. So you need to start your child process as a new process group, otherwise everything blows up. Ok.

So you tell yourself: ok, I’m going to read the 561 words CreateProcess() Remarks section, use CREATE_NEW_PROCESS_GROUP value as creationflags argument to subprocess.Popen(). Everything’s cool.

Sending

When you want to send a SIGTERM to a process to initiate its shutdown, you use Popen.terminate(), right? Too bad, because it happens python stdlib’s subprocess.py doesn’t implement sending CTRL_BREAK_EVENT either in v3.7.2 nor v2.7.15, and instead chose to call the ill-named Win32 TerminateProcess() function which is a kill operation. So I wrote my own subprocess42.py wrapper that implements a terminate() method that sends the magical event. Well, you could use send_signal(), but you have to send the right one on the right OS, that’s annoying. So an overload of terminate() this is.

Why ‘42’ you ask? Because after 42 tries, maybe we’ve got it right, and 42 is the answer to everything.

Handling

Unlike real posix signal, on Windows you handle this event via SetConsoleCtrlHandler(). This function is a bit wonky with a 629 words Remarks section, including different behavior if the process loads gdi32.dll (python does).

Upon receiving the event, the OS (conhost.exe really) creates a new thread to service this event via your provided HandlerRoutine. Don’t forget to read the 362 words Remarks section! It’s by definition out of band as a new OS thread is implicitly created on your behalf, so the ‘signal’ has to be moved back in the main thread. How?

How python handles it

Since python v2.3 up to v2.7.15, a function PyCtrlHandler converts this into an event, which is then surfaced … in time.sleep() which means … that time.sleep() throws an IOError upon a signal.SIGBREAK (really, a CTRL_BREAK_EVENT on Windows). For those following at home, time.sleep() never throws upon receiving a SIGTERM on posix.

But don’t send a SIGBREAK to a process! Or a SIGTERM! Send a CTRL_BREAK_EVENT, which will be translated as a SIGBREAK. Catch a SIGBREAK! Trying to catch CTRL_BREAK_EVENT doesn’t work, since this isn’t really a signal.

That’s all fake anyway.

So .. what about python 3, you ask? Well, the code was removed to fix a bug with multiprocessing and signal handling is only setup once you start a child process, handling a specific multiprocess use case which has the comment:

static BOOL WINAPI
ctrl_c_handler(DWORD code)
{
    return TRUE;    /* We just ignore all control events. */
}

So well, too bad for python3 users for the moment.