Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .ci/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ cython==0.28.3
aiohttp
tinys3
twine
psutil
1 change: 1 addition & 0 deletions requirements.dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Cython==0.28.3
Sphinx>=1.4.1
psutil
45 changes: 45 additions & 0 deletions tests/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
import time
import unittest

import psutil

from uvloop import _testbase as tb


class _TestProcess:
def get_num_fds(self):
return psutil.Process(os.getpid()).num_fds()

def test_process_env_1(self):
async def test():
cmd = 'echo $FOO$BAR'
Expand Down Expand Up @@ -330,6 +335,46 @@ async def test():

self.loop.run_until_complete(test())

def test_subprocess_fd_leak_1(self):
async def main(n):
for i in range(n):
try:
await asyncio.create_subprocess_exec(
'nonexistant',
loop=self.loop,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except FileNotFoundError:
pass
await asyncio.sleep(0, loop=self.loop)

self.loop.run_until_complete(main(10))
num_fd_1 = self.get_num_fds()
self.loop.run_until_complete(main(10))
num_fd_2 = self.get_num_fds()

self.assertEqual(num_fd_1, num_fd_2)

def test_subprocess_fd_leak_2(self):
async def main(n):
for i in range(n):
try:
p = await asyncio.create_subprocess_exec(
'ls',
loop=self.loop,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
finally:
await p.wait()
await asyncio.sleep(0, loop=self.loop)

self.loop.run_until_complete(main(10))
num_fd_1 = self.get_num_fds()
self.loop.run_until_complete(main(10))
num_fd_2 = self.get_num_fds()

self.assertEqual(num_fd_1, num_fd_2)


class _AsyncioTests:

Expand Down
2 changes: 2 additions & 0 deletions uvloop/handles/process.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ cdef class UVProcess(UVHandle):
char *uv_opt_file
bytes __cwd

cdef _close_process_handle(self)

cdef _init(self, Loop loop, list args, dict env, cwd,
start_new_session,
_stdin, _stdout, _stderr, pass_fds,
Expand Down
19 changes: 16 additions & 3 deletions uvloop/handles/process.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ cdef class UVProcess(UVHandle):
self._preexec_fn = None
self._restore_signals = True

cdef _close_process_handle(self):
# XXX: This is a workaround for a libuv bug:
# - https://github.com/libuv/libuv/issues/1933
# - https://github.com/libuv/libuv/pull/551
if self._handle is NULL:
return
self._handle.data = NULL
uv.uv_close(self._handle, __uv_close_process_handle_cb)
self._handle = NULL # close callback will free() the memory

cdef _init(self, Loop loop, list args, dict env,
cwd, start_new_session,
_stdin, _stdout, _stderr, # std* can be defined as macros in C
Expand Down Expand Up @@ -79,16 +89,15 @@ cdef class UVProcess(UVHandle):

if _PyImport_ReleaseLock() < 0:
# See CPython/posixmodule.c for details
self._close_process_handle()
if err < 0:
self._abort_init()
else:
self._close()
raise RuntimeError('not holding the import lock')

if err < 0:
if UVLOOP_DEBUG and uv.uv_is_active(self._handle):
raise RuntimeError(
'active uv_process_t handle after failed uv_spawn')
self._close_process_handle()
self._abort_init()
raise convert_error(err)

Expand Down Expand Up @@ -754,3 +763,7 @@ cdef __socketpair():
os_set_inheritable(fds[1], False)

return fds[0], fds[1]


cdef void __uv_close_process_handle_cb(uv.uv_handle_t* handle) with gil:
PyMem_RawFree(handle)