Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f11d0d8

Browse files
authored
gh-91227: Ignore ERROR_PORT_UNREACHABLE in proactor recvfrom() (#32011)
1 parent 9967b56 commit f11d0d8

File tree

5 files changed

+174
-12
lines changed

5 files changed

+174
-12
lines changed

Lib/asyncio/windows_events.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import _overlapped
99
import _winapi
1010
import errno
11+
from functools import partial
1112
import math
1213
import msvcrt
1314
import socket
@@ -467,6 +468,18 @@ def finish_socket_func(trans, key, ov):
467468
else:
468469
raise
469470

471+
@classmethod
472+
def _finish_recvfrom(cls, trans, key, ov, *, empty_result):
473+
try:
474+
return cls.finish_socket_func(trans, key, ov)
475+
except OSError as exc:
476+
# WSARecvFrom will report ERROR_PORT_UNREACHABLE when the same
477+
# socket is used to send to an address that is not listening.
478+
if exc.winerror == _overlapped.ERROR_PORT_UNREACHABLE:
479+
return empty_result, None
480+
else:
481+
raise
482+
470483
def recv(self, conn, nbytes, flags=0):
471484
self._register_with_iocp(conn)
472485
ov = _overlapped.Overlapped(NULL)
@@ -501,7 +514,8 @@ def recvfrom(self, conn, nbytes, flags=0):
501514
except BrokenPipeError:
502515
return self._result((b'', None))
503516

504-
return self._register(ov, conn, self.finish_socket_func)
517+
return self._register(ov, conn, partial(self._finish_recvfrom,
518+
empty_result=b''))
505519

506520
def recvfrom_into(self, conn, buf, flags=0):
507521
self._register_with_iocp(conn)
@@ -511,17 +525,8 @@ def recvfrom_into(self, conn, buf, flags=0):
511525
except BrokenPipeError:
512526
return self._result((0, None))
513527

514-
def finish_recv(trans, key, ov):
515-
try:
516-
return ov.getresult()
517-
except OSError as exc:
518-
if exc.winerror in (_overlapped.ERROR_NETNAME_DELETED,
519-
_overlapped.ERROR_OPERATION_ABORTED):
520-
raise ConnectionResetError(*exc.args)
521-
else:
522-
raise
523-
524-
return self._register(ov, conn, finish_recv)
528+
return self._register(ov, conn, partial(self._finish_recvfrom,
529+
empty_result=0))
525530

526531
def sendto(self, conn, buf, flags=0, addr=None):
527532
self._register_with_iocp(conn)

Lib/test/test_asyncio/test_events.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,80 @@ def test_create_datagram_endpoint_sock(self):
13781378
tr.close()
13791379
self.loop.run_until_complete(pr.done)
13801380

1381+
def test_datagram_send_to_non_listening_address(self):
1382+
# see:
1383+
# https://github.com/python/cpython/issues/91227
1384+
# https://github.com/python/cpython/issues/88906
1385+
# https://bugs.python.org/issue47071
1386+
# https://bugs.python.org/issue44743
1387+
# The Proactor event loop would fail to receive datagram messages after
1388+
# sending a message to an address that wasn't listening.
1389+
loop = self.loop
1390+
1391+
class Protocol(asyncio.DatagramProtocol):
1392+
1393+
_received_datagram = None
1394+
1395+
def datagram_received(self, data, addr):
1396+
self._received_datagram.set_result(data)
1397+
1398+
async def wait_for_datagram_received(self):
1399+
self._received_datagram = loop.create_future()
1400+
result = await asyncio.wait_for(self._received_datagram, 10)
1401+
self._received_datagram = None
1402+
return result
1403+
1404+
def create_socket():
1405+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1406+
sock.setblocking(False)
1407+
sock.bind(('127.0.0.1', 0))
1408+
return sock
1409+
1410+
socket_1 = create_socket()
1411+
transport_1, protocol_1 = loop.run_until_complete(
1412+
loop.create_datagram_endpoint(Protocol, sock=socket_1)
1413+
)
1414+
addr_1 = socket_1.getsockname()
1415+
1416+
socket_2 = create_socket()
1417+
transport_2, protocol_2 = loop.run_until_complete(
1418+
loop.create_datagram_endpoint(Protocol, sock=socket_2)
1419+
)
1420+
addr_2 = socket_2.getsockname()
1421+
1422+
# creating and immediately closing this to try to get an address that
1423+
# is not listening
1424+
socket_3 = create_socket()
1425+
transport_3, protocol_3 = loop.run_until_complete(
1426+
loop.create_datagram_endpoint(Protocol, sock=socket_3)
1427+
)
1428+
addr_3 = socket_3.getsockname()
1429+
transport_3.abort()
1430+
1431+
transport_1.sendto(b'a', addr=addr_2)
1432+
self.assertEqual(loop.run_until_complete(
1433+
protocol_2.wait_for_datagram_received()
1434+
), b'a')
1435+
1436+
transport_2.sendto(b'b', addr=addr_1)
1437+
self.assertEqual(loop.run_until_complete(
1438+
protocol_1.wait_for_datagram_received()
1439+
), b'b')
1440+
1441+
# this should send to an address that isn't listening
1442+
transport_1.sendto(b'c', addr=addr_3)
1443+
loop.run_until_complete(asyncio.sleep(0))
1444+
1445+
# transport 1 should still be able to receive messages after sending to
1446+
# an address that wasn't listening
1447+
transport_2.sendto(b'd', addr=addr_1)
1448+
self.assertEqual(loop.run_until_complete(
1449+
protocol_1.wait_for_datagram_received()
1450+
), b'd')
1451+
1452+
transport_1.close()
1453+
transport_2.close()
1454+
13811455
def test_internal_fds(self):
13821456
loop = self.create_event_loop()
13831457
if not isinstance(loop, selector_events.BaseSelectorEventLoop):

Lib/test/test_asyncio/test_sock_lowlevel.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,93 @@ class SelectEventLoopTests(BaseSockTestsMixin,
555555
def create_event_loop(self):
556556
return asyncio.SelectorEventLoop()
557557

558+
558559
class ProactorEventLoopTests(BaseSockTestsMixin,
559560
test_utils.TestCase):
560561

561562
def create_event_loop(self):
562563
return asyncio.ProactorEventLoop()
563564

565+
566+
async def _basetest_datagram_send_to_non_listening_address(self,
567+
recvfrom):
568+
# see:
569+
# https://github.com/python/cpython/issues/91227
570+
# https://github.com/python/cpython/issues/88906
571+
# https://bugs.python.org/issue47071
572+
# https://bugs.python.org/issue44743
573+
# The Proactor event loop would fail to receive datagram messages
574+
# after sending a message to an address that wasn't listening.
575+
576+
def create_socket():
577+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
578+
sock.setblocking(False)
579+
sock.bind(('127.0.0.1', 0))
580+
return sock
581+
582+
socket_1 = create_socket()
583+
addr_1 = socket_1.getsockname()
584+
585+
socket_2 = create_socket()
586+
addr_2 = socket_2.getsockname()
587+
588+
# creating and immediately closing this to try to get an address
589+
# that is not listening
590+
socket_3 = create_socket()
591+
addr_3 = socket_3.getsockname()
592+
socket_3.shutdown(socket.SHUT_RDWR)
593+
socket_3.close()
594+
595+
socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
596+
socket_2_recv_task = self.loop.create_task(recvfrom(socket_2))
597+
await asyncio.sleep(0)
598+
599+
await self.loop.sock_sendto(socket_1, b'a', addr_2)
600+
self.assertEqual(await socket_2_recv_task, b'a')
601+
602+
await self.loop.sock_sendto(socket_2, b'b', addr_1)
603+
self.assertEqual(await socket_1_recv_task, b'b')
604+
socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
605+
await asyncio.sleep(0)
606+
607+
# this should send to an address that isn't listening
608+
await self.loop.sock_sendto(socket_1, b'c', addr_3)
609+
self.assertEqual(await socket_1_recv_task, b'')
610+
socket_1_recv_task = self.loop.create_task(recvfrom(socket_1))
611+
await asyncio.sleep(0)
612+
613+
# socket 1 should still be able to receive messages after sending
614+
# to an address that wasn't listening
615+
socket_2.sendto(b'd', addr_1)
616+
self.assertEqual(await socket_1_recv_task, b'd')
617+
618+
socket_1.shutdown(socket.SHUT_RDWR)
619+
socket_1.close()
620+
socket_2.shutdown(socket.SHUT_RDWR)
621+
socket_2.close()
622+
623+
624+
def test_datagram_send_to_non_listening_address_recvfrom(self):
625+
async def recvfrom(socket):
626+
data, _ = await self.loop.sock_recvfrom(socket, 4096)
627+
return data
628+
629+
self.loop.run_until_complete(
630+
self._basetest_datagram_send_to_non_listening_address(
631+
recvfrom))
632+
633+
634+
def test_datagram_send_to_non_listening_address_recvfrom_into(self):
635+
async def recvfrom_into(socket):
636+
buf = bytearray(4096)
637+
length, _ = await self.loop.sock_recvfrom_into(socket, buf,
638+
4096)
639+
return buf[:length]
640+
641+
self.loop.run_until_complete(
642+
self._basetest_datagram_send_to_non_listening_address(
643+
recvfrom_into))
644+
564645
else:
565646
import selectors
566647

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix the asyncio ProactorEventLoop implementation so that sending a datagram to an address that is not listening does not prevent receiving any more datagrams.

Modules/overlapped.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,7 @@ overlapped_exec(PyObject *module)
20562056
WINAPI_CONSTANT(F_DWORD, ERROR_OPERATION_ABORTED);
20572057
WINAPI_CONSTANT(F_DWORD, ERROR_SEM_TIMEOUT);
20582058
WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_BUSY);
2059+
WINAPI_CONSTANT(F_DWORD, ERROR_PORT_UNREACHABLE);
20592060
WINAPI_CONSTANT(F_DWORD, INFINITE);
20602061
WINAPI_CONSTANT(F_HANDLE, INVALID_HANDLE_VALUE);
20612062
WINAPI_CONSTANT(F_HANDLE, NULL);

0 commit comments

Comments
 (0)