Skip to content

Commit

Permalink
use a stack of self._fds_to_close to prevent double closes (#481)
Browse files Browse the repository at this point in the history
* Add test for preexec_fn fd double close issue
* use a stack of self._fds_to_close to prevent double closes

and make tests easier to write because the close order is deterministic
and in the order that opens happen in

this should also be a bit faster because list.append is faster
than set.add and we skip a call to os_close(-1) and catching an
OSError exception

* DRY os_dup call

Co-authored-by: Fantix King <[email protected]>
  • Loading branch information
graingert and fantix authored Aug 23, 2022
1 parent ada43c0 commit 3214cf6
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 21 deletions.
54 changes: 53 additions & 1 deletion tests/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import subprocess
import sys
import tempfile
import textwrap
import time
import unittest

Expand Down Expand Up @@ -796,7 +797,58 @@ async def test():


class Test_UV_Process(_TestProcess, tb.UVTestCase):
pass
def test_process_double_close(self):
script = textwrap.dedent("""
import os
import sys
from unittest import mock
import asyncio
pipes = []
original_os_pipe = os.pipe
def log_pipes():
pipe = original_os_pipe()
pipes.append(pipe)
return pipe
dups = []
original_os_dup = os.dup
def log_dups(*args, **kwargs):
dup = original_os_dup(*args, **kwargs)
dups.append(dup)
return dup
with mock.patch(
"os.close", wraps=os.close
) as os_close, mock.patch(
"os.pipe", new=log_pipes
), mock.patch(
"os.dup", new=log_dups
):
import uvloop
async def test():
proc = await asyncio.create_subprocess_exec(
sys.executable, "-c", "pass"
)
await proc.communicate()
uvloop.install()
asyncio.run(test())
stdin, stdout, stderr = dups
(r, w), = pipes
assert os_close.mock_calls == [
mock.call(w),
mock.call(r),
mock.call(stderr),
mock.call(stdout),
mock.call(stdin),
]
""")
subprocess.run([sys.executable, '-c', script], check=True)


class Test_AIO_Process(_TestProcess, tb.AIOTestCase):
Expand Down
2 changes: 1 addition & 1 deletion uvloop/handles/process.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ cdef class UVProcess(UVHandle):
object _preexec_fn
bint _restore_signals

set _fds_to_close
list _fds_to_close

# Attributes used to compose uv_process_options_t:
uv.uv_process_options_t options
Expand Down
31 changes: 12 additions & 19 deletions uvloop/handles/process.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ cdef class UVProcess(UVHandle):
self.uv_opt_args = NULL
self._returncode = None
self._pid = None
self._fds_to_close = set()
self._fds_to_close = list()
self._preexec_fn = None
self._restore_signals = True
self.context = Context_CopyCurrent()
Expand Down Expand Up @@ -69,6 +69,11 @@ cdef class UVProcess(UVHandle):
'Racing with another loop to spawn a process.')

self._errpipe_read, self._errpipe_write = os_pipe()
fds_to_close = self._fds_to_close
self._fds_to_close = None
fds_to_close.append(self._errpipe_read)
# add the write pipe last so we can close it early
fds_to_close.append(self._errpipe_write)
try:
os_set_inheritable(self._errpipe_write, True)

Expand Down Expand Up @@ -100,8 +105,8 @@ cdef class UVProcess(UVHandle):

self._finish_init()

os_close(self._errpipe_write)
self._errpipe_write = -1
# close the write pipe early
os_close(fds_to_close.pop())

if preexec_fn is not None:
errpipe_data = bytearray()
Expand All @@ -115,17 +120,8 @@ cdef class UVProcess(UVHandle):
break

finally:
os_close(self._errpipe_read)
try:
os_close(self._errpipe_write)
except OSError:
# Might be already closed
pass

fds_to_close = self._fds_to_close
self._fds_to_close = None
for fd in fds_to_close:
os_close(fd)
while fds_to_close:
os_close(fds_to_close.pop())

for fd in restore_inheritable:
os_set_inheritable(fd, False)
Expand Down Expand Up @@ -202,7 +198,7 @@ cdef class UVProcess(UVHandle):
if self._fds_to_close is None:
raise RuntimeError(
'UVProcess._close_after_spawn called after uv_spawn')
self._fds_to_close.add(fd)
self._fds_to_close.append(fd)

def __dealloc__(self):
if self.uv_opt_env is not NULL:
Expand Down Expand Up @@ -500,10 +496,7 @@ cdef class UVProcessTransport(UVProcess):
# shouldn't ever happen
raise RuntimeError('cannot apply subprocess.STDOUT')

newfd = os_dup(io[1])
os_set_inheritable(newfd, True)
self._close_after_spawn(newfd)
io[2] = newfd
io[2] = self._file_redirect_stdio(io[1])
elif _stderr == subprocess_DEVNULL:
io[2] = self._file_devnull()
else:
Expand Down

0 comments on commit 3214cf6

Please sign in to comment.