diff --git a/mypy/ipc.py b/mypy/ipc.py index 607a27668caf..08ca0caf75f1 100644 --- a/mypy/ipc.py +++ b/mypy/ipc.py @@ -421,16 +421,36 @@ def ready_to_read(conns: Sequence[IPCBase], timeout: float | None = None) -> lis ov.cancel() raise IPCException(f"Failed to wait for connections: {_winapi.GetLastError()}") - # Check which pending operations completed, cancel the rest + # Cancel all pending operations. CancelIoEx is asynchronous, so an + # operation may have completed before the cancel took effect. We then + # wait for all operations to finalize and check each result: completed + # reads get their data saved and are marked ready; cancelled ones are + # simply skipped. This avoids a race between checking if an operation + # is signaled and cancelling it. + for _, ov in pending: + ov.cancel() for i, ov in pending: - if _winapi.WaitForSingleObject(ov.event, 0) == _winapi.WAIT_OBJECT_0: + try: _, err = ov.GetOverlappedResult(True) + except OSError as e: + err = e.winerror + # Cancellation is expected here; broken/disconnected pipes should be + # surfaced as readable so the follow-up receive observes EOF/closure. + if err not in ( + _winapi.ERROR_OPERATION_ABORTED, + _winapi.ERROR_BROKEN_PIPE, + _winapi.ERROR_NETNAME_DELETED, + ): + # Anything else is a real IPC failure, not part of the probe race. + raise + if err == _winapi.ERROR_OPERATION_ABORTED: + # Operation was successfully cancelled -- no data consumed. + continue + if err in (0, _winapi.ERROR_MORE_DATA): data = ov.getbuffer() if data: conns[i].buffer.extend(data) - ready.append(i) - else: - ov.cancel() + ready.append(i) return ready