Skip to content

Commit fb5a139

Browse files
Add cleanup_socket param on create_unix_server() (#623)
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. * Also add Python 3.13 to CI (#610) * Enable CI in debug mode * Fix CI to include [dev] --------- Co-authored-by: Fantix King <[email protected]>
1 parent 3fba9fa commit fb5a139

File tree

9 files changed

+91
-24
lines changed

9 files changed

+91
-24
lines changed

.github/workflows/release.yml

+8-2
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,13 @@ jobs:
7676
fail-fast: false
7777
matrix:
7878
os: [ubuntu-latest, macos-latest]
79-
cibw_python: ["cp38-*", "cp39-*", "cp310-*", "cp311-*", "cp312-*"]
79+
cibw_python:
80+
- "cp38-*"
81+
- "cp39-*"
82+
- "cp310-*"
83+
- "cp311-*"
84+
- "cp312-*"
85+
- "cp313-*"
8086
cibw_arch: ["x86_64", "aarch64", "universal2"]
8187
exclude:
8288
- os: ubuntu-latest
@@ -108,7 +114,7 @@ jobs:
108114
run: |
109115
brew install gnu-sed libtool autoconf automake
110116
111-
- uses: pypa/cibuildwheel@fff9ec32ed25a9c576750c91e06b410ed0c15db7 # v2.16.2
117+
- uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0
112118
env:
113119
CIBW_BUILD_VERBOSITY: 1
114120
CIBW_BUILD: ${{ matrix.cibw_python }}

.github/workflows/tests.yml

+12-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
18+
python-version:
19+
- "3.8"
20+
- "3.9"
21+
- "3.10"
22+
- "3.11"
23+
- "3.12"
24+
- "3.13"
1925
os: [ubuntu-latest, macos-latest]
2026

2127
env:
@@ -42,6 +48,7 @@ jobs:
4248
if: steps.release.outputs.version == 0
4349
with:
4450
python-version: ${{ matrix.python-version }}
51+
allow-prereleases: true
4552

4653
- name: Install macOS deps
4754
if: matrix.os == 'macos-latest' && steps.release.outputs.version == 0
@@ -50,17 +57,18 @@ jobs:
5057
5158
- name: Install Python Deps
5259
if: steps.release.outputs.version == 0
60+
env:
61+
PIP_PRE: ${{ matrix.python-version == '3.13' && '1' || '0' }}
5362
run: |
54-
pip install -e .[test]
63+
pip install -e .[test,dev]
5564
5665
- name: Test
5766
if: steps.release.outputs.version == 0
5867
run: |
5968
make test
6069
6170
- name: Test (debug build)
62-
# XXX Re-enable 3.12 once we migrate to Cython 3
63-
if: steps.release.outputs.version == 0 && matrix.python-version != '3.12'
71+
if: steps.release.outputs.version == 0
6472
run: |
6573
make distclean && make debug && make test
6674

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ classifiers = [
2424
"Programming Language :: Python :: 3.10",
2525
"Programming Language :: Python :: 3.11",
2626
"Programming Language :: Python :: 3.12",
27+
"Programming Language :: Python :: 3.13",
2728
"Programming Language :: Python :: Implementation :: CPython",
2829
"Topic :: System :: Networking",
2930
]

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def finalize_options(self):
140140
v = True
141141

142142
directives[k] = v
143+
self.cython_directives = directives
143144

144145
self.distribution.ext_modules[:] = cythonize(
145146
self.distribution.ext_modules,

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)