mtest: timeout if the write side of pipes does not close

If a test program forks a child, the pipes might remain open and
"await stdo_task"/"await stde_task" will never complete in
SingleTestRunner._run_cmd().

Instead, catch them in TestSubprocess.wait() so that the whole
process group is killed.

Fixes: #8533
Reported-by: Bastien Nocera <hadess@hadess.net>
pull/8569/head
Paolo Bonzini 4 years ago committed by Jussi Pakkanen
parent 13d3fbbf3e
commit ea48edbb0f
  1. 50
      mesonbuild/mtest.py

@ -1148,14 +1148,37 @@ async def complete(future: asyncio.Future) -> None:
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
async def complete_all(futures: T.Iterable[asyncio.Future]) -> None: async def complete_all(futures: T.Iterable[asyncio.Future],
"""Wait for completion of all the given futures, ignoring cancellation.""" timeout: T.Optional[T.Union[int, float]] = None) -> None:
while futures: """Wait for completion of all the given futures, ignoring cancellation.
done, futures = await asyncio.wait(futures, return_when=asyncio.FIRST_EXCEPTION) If timeout is not None, raise an asyncio.TimeoutError after the given
# Raise exceptions if needed for all the "done" futures time has passed. asyncio.TimeoutError is only raised if some futures
for f in done: have not completed and none have raised exceptions, even if timeout
if not f.cancelled(): is zero."""
def check_futures(futures: T.Iterable[asyncio.Future]) -> None:
# Raise exceptions if needed
left = False
for f in futures:
if not f.done():
left = True
elif not f.cancelled():
f.result() f.result()
if left:
raise asyncio.TimeoutError
# Python is silly and does not have a variant of asyncio.wait with an
# absolute time as deadline.
deadline = None if timeout is None else asyncio.get_event_loop().time() + timeout
while futures and (timeout is None or timeout > 0):
done, futures = await asyncio.wait(futures, timeout=timeout,
return_when=asyncio.FIRST_EXCEPTION)
check_futures(done)
if deadline:
timeout = deadline - asyncio.get_event_loop().time()
check_futures(futures)
class TestSubprocess: class TestSubprocess:
def __init__(self, p: asyncio.subprocess.Process, def __init__(self, p: asyncio.subprocess.Process,
@ -1167,6 +1190,7 @@ class TestSubprocess:
self.stdo_task = None # type: T.Optional[asyncio.Future[str]] self.stdo_task = None # type: T.Optional[asyncio.Future[str]]
self.stde_task = None # type: T.Optional[asyncio.Future[str]] self.stde_task = None # type: T.Optional[asyncio.Future[str]]
self.postwait_fn = postwait_fn # type: T.Callable[[], None] self.postwait_fn = postwait_fn # type: T.Callable[[], None]
self.all_futures = [] # type: T.List[asyncio.Future]
def stdout_lines(self, console_mode: ConsoleUser) -> T.AsyncIterator[str]: def stdout_lines(self, console_mode: ConsoleUser) -> T.AsyncIterator[str]:
q = asyncio.Queue() # type: asyncio.Queue[T.Optional[str]] q = asyncio.Queue() # type: asyncio.Queue[T.Optional[str]]
@ -1181,9 +1205,11 @@ class TestSubprocess:
if self.stdo_task is None and self.stdout is not None: if self.stdo_task is None and self.stdout is not None:
decode_coro = read_decode(self._process.stdout, console_mode) decode_coro = read_decode(self._process.stdout, console_mode)
self.stdo_task = asyncio.ensure_future(decode_coro) self.stdo_task = asyncio.ensure_future(decode_coro)
self.all_futures.append(self.stdo_task)
if self.stderr is not None and self.stderr != asyncio.subprocess.STDOUT: if self.stderr is not None and self.stderr != asyncio.subprocess.STDOUT:
decode_coro = read_decode(self._process.stderr, console_mode) decode_coro = read_decode(self._process.stderr, console_mode)
self.stde_task = asyncio.ensure_future(decode_coro) self.stde_task = asyncio.ensure_future(decode_coro)
self.all_futures.append(self.stde_task)
return self.stdo_task, self.stde_task return self.stdo_task, self.stde_task
@ -1236,11 +1262,13 @@ class TestSubprocess:
p = self._process p = self._process
result = None result = None
additional_error = None additional_error = None
self.all_futures.append(asyncio.ensure_future(p.wait()))
try: try:
await try_wait_one(p.wait(), timeout=timeout) await complete_all(self.all_futures, timeout=timeout)
if p.returncode is None: except asyncio.TimeoutError:
additional_error = await self._kill() additional_error = await self._kill()
result = TestResult.TIMEOUT result = TestResult.TIMEOUT
except asyncio.CancelledError: except asyncio.CancelledError:
# The main loop must have seen Ctrl-C. # The main loop must have seen Ctrl-C.
additional_error = await self._kill() additional_error = await self._kill()

Loading…
Cancel
Save