Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-111246: Remove listening Unix socket on close #111483

Merged
merged 6 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
10 changes: 9 additions & 1 deletion Doc/library/asyncio-eventloop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ Creating network servers
*, sock=None, backlog=100, ssl=None, \
ssl_handshake_timeout=None, \
ssl_shutdown_timeout=None, \
start_serving=True)
start_serving=True, cleanup_socket=True)

Similar to :meth:`loop.create_server` but works with the
:py:const:`~socket.AF_UNIX` socket family.
Expand All @@ -786,6 +786,10 @@ Creating network servers
:class:`str`, :class:`bytes`, and :class:`~pathlib.Path` paths
are supported.

If *cleanup_socket* is True then the Unix socket will automatically
be removed from the filesystem when the server is closed, unless the
socket has been replaced after the server has been created.

See the documentation of the :meth:`loop.create_server` method
for information about arguments to this method.

Expand All @@ -800,6 +804,10 @@ Creating network servers

Added the *ssl_shutdown_timeout* parameter.

.. versionchanged:: 3.13

Added the *cleanup_socket* parameter.


.. coroutinemethod:: loop.connect_accepted_socket(protocol_factory, \
sock, *, ssl=None, ssl_handshake_timeout=None, \
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ array
It can be used instead of ``'u'`` type code, which is deprecated.
(Contributed by Inada Naoki in :gh:`80480`.)

asyncio
-------

* :meth:`asyncio.loop.create_unix_server` will now automatically remove
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved
the Unix socket when the server is closed.

copy
----

Expand Down
33 changes: 32 additions & 1 deletion Lib/asyncio/unix_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
def __init__(self, selector=None):
super().__init__(selector)
self._signal_handlers = {}
self._unix_server_sockets = {}

def close(self):
super().close()
Expand Down Expand Up @@ -284,7 +285,7 @@ async def create_unix_server(
sock=None, backlog=100, ssl=None,
ssl_handshake_timeout=None,
ssl_shutdown_timeout=None,
start_serving=True):
start_serving=True, cleanup_socket=True):
if isinstance(ssl, bool):
raise TypeError('ssl argument must be an SSLContext or None')

Expand Down Expand Up @@ -340,6 +341,15 @@ async def create_unix_server(
raise ValueError(
f'A UNIX Domain Stream Socket was expected, got {sock!r}')

if cleanup_socket:
path = sock.getsockname()
# Check for abstract socket. `str` and `bytes` paths are supported.
if path[0] not in (0, '\x00'):
try:
self._unix_server_sockets[sock] = os.stat(path).st_ino
except FileNotFoundError:
pass

sock.setblocking(False)
server = base_events.Server(self, [sock], protocol_factory,
ssl, backlog, ssl_handshake_timeout,
Expand Down Expand Up @@ -460,6 +470,27 @@ def cb(fut):
self.remove_writer(fd)
fut.add_done_callback(cb)

def _stop_serving(self, sock):
# Is this a unix socket that needs cleanup?
if sock in self._unix_server_sockets:
path = sock.getsockname()
else:
path = None

super()._stop_serving(sock)

if path is not None:
prev_ino = self._unix_server_sockets[sock]
del self._unix_server_sockets[sock]
try:
if os.stat(path).st_ino == prev_ino:
os.unlink(path)
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved
except FileNotFoundError:
pass
except OSError as err:
logger.error('Unable to clean up listening UNIX socket '
'%r: %r', path, err)


class _UnixReadPipeTransport(transports.ReadTransport):

Expand Down
70 changes: 70 additions & 0 deletions Lib/test/test_asyncio/test_server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import asyncio
import os
import socket
import time
import threading
import unittest
Expand Down Expand Up @@ -147,6 +149,74 @@ async def serve(*args):
await task2


class UnixServerCleanupTests(unittest.IsolatedAsyncioTestCase):
@socket_helper.skip_unless_bind_unix_socket
async def test_unix_server_addr_cleanup(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding one-line comments to the new tests explaining what each test checks? They look so similar I first thought you had accidentally copied the test a few times. :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. A commit with comments have been pushed.

with test_utils.unix_socket_path() as addr:
async def serve(*args):
pass

srv = await asyncio.start_unix_server(serve, addr)

srv.close()
self.assertFalse(os.path.exists(addr))

@socket_helper.skip_unless_bind_unix_socket
async def test_unix_server_sock_cleanup(self):
with test_utils.unix_socket_path() as addr:
async def serve(*args):
pass

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(addr)

srv = await asyncio.start_unix_server(serve, sock=sock)

srv.close()
self.assertFalse(os.path.exists(addr))

@socket_helper.skip_unless_bind_unix_socket
async def test_unix_server_cleanup_gone(self):
with test_utils.unix_socket_path() as addr:
async def serve(*args):
pass

sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(addr)

srv = await asyncio.start_unix_server(serve, sock=sock)

os.unlink(addr)

srv.close()

@socket_helper.skip_unless_bind_unix_socket
async def test_unix_server_cleanup_replaced(self):
with test_utils.unix_socket_path() as addr:
async def serve(*args):
pass

srv = await asyncio.start_unix_server(serve, addr)

os.unlink(addr)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(addr)

srv.close()
self.assertTrue(os.path.exists(addr))

@socket_helper.skip_unless_bind_unix_socket
async def test_unix_server_cleanup_prevented(self):
with test_utils.unix_socket_path() as addr:
async def serve(*args):
pass

srv = await asyncio.start_unix_server(serve, addr, cleanup_socket=False)

srv.close()
self.assertTrue(os.path.exists(addr))


@unittest.skipUnless(hasattr(asyncio, 'ProactorEventLoop'), 'Windows only')
class ProactorStartServerTests(BaseStartServer, unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:meth:`asyncio.loop.create_unix_server` will now automatically remove the
Unix socket when the server is closed.
Loading