diff options
author | Paolo Bonzini <pbonzini@redhat.com> | 2021-03-17 09:59:30 +0100 |
---|---|---|
committer | Jussi Pakkanen <jpakkane@gmail.com> | 2021-03-23 00:07:14 +0200 |
commit | ea48edbb0fcd567b26d94e27b9b61612f56728f5 (patch) | |
tree | ea131f8762c9318adc68e86188e54ac711c4adc6 | |
parent | 13d3fbbf3e37404b2c5b46ca45e2306307104f26 (diff) | |
download | meson-ea48edbb0fcd567b26d94e27b9b61612f56728f5.zip meson-ea48edbb0fcd567b26d94e27b9b61612f56728f5.tar.gz meson-ea48edbb0fcd567b26d94e27b9b61612f56728f5.tar.bz2 |
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>
-rw-r--r-- | mesonbuild/mtest.py | 50 |
1 files changed, 39 insertions, 11 deletions
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index bef829a..e54740e 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -1148,14 +1148,37 @@ async def complete(future: asyncio.Future) -> None: except asyncio.CancelledError: pass -async def complete_all(futures: T.Iterable[asyncio.Future]) -> None: - """Wait for completion of all the given futures, ignoring cancellation.""" - while futures: - done, futures = await asyncio.wait(futures, return_when=asyncio.FIRST_EXCEPTION) - # Raise exceptions if needed for all the "done" futures - for f in done: - if not f.cancelled(): +async def complete_all(futures: T.Iterable[asyncio.Future], + timeout: T.Optional[T.Union[int, float]] = None) -> None: + """Wait for completion of all the given futures, ignoring cancellation. + If timeout is not None, raise an asyncio.TimeoutError after the given + time has passed. asyncio.TimeoutError is only raised if some futures + have not completed and none have raised exceptions, even if timeout + 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() + 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: def __init__(self, p: asyncio.subprocess.Process, @@ -1167,6 +1190,7 @@ class TestSubprocess: self.stdo_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.all_futures = [] # type: T.List[asyncio.Future] def stdout_lines(self, console_mode: ConsoleUser) -> T.AsyncIterator[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: decode_coro = read_decode(self._process.stdout, console_mode) 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: decode_coro = read_decode(self._process.stderr, console_mode) self.stde_task = asyncio.ensure_future(decode_coro) + self.all_futures.append(self.stde_task) return self.stdo_task, self.stde_task @@ -1236,11 +1262,13 @@ class TestSubprocess: p = self._process result = None additional_error = None + + self.all_futures.append(asyncio.ensure_future(p.wait())) try: - await try_wait_one(p.wait(), timeout=timeout) - if p.returncode is None: - additional_error = await self._kill() - result = TestResult.TIMEOUT + await complete_all(self.all_futures, timeout=timeout) + except asyncio.TimeoutError: + additional_error = await self._kill() + result = TestResult.TIMEOUT except asyncio.CancelledError: # The main loop must have seen Ctrl-C. additional_error = await self._kill() |