diff options
author | Paolo Bonzini <pbonzini@redhat.com> | 2020-11-25 13:19:55 +0100 |
---|---|---|
committer | Paolo Bonzini <pbonzini@redhat.com> | 2021-01-07 19:20:40 +0100 |
commit | 0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530 (patch) | |
tree | e625956251ec60b6ef4f3c94d62ee1daed8a558c | |
parent | 63e26ba05fe9f367b8e83800ebd22ccb06204eda (diff) | |
download | meson-0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530.zip meson-0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530.tar.gz meson-0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530.tar.bz2 |
mtest: do not wait inside _run_subprocess
We would like SingleTestRunner to run code before waiting on the process,
for example starting tasks to read stdout and stderr.
Return a new object that is able to complete _run_subprocess's task.
In the next patch, SingleTestRunner will also use the object to get hold
of the stdout and stderr StreamReaders.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
-rw-r--r-- | mesonbuild/mtest.py | 147 |
1 files changed, 81 insertions, 66 deletions
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index 454646d..e2c2c56 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -924,6 +924,69 @@ async def complete_all(futures: T.Iterable[asyncio.Future]) -> None: if not f.cancelled(): f.result() +class TestSubprocess: + def __init__(self, p: asyncio.subprocess.Process, postwait_fn: T.Callable[[], None] = None): + self._process = p + self.postwait_fn = postwait_fn # type: T.Callable[[], None] + + async def _kill(self) -> T.Optional[str]: + # Python does not provide multiplatform support for + # killing a process and all its children so we need + # to roll our own. + p = self._process + try: + if is_windows(): + subprocess.run(['taskkill', '/F', '/T', '/PID', str(p.pid)]) + else: + # Send a termination signal to the process group that setsid() + # created - giving it a chance to perform any cleanup. + os.killpg(p.pid, signal.SIGTERM) + + # Make sure the termination signal actually kills the process + # group, otherwise retry with a SIGKILL. + await try_wait_one(p.wait(), timeout=0.5) + if p.returncode is not None: + return None + + os.killpg(p.pid, signal.SIGKILL) + + await try_wait_one(p.wait(), timeout=1) + if p.returncode is not None: + return None + + # An earlier kill attempt has not worked for whatever reason. + # Try to kill it one last time with a direct call. + # If the process has spawned children, they will remain around. + p.kill() + await try_wait_one(p.wait(), timeout=1) + if p.returncode is not None: + return None + return 'Test process could not be killed.' + except ProcessLookupError: + # Sometimes (e.g. with Wine) this happens. There's nothing + # we can do, probably the process already died so just wait + # for the event loop to pick that up. + await p.wait() + return None + + async def wait(self, timeout: T.Optional[int]) -> T.Tuple[int, TestResult, T.Optional[str]]: + p = self._process + result = None + additional_error = None + try: + await try_wait_one(p.wait(), timeout=timeout) + if p.returncode is None: + additional_error = await self._kill() + result = TestResult.TIMEOUT + except asyncio.CancelledError: + # The main loop must have seen Ctrl-C. + additional_error = await self._kill() + result = TestResult.INTERRUPT + finally: + if self.postwait_fn: + self.postwait_fn() + + return p.returncode or 0, result, additional_error class SingleTestRunner: @@ -969,48 +1032,9 @@ class SingleTestRunner: await self._run_cmd(wrap + cmd + self.test.cmd_args + self.options.test_args) return self.runobj - async def _run_subprocess(self, args: T.List[str], *, timeout: T.Optional[int], + async def _run_subprocess(self, args: T.List[str], *, stdout: T.IO, stderr: T.IO, - env: T.Dict[str, str], cwd: T.Optional[str]) -> T.Tuple[int, TestResult, T.Optional[str]]: - async def kill_process(p: asyncio.subprocess.Process) -> T.Optional[str]: - # Python does not provide multiplatform support for - # killing a process and all its children so we need - # to roll our own. - try: - if is_windows(): - subprocess.run(['taskkill', '/F', '/T', '/PID', str(p.pid)]) - else: - # Send a termination signal to the process group that setsid() - # created - giving it a chance to perform any cleanup. - os.killpg(p.pid, signal.SIGTERM) - - # Make sure the termination signal actually kills the process - # group, otherwise retry with a SIGKILL. - await try_wait_one(p.wait(), timeout=0.5) - if p.returncode is not None: - return None - - os.killpg(p.pid, signal.SIGKILL) - - await try_wait_one(p.wait(), timeout=1) - if p.returncode is not None: - return None - - # An earlier kill attempt has not worked for whatever reason. - # Try to kill it one last time with a direct call. - # If the process has spawned children, they will remain around. - p.kill() - await try_wait_one(p.wait(), timeout=1) - if p.returncode is not None: - return None - return 'Test process could not be killed.' - except ProcessLookupError: - # Sometimes (e.g. with Wine) this happens. There's nothing - # we can do, probably the process already died so just wait - # for the event loop to pick that up. - await p.wait() - return None - + env: T.Dict[str, str], cwd: T.Optional[str]) -> TestSubprocess: # Let gdb handle ^C instead of us if self.options.gdb: previous_sigint_handler = signal.getsignal(signal.SIGINT) @@ -1028,31 +1052,18 @@ class SingleTestRunner: # errors avoid not being able to use the terminal. os.setsid() + def postwait_fn() -> None: + if self.options.gdb: + # Let us accept ^C again + signal.signal(signal.SIGINT, previous_sigint_handler) + p = await asyncio.create_subprocess_exec(*args, stdout=stdout, stderr=stderr, env=env, cwd=cwd, preexec_fn=preexec_fn if not is_windows() else None) - result = None - additional_error = None - try: - await try_wait_one(p.wait(), timeout=timeout) - if p.returncode is None: - if self.options.verbose: - print('{} time out (After {} seconds)'.format(self.test.name, timeout)) - additional_error = await kill_process(p) - result = TestResult.TIMEOUT - except asyncio.CancelledError: - # The main loop must have seen Ctrl-C. - additional_error = await kill_process(p) - result = TestResult.INTERRUPT - finally: - if self.options.gdb: - # Let us accept ^C again - signal.signal(signal.SIGINT, previous_sigint_handler) - - return p.returncode or 0, result, additional_error + return TestSubprocess(p, postwait_fn=postwait_fn if not is_windows() else None) async def _run_cmd(self, cmd: T.List[str]) -> None: if self.test.extra_paths: @@ -1097,12 +1108,16 @@ class SingleTestRunner: else: timeout = self.test.timeout - returncode, result, additional_error = await self._run_subprocess(cmd + extra_cmd, - timeout=timeout, - stdout=stdout, - stderr=stderr, - env=self.env, - cwd=self.test.workdir) + p = await self._run_subprocess(cmd + extra_cmd, + stdout=stdout, + stderr=stderr, + env=self.env, + cwd=self.test.workdir) + + returncode, result, additional_error = await p.wait(timeout) + if result is TestResult.TIMEOUT and self.options.verbose: + print('{} time out (After {} seconds)'.format(self.test.name, timeout)) + if additional_error is None: if stdout is None: stdo = '' |