From 21ff6be9c98192116cebde3fa1a54bb112a8b3ac Mon Sep 17 00:00:00 2001 From: Pierre Ossman <ossman@cendio.se> Date: Fri, 27 Oct 2023 16:35:29 +0200 Subject: [PATCH 1/6] gh-111246: Remove listening Unix socket on close (#111246) Try to clean up the socket file we create so we don't add unused noise to the file system. --- Doc/library/asyncio-eventloop.rst | 5 ++++ Doc/whatsnew/3.13.rst | 6 ++++ Lib/asyncio/unix_events.py | 21 ++++++++++++++ Lib/test/test_asyncio/test_server.py | 29 +++++++++++++++++++ ...-10-30-14-47-23.gh-issue-111246.QJ_ehs.rst | 2 ++ 5 files changed, 63 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-10-30-14-47-23.gh-issue-111246.QJ_ehs.rst diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 75c459c0cb601f..435df34d6971ba 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -800,6 +800,11 @@ Creating network servers Added the *ssl_shutdown_timeout* parameter. + .. versionchanged:: 3.13 + + The Unix socket will automatically be removed from the filesystem + when the server is closed. + .. coroutinemethod:: loop.connect_accepted_socket(protocol_factory, \ sock, *, ssl=None, ssl_handshake_timeout=None, \ diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 34dd3ea8858ea2..b3e635b9d567fc 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -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 + the Unix socket when the server is closed. + copy ---- diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index 809f29eecba198..dad41b3f230032 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -460,6 +460,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.family == socket.AF_UNIX: + path = sock.getsockname() + if path == '': + path = None + # Check for abstract socket. `str` and `bytes` paths are supported. + elif path[0] in (0, '\x00'): + path = None + else: + path = None + + super()._stop_serving(sock) + + if path is not None: + try: + os.unlink(path) + except OSError as err: + logger.error('Unable to clean up listening UNIX socket ' + '%r: %r', path, err) + class _UnixReadPipeTransport(transports.ReadTransport): diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index 06d8b60f219f1a..b37bd3f8a34c8c 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -1,4 +1,6 @@ import asyncio +import os +import socket import time import threading import unittest @@ -147,6 +149,33 @@ 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): + 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)) + + @unittest.skipUnless(hasattr(asyncio, 'ProactorEventLoop'), 'Windows only') class ProactorStartServerTests(BaseStartServer, unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2023-10-30-14-47-23.gh-issue-111246.QJ_ehs.rst b/Misc/NEWS.d/next/Library/2023-10-30-14-47-23.gh-issue-111246.QJ_ehs.rst new file mode 100644 index 00000000000000..a9630de5cc6947 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-10-30-14-47-23.gh-issue-111246.QJ_ehs.rst @@ -0,0 +1,2 @@ +:meth:`asyncio.loop.create_unix_server` will now automatically remove the +Unix socket when the server is closed. From 21e9d8cec082e627cba95dbfbfe366deff7803e6 Mon Sep 17 00:00:00 2001 From: Pierre Ossman <ossman@cendio.se> Date: Fri, 27 Oct 2023 16:37:12 +0200 Subject: [PATCH 2/6] gh-111246: Don't log on redundant socket cleanup (#111246) --- Lib/asyncio/unix_events.py | 2 ++ Lib/test/test_asyncio/test_server.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index dad41b3f230032..f397a1ed1d7b68 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -477,6 +477,8 @@ def _stop_serving(self, sock): if path is not None: try: os.unlink(path) + except FileNotFoundError: + pass except OSError as err: logger.error('Unable to clean up listening UNIX socket ' '%r: %r', path, err) diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index b37bd3f8a34c8c..009b5093baf405 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -175,6 +175,21 @@ async def serve(*args): 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() + @unittest.skipUnless(hasattr(asyncio, 'ProactorEventLoop'), 'Windows only') class ProactorStartServerTests(BaseStartServer, unittest.TestCase): From df3b74edfe09de4322bf922430493d5fae24c82a Mon Sep 17 00:00:00 2001 From: Pierre Ossman <ossman@cendio.se> Date: Fri, 27 Oct 2023 17:08:22 +0200 Subject: [PATCH 3/6] gh-111246: Don't remove stolen Unix socket address (#111246) We only want to clean up *our* socket, so try to determine if we still own this address or if something else has replaced it. --- Doc/library/asyncio-eventloop.rst | 3 ++- Lib/asyncio/unix_events.py | 21 ++++++++++++++------- Lib/test/test_asyncio/test_server.py | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 435df34d6971ba..398bd1ab8ce268 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -803,7 +803,8 @@ Creating network servers .. versionchanged:: 3.13 The Unix socket will automatically be removed from the filesystem - when the server is closed. + when the server is closed, unless the socket has been replaced + after the server has been created. .. coroutinemethod:: loop.connect_accepted_socket(protocol_factory, \ diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index f397a1ed1d7b68..ba2a9e7fc57d9a 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -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() @@ -340,6 +341,14 @@ async def create_unix_server( raise ValueError( f'A UNIX Domain Stream Socket was expected, got {sock!r}') + 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, @@ -462,21 +471,19 @@ def cb(fut): def _stop_serving(self, sock): # Is this a unix socket that needs cleanup? - if sock.family == socket.AF_UNIX: + if sock in self._unix_server_sockets: path = sock.getsockname() - if path == '': - path = None - # Check for abstract socket. `str` and `bytes` paths are supported. - elif path[0] in (0, '\x00'): - path = None 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: - os.unlink(path) + if os.stat(path).st_ino == prev_ino: + os.unlink(path) except FileNotFoundError: pass except OSError as err: diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index 009b5093baf405..b8d35f8d213293 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -190,6 +190,21 @@ async def serve(*args): 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)) + @unittest.skipUnless(hasattr(asyncio, 'ProactorEventLoop'), 'Windows only') class ProactorStartServerTests(BaseStartServer, unittest.TestCase): From 93571fe1f922b47996f171a8d7a41928ada0e9d7 Mon Sep 17 00:00:00 2001 From: Pierre Ossman <ossman@cendio.se> Date: Fri, 27 Oct 2023 17:26:05 +0200 Subject: [PATCH 4/6] gh-111246: Add opt-out of Unix socket cleanup (#111246) Allow the implicit cleanup to be disabled in case there are some odd corner cases where this causes issues. --- Doc/library/asyncio-eventloop.rst | 10 ++++++---- Lib/asyncio/unix_events.py | 17 +++++++++-------- Lib/test/test_asyncio/test_server.py | 11 +++++++++++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 398bd1ab8ce268..8227ff7710484c 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -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. @@ -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. @@ -802,9 +806,7 @@ Creating network servers .. versionchanged:: 3.13 - 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. + Added the *cleanup_socket* parameter. .. coroutinemethod:: loop.connect_accepted_socket(protocol_factory, \ diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index ba2a9e7fc57d9a..58f8a997182f00 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -285,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') @@ -341,13 +341,14 @@ async def create_unix_server( raise ValueError( f'A UNIX Domain Stream Socket was expected, got {sock!r}') - 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 + 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, diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index b8d35f8d213293..d8ce3b43998665 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -205,6 +205,17 @@ async def serve(*args): 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): From acadacbf57c83123d6c28ed291eb79735a49f936 Mon Sep 17 00:00:00 2001 From: Pierre Ossman <ossman@cendio.se> Date: Mon, 6 Nov 2023 16:36:21 +0100 Subject: [PATCH 5/6] Mention gh issue in whatsnew --- Doc/whatsnew/3.13.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index b3e635b9d567fc..e2af8f27f74988 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -149,6 +149,7 @@ asyncio * :meth:`asyncio.loop.create_unix_server` will now automatically remove the Unix socket when the server is closed. + (Contributed by Pierre Ossman in :gh:`111246`.) copy ---- From 1db87b7ccd324e35d080715df3b3d8f0421efddc Mon Sep 17 00:00:00 2001 From: Pierre Ossman <ossman@cendio.se> Date: Mon, 6 Nov 2023 16:37:52 +0100 Subject: [PATCH 6/6] Add comments to new unix server cleanup tests --- Lib/test/test_asyncio/test_server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_asyncio/test_server.py b/Lib/test/test_asyncio/test_server.py index d8ce3b43998665..6b5c15b22bbea1 100644 --- a/Lib/test/test_asyncio/test_server.py +++ b/Lib/test/test_asyncio/test_server.py @@ -149,9 +149,11 @@ async def serve(*args): await task2 +# Test the various corner cases of Unix server socket removal class UnixServerCleanupTests(unittest.IsolatedAsyncioTestCase): @socket_helper.skip_unless_bind_unix_socket async def test_unix_server_addr_cleanup(self): + # Default scenario with test_utils.unix_socket_path() as addr: async def serve(*args): pass @@ -163,6 +165,7 @@ async def serve(*args): @socket_helper.skip_unless_bind_unix_socket async def test_unix_server_sock_cleanup(self): + # Using already bound socket with test_utils.unix_socket_path() as addr: async def serve(*args): pass @@ -177,6 +180,7 @@ async def serve(*args): @socket_helper.skip_unless_bind_unix_socket async def test_unix_server_cleanup_gone(self): + # Someone else has already cleaned up the socket with test_utils.unix_socket_path() as addr: async def serve(*args): pass @@ -192,6 +196,7 @@ async def serve(*args): @socket_helper.skip_unless_bind_unix_socket async def test_unix_server_cleanup_replaced(self): + # Someone else has replaced the socket with their own with test_utils.unix_socket_path() as addr: async def serve(*args): pass @@ -207,6 +212,7 @@ async def serve(*args): @socket_helper.skip_unless_bind_unix_socket async def test_unix_server_cleanup_prevented(self): + # Automatic cleanup explicitly disabled with test_utils.unix_socket_path() as addr: async def serve(*args): pass