Skip to content

Commit d6114d2

Browse files
committed
Add cleanup_socket param on create_unix_server()
This is derived from python/cpython#111483 but available on all Python versions with uvloop, only that it's only enabled by default for Python 3.13 and above to be consistent with CPython behavior.
1 parent 0019ff9 commit d6114d2

File tree

5 files changed

+69
-18
lines changed

5 files changed

+69
-18
lines changed

tests/test_unix.py

+24-16
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,18 @@ async def start_server():
9696

9797
self.assertFalse(srv.is_serving())
9898

99-
# asyncio doesn't cleanup the sock file
100-
self.assertTrue(os.path.exists(sock_name))
99+
if sys.version_info < (3, 13):
100+
# asyncio doesn't cleanup the sock file under Python 3.13
101+
self.assertTrue(os.path.exists(sock_name))
102+
else:
103+
self.assertFalse(os.path.exists(sock_name))
104+
105+
async def start_server_sock(start_server, is_unix_api=True):
106+
# is_unix_api indicates whether `start_server` is calling
107+
# `loop.create_unix_server()` or `loop.create_server()`,
108+
# because asyncio `loop.create_server()` doesn't cleanup
109+
# the socket file even if it's a UNIX socket.
101110

102-
async def start_server_sock(start_server):
103111
nonlocal CNT
104112
CNT = 0
105113

@@ -140,8 +148,11 @@ async def start_server_sock(start_server):
140148

141149
self.assertFalse(srv.is_serving())
142150

143-
# asyncio doesn't cleanup the sock file
144-
self.assertTrue(os.path.exists(sock_name))
151+
if sys.version_info < (3, 13) or not is_unix_api:
152+
# asyncio doesn't cleanup the sock file under Python 3.13
153+
self.assertTrue(os.path.exists(sock_name))
154+
else:
155+
self.assertFalse(os.path.exists(sock_name))
145156

146157
with self.subTest(func='start_unix_server(host, port)'):
147158
self.loop.run_until_complete(start_server())
@@ -160,7 +171,7 @@ async def start_server_sock(start_server):
160171
lambda sock: asyncio.start_server(
161172
handle_client,
162173
None, None,
163-
sock=sock)))
174+
sock=sock), is_unix_api=False))
164175
self.assertEqual(CNT, TOTAL_CNT)
165176

166177
def test_create_unix_server_2(self):
@@ -455,16 +466,13 @@ def test_create_unix_server_path_stream_bittype(self):
455466
socket.AF_UNIX, socket.SOCK_STREAM | socket.SOCK_NONBLOCK)
456467
with tempfile.NamedTemporaryFile() as file:
457468
fn = file.name
458-
try:
459-
with sock:
460-
sock.bind(fn)
461-
coro = self.loop.create_unix_server(lambda: None, path=None,
462-
sock=sock)
463-
srv = self.loop.run_until_complete(coro)
464-
srv.close()
465-
self.loop.run_until_complete(srv.wait_closed())
466-
finally:
467-
os.unlink(fn)
469+
with sock:
470+
sock.bind(fn)
471+
coro = self.loop.create_unix_server(lambda: None, path=None,
472+
sock=sock, cleanup_socket=True)
473+
srv = self.loop.run_until_complete(coro)
474+
srv.close()
475+
self.loop.run_until_complete(srv.wait_closed())
468476

469477
@unittest.skipUnless(sys.platform.startswith('linux'), 'requires epoll')
470478
def test_epollhup(self):

uvloop/handles/pipe.pyx

+21
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@ cdef class UnixServer(UVStreamServer):
8080
context)
8181
return <UVStream>tr
8282

83+
cdef _close(self):
84+
sock = self._fileobj
85+
if sock is not None and sock in self._loop._unix_server_sockets:
86+
path = sock.getsockname()
87+
else:
88+
path = None
89+
90+
UVStreamServer._close(self)
91+
92+
if path is not None:
93+
prev_ino = self._loop._unix_server_sockets[sock]
94+
del self._loop._unix_server_sockets[sock]
95+
try:
96+
if os_stat(path).st_ino == prev_ino:
97+
os_unlink(path)
98+
except FileNotFoundError:
99+
pass
100+
except OSError as err:
101+
aio_logger.error('Unable to clean up listening UNIX socket '
102+
'%r: %r', path, err)
103+
83104

