From a2c134c30bc02defa842e771907db16c6a647e1e Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Mon, 12 Oct 2020 16:06:13 +0200 Subject: [PATCH] mtest: switch to asyncio subprocesses No functional change except that the extra thread goes away. --- mesonbuild/mtest.py | 54 ++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index 6e4ac6ef1..92d02b3e9 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -34,7 +34,6 @@ import subprocess import sys import tempfile import textwrap -import threading import time import typing as T import xml.etree.ElementTree as et @@ -680,7 +679,7 @@ class SingleTestRunner: async def _run_subprocess(self, args: T.List[str], *, timeout: T.Optional[int], 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: subprocess.Popen, f: asyncio.Future) -> 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. @@ -694,40 +693,31 @@ class SingleTestRunner: # Make sure the termination signal actually kills the process # group, otherwise retry with a SIGKILL. - await try_wait_one(f, timeout=0.5) - if f.done(): + 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(f, timeout=1) - if f.done(): + 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(f, timeout=1) - if f.done(): + 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 f + await p.wait() return None - def wait(p: subprocess.Popen, loop: asyncio.AbstractEventLoop, f: asyncio.Future) -> None: - try: - p.wait() - loop.call_soon_threadsafe(f.set_result, p.returncode) - except BaseException as e: # lgtm [py/catch-base-exception] - # The exception will be raised again in the main thread, - # so catching BaseException is okay. - loop.call_soon_threadsafe(f.set_exception, e) - # Let gdb handle ^C instead of us if self.options.gdb: previous_sigint_handler = signal.getsignal(signal.SIGINT) @@ -745,37 +735,31 @@ class SingleTestRunner: # errors avoid not being able to use the terminal. os.setsid() - p = subprocess.Popen(args, - stdout=stdout, - stderr=stderr, - env=env, - cwd=cwd, - preexec_fn=preexec_fn if not is_windows() else None) + 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 - loop = asyncio.get_event_loop() - future = asyncio.get_event_loop().create_future() - threading.Thread(target=wait, args=(p, loop, future), daemon=True).start() try: - await try_wait_one(future, timeout=timeout) - if not future.done(): + 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, future) + 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, future) + 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) - if future.done(): - return future.result(), result, None - else: - return 0, result, additional_error + return p.returncode or 0, result, additional_error async def _run_cmd(self, cmd: T.List[str]) -> TestRun: starttime = time.time()