Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Lib/asyncio/proactor_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,13 @@ def loop(f=None):
if self.is_closed():
return
f = self._proactor.accept(sock)
except (ConnectionResetError, ConnectionAbortedError):
# The peer closed the connection before it was accepted.
# The listening socket is still usable: keep accepting.
if self._debug:
logger.debug("Accept failed on socket %r",
sock, exc_info=True)
self.call_soon(loop)
except OSError as exc:
if sock.fileno() != -1:
self.call_exception_handler({
Expand Down
12 changes: 11 additions & 1 deletion Lib/asyncio/windows_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,14 @@ def accept(self, listener):
ov.AcceptEx(listener.fileno(), conn.fileno())

def finish_accept(trans, key, ov):
ov.getresult()
try:
ov.getresult()
except OSError as exc:
conn.close()
# The peer reset the connection before it was accepted.
if exc.winerror == _overlapped.ERROR_NETNAME_DELETED:
raise ConnectionResetError(*exc.args)
raise
# Use SO_UPDATE_ACCEPT_CONTEXT so getsockname() etc work.
buf = struct.pack('@P', listener.fileno())
conn.setsockopt(socket.SOL_SOCKET,
Expand All @@ -542,6 +549,9 @@ async def accept_coro(future, conn):
except exceptions.CancelledError:
conn.close()
raise
except ConnectionResetError:
# Reported to the caller through the future.
pass

future = self._register(ov, listener, finish_accept)
coro = accept_coro(future, conn)
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_asyncio/test_proactor_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,26 @@ def test_create_server(self, m_log):
self.assertTrue(self.sock.close.called)
self.assertTrue(m_log.error.called)

def test_create_server_connection_reset(self):
# gh-93821: a peer resetting its connection before it is accepted
# must not close the listening socket.
pf = mock.Mock()
call_soon = self.loop.call_soon = mock.Mock()
self.loop.call_exception_handler = mock.Mock()

self.loop._start_serving(pf, self.sock)
loop = call_soon.call_args[0][0]

for exc in ConnectionResetError, ConnectionAbortedError:
with self.subTest(exc=exc):
call_soon.reset_mock()
fut = mock.Mock()
fut.result.side_effect = exc()
loop(fut)
self.assertFalse(self.sock.close.called)
self.assertFalse(self.loop.call_exception_handler.called)
call_soon.assert_called_once_with(loop)

def test_create_server_cancel(self):
pf = mock.Mock()
call_soon = self.loop.call_soon = mock.Mock()
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_asyncio/test_windows_events.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import signal
import socket
import struct
import sys
import time
import threading
Expand All @@ -19,6 +20,7 @@
from asyncio import windows_utils
from test import support
from test.support import os_helper
from test.support import socket_helper
from test.test_asyncio import utils as test_utils


Expand Down Expand Up @@ -115,6 +117,38 @@ def test_close(self):
self.assertEqual(f.result(), b'')
b.close()

def test_accept_connection_reset(self):
# gh-93821: a connection reset while it waits in the listen queue
# makes AcceptEx fail with ERROR_NETNAME_DELETED. The server must
# keep accepting.
async def handle(reader, writer):
writer.close()

async def main():
server = await asyncio.start_server(handle, socket_helper.HOSTv4, 0)
port = server.sockets[0].getsockname()[1]
# Both connects complete while the loop is blocked here: the
# first is taken by the pending AcceptEx, the second waits in
# the listen queue and is reset there.
first = socket.create_connection((socket_helper.HOSTv4, port))
second = socket.create_connection((socket_helper.HOSTv4, port))
second.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
struct.pack('HH', 1, 0))
second.close()
try:
await asyncio.sleep(0.5)
reader, writer = await asyncio.wait_for(
asyncio.open_connection(socket_helper.HOSTv4, port),
support.SHORT_TIMEOUT)
writer.close()
await writer.wait_closed()
finally:
first.close()
server.close()
await server.wait_closed()

self.loop.run_until_complete(main())

def test_double_bind(self):
ADDRESS = r'\\.\pipe\test_double_bind-%s' % os.getpid()
server1 = windows_events.PipeServer(ADDRESS)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :class:`asyncio.ProactorEventLoop` servers that stopped accepting new
connections on Windows when a client reset its connection before it was
accepted (``WinError 64``, ``ERROR_NETNAME_DELETED``). The listening socket is
now kept open and the server continues to accept connections.
Loading