84105
@cython.no_gc_clear
85106
cdef class UnixTransport(UVStream):

uvloop/includes/stdlib.pxi

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ cdef os_pipe = os.pipe
112112
cdef os_read = os.read
113113
cdef os_remove = os.remove
114114
cdef os_stat = os.stat
115+
cdef os_unlink = os.unlink
115116
cdef os_fspath = os.fspath
116117

117118
cdef stat_S_ISSOCK = stat.S_ISSOCK

uvloop/loop.pxd

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ cdef class Loop:
5858
set _processes
5959
dict _fd_to_reader_fileobj
6060
dict _fd_to_writer_fileobj
61+
dict _unix_server_sockets
6162

6263
set _signals
6364
dict _signal_handlers

uvloop/loop.pyx

+22-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ include "errors.pyx"
5050
cdef:
5151
int PY39 = PY_VERSION_HEX >= 0x03090000
5252
int PY311 = PY_VERSION_HEX >= 0x030b0000
53+
int PY313 = PY_VERSION_HEX >= 0x030d0000
5354
uint64_t MAX_SLEEP = 3600 * 24 * 365 * 100
5455

5556

@@ -155,6 +156,8 @@ cdef class Loop:
155156
self._fd_to_reader_fileobj = {}
156157
self._fd_to_writer_fileobj = {}
157158

159+
self._unix_server_sockets = {}
160+
158161
self._timers = set()
159162
self._polls = {}
160163

@@ -1704,7 +1707,10 @@ cdef class Loop:
17041707
'host/port and sock can not be specified at the same time')
17051708
return await self.create_unix_server(
17061709
protocol_factory, sock=sock, backlog=backlog, ssl=ssl,
1707-
start_serving=start_serving)
1710+
start_serving=start_serving,
1711+
# asyncio won't clean up socket file using create_server() API
1712+
cleanup_socket=False,
1713+
)
17081714

17091715
server = Server(self)
17101716

@@ -2089,7 +2095,7 @@ cdef class Loop:
20892095
*, backlog=100, sock=None, ssl=None,
20902096
ssl_handshake_timeout=None,
20912097
ssl_shutdown_timeout=None,
2092-
start_serving=True):
2098+
start_serving=True, cleanup_socket=PY313):
20932099
"""A coroutine which creates a UNIX Domain Socket server.
20942100
20952101
The return value is a Server object, which can be used to stop
@@ -2114,6 +2120,11 @@ cdef class Loop:
21142120
ssl_shutdown_timeout is the time in seconds that an SSL server
21152121
will wait for completion of the SSL shutdown before aborting the
21162122
connection. Default is 30s.
2123+
2124+
If *cleanup_socket* is true then the Unix socket will automatically
2125+
be removed from the filesystem when the server is closed, unless the
2126+
socket has been replaced after the server has been created.
2127+
This defaults to True on Python 3.13 and above, or False otherwise.
21172128
"""
21182129
cdef:
21192130
UnixServer pipe
@@ -2191,6 +2202,15 @@ cdef class Loop:
21912202
# we want Python socket object to notice that.
21922203
sock.setblocking(False)
21932204

2205+
if cleanup_socket:
2206+
path = sock.getsockname()
2207+
# Check for abstract socket. `str` and `bytes` paths are supported.
2208+
if path[0] not in (0, '\x00'):
2209+
try:
2210+
self._unix_server_sockets[sock] = os_stat(path).st_ino
2211+
except FileNotFoundError:
2212+
pass
2213+
21942214
pipe = UnixServer.new(
21952215
self, protocol_factory, server, backlog,
21962216
ssl, ssl_handshake_timeout, ssl_shutdown_timeout)

0 commit comments

Comments
 (0)