mtest: switch to asyncio subprocesses

No functional change except that the extra thread goes away.
pull/7836/head
Paolo Bonzini 4 years ago
parent 09253c1c70
commit a2c134c30b
  1. 54
      mesonbuild/mtest.py

@ -34,7 +34,6 @@ import subprocess
import sys import sys
import tempfile import tempfile
import textwrap import textwrap
import threading
import time import time
import typing as T import typing as T
import xml.etree.ElementTree as et 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], async def _run_subprocess(self, args: T.List[str], *, timeout: T.Optional[int],
stdout: T.IO, stderr: T.IO, stdout: T.IO, stderr: T.IO,
env: T.Dict[str, str], cwd: T.Optional[str]) -> T.Tuple[int, TestResult, T.Optional[str]]: 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 # Python does not provide multiplatform support for
# killing a process and all its children so we need # killing a process and all its children so we need
# to roll our own. # to roll our own.
@ -694,40 +693,31 @@ class SingleTestRunner:
# Make sure the termination signal actually kills the process # Make sure the termination signal actually kills the process
# group, otherwise retry with a SIGKILL. # group, otherwise retry with a SIGKILL.
await try_wait_one(f, timeout=0.5) await try_wait_one(p.wait(), timeout=0.5)
if f.done(): if p.returncode is not None:
return None return None
os.killpg(p.pid, signal.SIGKILL) os.killpg(p.pid, signal.SIGKILL)
await try_wait_one(f, timeout=1) await try_wait_one(p.wait(), timeout=1)
if f.done(): if p.returncode is not None:
return None return None
# An earlier kill attempt has not worked for whatever reason. # An earlier kill attempt has not worked for whatever reason.
# Try to kill it one last time with a direct call. # Try to kill it one last time with a direct call.
# If the process has spawned children, they will remain around. # If the process has spawned children, they will remain around.
p.kill() p.kill()
await try_wait_one(f, timeout=1) await try_wait_one(p.wait(), timeout=1)
if f.done(): if p.returncode is not None:
return None return None
return 'Test process could not be killed.' return 'Test process could not be killed.'
except ProcessLookupError: except ProcessLookupError:
# Sometimes (e.g. with Wine) this happens. There's nothing # Sometimes (e.g. with Wine) this happens. There's nothing
# we can do, probably the process already died so just wait # we can do, probably the process already died so just wait
# for the event loop to pick that up. # for the event loop to pick that up.
await f await p.wait()
return None 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 # Let gdb handle ^C instead of us
if self.options.gdb: if self.options.gdb:
previous_sigint_handler = signal.getsignal(signal.SIGINT) previous_sigint_handler = signal.getsignal(signal.SIGINT)
@ -745,37 +735,31 @@ class SingleTestRunner:
# errors avoid not being able to use the terminal. # errors avoid not being able to use the terminal.
os.setsid() os.setsid()
p = subprocess.Popen(args, p = await asyncio.create_subprocess_exec(*args,
stdout=stdout, stdout=stdout,
stderr=stderr, stderr=stderr,
env=env, env=env,
cwd=cwd, cwd=cwd,
preexec_fn=preexec_fn if not is_windows() else None) preexec_fn=preexec_fn if not is_windows() else None)
result = None result = None
additional_error = 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: try:
await try_wait_one(future, timeout=timeout) await try_wait_one(p.wait(), timeout=timeout)
if not future.done(): if p.returncode is None:
if self.options.verbose: if self.options.verbose:
print('{} time out (After {} seconds)'.format(self.test.name, timeout)) 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 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 kill_process(p, future) additional_error = await kill_process(p)
result = TestResult.INTERRUPT result = TestResult.INTERRUPT
finally: finally:
if self.options.gdb: if self.options.gdb:
# Let us accept ^C again # Let us accept ^C again
signal.signal(signal.SIGINT, previous_sigint_handler) signal.signal(signal.SIGINT, previous_sigint_handler)
if future.done(): return p.returncode or 0, result, additional_error
return future.result(), result, None
else:
return 0, result, additional_error
async def _run_cmd(self, cmd: T.List[str]) -> TestRun: async def _run_cmd(self, cmd: T.List[str]) -> TestRun:
starttime = time.time() starttime = time.time()

Loading…
Cancel
Save