Author eryksun
Recipients Chris Billington, eryksun, paul.moore, steve.dower, tim.golden, zach.ware
Date 2019-02-08.05:13:00
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1549602780.77.0.102500044432.issue35935@roundup.psfhosted.org>
In-reply-to
Content
Python's C signal handler sets a flag and returns, and the signal is eventually handled in the main thread. In Windows, this means the Python SIGINT handler won't be called so long as the main thread is blocked. (In Unix the signal is delivered on the main thread and interrupts most blocking calls.) 

In Python 3, our C signal handler also signals a SIGINT kernel event object. This gets used in functions such as time.sleep(). However, threading wait and join methods do not support this event. In principle they could, so long as the underlying implementation continues to use kernel semaphore objects, but that may change. There's been pressure to adopt native condition variables instead of using semaphores.

When you enable the default handler, that's actually the default console control-event handler. It simply exits via ExitProcess(STATUS_CONTROL_C_EXIT). This works because the console control event is delivered by creating a new thread that starts at a private CtrlRoutine function in kernelbase.dll, so it doesn't matter that the main thread may be blocked. By default SIGBREAK also executes the default handler, so Ctrl+Break almost always works to kill a console process. Shells such as cmd.exe usually ignore it, because it would be annoying if Ctrl+Break also killed the shell and destroyed the console window. 

Note also that Python's signal architecture cannot support CTRL_CLOSE_EVENT, even though it's also mapped to SIGBREAK. The problem is that our C handler simply sets a flag and returns. For the close event, the session server waits on the control thread for up to 5 seconds and then terminates the process. Thus the C signal handler returning immediately means our process will be killed long before our Python handler gets called.

We may need to actually handle the event, such as ensuring that atexit functions are called. Currently the only way to handle closing the console window and cases where the main thread is blocked is to install our own console control handler using ctypes or PyWin32. Usually we do this to ensure a clean, controlled shutdown. Here's what this looks like with ctypes:

    import ctypes
    from ctypes import wintypes

    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

    CTRL_C_EVENT = 0
    CTRL_BREAK_EVENT = 1
    CTRL_CLOSE_EVENT = 2

    HANDLER_ROUTINE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)
    kernel32.SetConsoleCtrlHandler.argtypes = (
        HANDLER_ROUTINE,
        wintypes.BOOL)

    @HANDLER_ROUTINE
    def handler(ctrl):
        if ctrl == CTRL_C_EVENT:
            handled = do_ctrl_c()
        elif ctrl == CTRL_BREAK_EVENT:
            handled = do_ctrl_break()
        elif ctrl == CTRL_CLOSE_EVENT:
            handled = do_ctrl_close()
        else:
            handled = False
        # If not handled, call the next handler.
        return handled 

    if not kernel32.SetConsoleCtrlHandler(handler, True):
        raise ctypes.WinError(ctypes.get_last_error())

The do_ctrl_* functions could simply be sys.exit(1), which will ensure that atexit handlers get called.
History
Date User Action Args
2019-02-08 05:13:01eryksunsetrecipients: + eryksun, paul.moore, tim.golden, zach.ware, steve.dower, Chris Billington
2019-02-08 05:13:00eryksunsetmessageid: <1549602780.77.0.102500044432.issue35935@roundup.psfhosted.org>
2019-02-08 05:13:00eryksunlinkissue35935 messages
2019-02-08 05:13:00eryksuncreate