Author eryksun
Recipients eryksun, paul.moore, steve.dower, tim.golden, zach.ware, zmwangx
Date 2020-07-29.22:20:55
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1596061255.87.0.146848383548.issue41437@roundup.psfhosted.org>
In-reply-to
Content
Winsock is inherently asynchronous. It implements synchronous functions by using an alertable wait for the completion of an asynchronous I/O request. Python doesn't implement anything for a console Ctrl+C event to alert the main thread when it's blocked in an alterable wait. NTAPI NtAlertThread will alert a thread in this case, but it won't help here because Winsock just rewaits when alerted. 

You need a user-mode asynchronous procedure call (APC) to make the waiting thread cancel all of its pended I/O request packets (IRPs) for the given file (socket) handle. Specifically, open a handle to the thread, and call QueueUserAPC to queue an APC to the thread that calls WinAPI CancelIo on the file handle. (I don't suggest using the newer CancelIoEx function from an arbitrary thread context in this case. It would be simpler than queuing an APC to the target thread, but you don't have an OVERLAPPED record to cancel a specific IRP, so it would cancel IRPs for all threads.)

Here's a context manager that temporarily sets a Ctrl+C handler that implements the above suggestion:

    import ctypes
    import threading
    import contextlib

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

    CTRL_C_EVENT = 0
    THREAD_SET_CONTEXT = 0x0010

    @contextlib.contextmanager
    def ctrl_cancel_async_io(file_handle):
        apc_sync_event = threading.Event()
        hthread = kernel32.OpenThread(THREAD_SET_CONTEXT, False,
            kernel32.GetCurrentThreadId())
        if not hthread:
            raise ctypes.WinError(ctypes.get_last_error())

        @ctypes.WINFUNCTYPE(None, ctypes.c_void_p)
        def apc_cancel_io(ignored):
            kernel32.CancelIo(file_handle)
            apc_sync_event.set()

        @ctypes.WINFUNCTYPE(ctypes.c_uint, ctypes.c_uint)
        def ctrl_handler(ctrl_event):
            # For a Ctrl+C cancel event, queue an async procedure call
            # to the target thread that cancels pending async I/O for
            # the given file handle.
            if ctrl_event == CTRL_C_EVENT:
                kernel32.QueueUserAPC(apc_cancel_io, hthread, None)
                # Synchronize here in case the APC was queued to the
                # main thread, else apc_cancel_io might get interrupted
                # by a KeyboardInterrupt.
                apc_sync_event.wait()
            return False # chain to next handler

        try:
            kernel32.SetConsoleCtrlHandler(ctrl_handler, True)
            yield
        finally:
            kernel32.SetConsoleCtrlHandler(ctrl_handler, False)
            kernel32.CloseHandle(hthread)


Use it as follows in your sample code:

    with ctrl_cancel_async_io(sock.fileno()):
        sock.sendall(b"hello")
        sock.recv(1024)

Note that this requires the value of sock.fileno() to be an NT kernel handle for a file opened in asynchronous mode. This is the case for a socket.

HTH
History
Date User Action Args
2020-07-29 22:20:55eryksunsetrecipients: + eryksun, paul.moore, tim.golden, zach.ware, steve.dower, zmwangx
2020-07-29 22:20:55eryksunsetmessageid: <1596061255.87.0.146848383548.issue41437@roundup.psfhosted.org>
2020-07-29 22:20:55eryksunlinkissue41437 messages
2020-07-29 22:20:55eryksuncreate