Skip to content

Commit 3214cf6

Browse files
graingertfantix
andauthored
use a stack of self._fds_to_close to prevent double closes (#481)
* 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]>
1 parent ada43c0 commit 3214cf6

File tree

3 files changed

+66
-21
lines changed

3 files changed

+66
-21
lines changed

tests/test_process.py

+53-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import subprocess
88
import sys
99
import tempfile
10+
import textwrap
1011
import time
1112
import unittest
1213

@@ -796,7 +797,58 @@ async def test():
796797

797798

798799
class Test_UV_Process(_TestProcess, tb.UVTestCase):
799-
pass
800+
def test_process_double_close(self):
801+
script = textwrap.dedent("""
802+
import os
803+
import sys
804+
from unittest import mock
805+
806+
import asyncio
807+
808+
pipes = []
809+
original_os_pipe = os.pipe
810+
def log_pipes():
811+
pipe = original_os_pipe()
812+
pipes.append(pipe)
813+
return pipe
814+
815+
dups = []
816+
original_os_dup = os.dup
817+
def log_dups(*args, **kwargs):
818+
dup = original_os_dup(*args, **kwargs)
819+
dups.append(dup)
820+
return dup
821+
822+
with mock.patch(
823+
"os.close", wraps=os.close
824+
) as os_close, mock.patch(
825+
"os.pipe", new=log_pipes
826+
), mock.patch(
827+
"os.dup", new=log_dups
828+
):
829+
import uvloop
830+
831+
832+
async def test():
833+
proc = await asyncio.create_subprocess_exec(
834+
sys.executable, "-c", "pass"
835+
)
836+
await proc.communicate()
837+
838+
uvloop.install()
839+
asyncio.run(test())
840+
841+
stdin, stdout, stderr = dups
842+
(r, w), = pipes
843+
assert os_close.mock_calls == [
844+
mock.call(w),
845+
mock.call(r),
846+
mock.call(stderr),
847+
mock.call(stdout),
848+
mock.call(stdin),
849+
]
850+
""")
851+
subprocess.run([sys.executable, '-c', script], check=True)
800852

801853

802854
class Test_AIO_Process(_TestProcess, tb.AIOTestCase):

uvloop/handles/process.pxd

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cdef class UVProcess(UVHandle):
88
object _preexec_fn
99
bint _restore_signals
1010

11-
set _fds_to_close
11+
list _fds_to_close
1212

1313
# Attributes used to compose uv_process_options_t:
1414
uv.uv_process_options_t options

uvloop/handles/process.pyx

+12-19
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ cdef class UVProcess(UVHandle):
77
self.uv_opt_args = NULL
88
self._returncode = None
99
self._pid = None
10-
self._fds_to_close = set()
10+
self._fds_to_close = list()
1111
self._preexec_fn = None
1212
self._restore_signals = True
1313
self.context = Context_CopyCurrent()
@@ -69,6 +69,11 @@ cdef class UVProcess(UVHandle):
6969
'Racing with another loop to spawn a process.')
7070

7171
self._errpipe_read, self._errpipe_write = os_pipe()
72+
fds_to_close = self._fds_to_close
73+
self._fds_to_close = None
74+
fds_to_close.append(self._errpipe_read)
75+
# add the write pipe last so we can close it early
76+
fds_to_close.append(self._errpipe_write)
7277
try:
7378
os_set_inheritable(self._errpipe_write, True)
7479

@@ -100,8 +105,8 @@ cdef class UVProcess(UVHandle):
100105

101106
self._finish_init()
102107

103-
os_close(self._errpipe_write)
104-
self._errpipe_write = -1
108+
# close the write pipe early
109+
os_close(fds_to_close.pop())
105110

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

117122
finally:
118-
os_close(self._errpipe_read)
119-
try:
120-
os_close(self._errpipe_write)
121-
except OSError:
122-
# Might be already closed
123-
pass
124-
125-
fds_to_close = self._fds_to_close
126-
self._fds_to_close = None
127-
for fd in fds_to_close:
128-
os_close(fd)
123+
while fds_to_close:
124+
os_close(fds_to_close.pop())
129125

130126
for fd in restore_inheritable:
131127
os_set_inheritable(fd, False)
@@ -202,7 +198,7 @@ cdef class UVProcess(UVHandle):
202198
if self._fds_to_close is None:
203199
raise RuntimeError(
204200
'UVProcess._close_after_spawn called after uv_spawn')
205-
self._fds_to_close.add(fd)
201+
self._fds_to_close.append(fd)
206202

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

503-
newfd = os_dup(io[1])
504-
os_set_inheritable(newfd, True)
505-
self._close_after_spawn(newfd)
506-
io[2] = newfd
499+
io[2] = self._file_redirect_stdio(io[1])
507500
elif _stderr == subprocess_DEVNULL:
508501
io[2] = self._file_devnull()
509502
else:

0 commit comments

Comments
 (0)