Skip to content

Commit 92ea517

Browse files
committed
Fix process handles leak when uv_spawn() fails (#187)
1 parent 491a068 commit 92ea517

File tree

5 files changed

+65
-3
lines changed

5 files changed

+65
-3
lines changed

.ci/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ cython==0.28.3
22
aiohttp
33
tinys3
44
twine
5+
psutil

requirements.dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
Cython==0.28.3
22
Sphinx>=1.4.1
3+
psutil

tests/test_process.py

+45
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
import time
1111
import unittest
1212

13+
import psutil
14+
1315
from uvloop import _testbase as tb
1416

1517

1618
class _TestProcess:
19+
def get_num_fds(self):
20+
return psutil.Process(os.getpid()).num_fds()
21+
1722
def test_process_env_1(self):
1823
async def test():
1924
cmd = 'echo $FOO$BAR'
@@ -330,6 +335,46 @@ async def test():
330335

331336
self.loop.run_until_complete(test())
332337

338+
def test_subprocess_fd_leak_1(self):
339+
async def main(n):
340+
for i in range(n):
341+
try:
342+
await asyncio.create_subprocess_exec(
343+
'nonexistant',
344+
loop=self.loop,
345+
stdout=subprocess.DEVNULL,
346+
stderr=subprocess.DEVNULL)
347+
except FileNotFoundError:
348+
pass
349+
await asyncio.sleep(0, loop=self.loop)
350+
351+
self.loop.run_until_complete(main(10))
352+
num_fd_1 = self.get_num_fds()
353+
self.loop.run_until_complete(main(10))
354+
num_fd_2 = self.get_num_fds()
355+
356+
self.assertEqual(num_fd_1, num_fd_2)
357+
358+
def test_subprocess_fd_leak_2(self):
359+
async def main(n):
360+
for i in range(n):
361+
try:
362+
p = await asyncio.create_subprocess_exec(
363+
'ls',
364+
loop=self.loop,
365+
stdout=subprocess.DEVNULL,
366+
stderr=subprocess.DEVNULL)
367+
finally:
368+
await p.wait()
369+
await asyncio.sleep(0, loop=self.loop)
370+
371+
self.loop.run_until_complete(main(10))
372+
num_fd_1 = self.get_num_fds()
373+
self.loop.run_until_complete(main(10))
374+
num_fd_2 = self.get_num_fds()
375+
376+
self.assertEqual(num_fd_1, num_fd_2)
377+
333378

334379
class _AsyncioTests:
335380

uvloop/handles/process.pxd

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ cdef class UVProcess(UVHandle):
2020
char *uv_opt_file
2121
bytes __cwd
2222

23+
cdef _close_process_handle(self)
24+
2325
cdef _init(self, Loop loop, list args, dict env, cwd,
2426
start_new_session,
2527
_stdin, _stdout, _stderr, pass_fds,

uvloop/handles/process.pyx

+16-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ cdef class UVProcess(UVHandle):
1111
self._preexec_fn = None
1212
self._restore_signals = True
1313

14+
cdef _close_process_handle(self):
15+
# XXX: This is a workaround for a libuv bug:
16+
# - https://github.com/libuv/libuv/issues/1933
17+
# - https://github.com/libuv/libuv/pull/551
18+
if self._handle is NULL:
19+
return
20+
self._handle.data = NULL
21+
uv.uv_close(self._handle, __uv_close_process_handle_cb)
22+
self._handle = NULL # close callback will free() the memory
23+
1424
cdef _init(self, Loop loop, list args, dict env,
1525
cwd, start_new_session,
1626
_stdin, _stdout, _stderr, # std* can be defined as macros in C
@@ -79,16 +89,15 @@ cdef class UVProcess(UVHandle):
7989

8090
if _PyImport_ReleaseLock() < 0:
8191
# See CPython/posixmodule.c for details
92+
self._close_process_handle()
8293
if err < 0:
8394
self._abort_init()
8495
else:
8596
self._close()
8697
raise RuntimeError('not holding the import lock')
8798

8899
if err < 0:
89-
if UVLOOP_DEBUG and uv.uv_is_active(self._handle):
90-
raise RuntimeError(
91-
'active uv_process_t handle after failed uv_spawn')
100+
self._close_process_handle()
92101
self._abort_init()
93102
raise convert_error(err)
94103

@@ -754,3 +763,7 @@ cdef __socketpair():
754763
os_set_inheritable(fds[1], False)
755764

756765
return fds[0], fds[1]
766+
767+
768+
cdef void __uv_close_process_handle_cb(uv.uv_handle_t* handle) with gil:
769+
PyMem_RawFree(handle)

0 commit comments

Comments
 (0)