diff --git a/scripts/runfvp b/scripts/runfvp index e9654268..a5a436e8 100755 --- a/scripts/runfvp +++ b/scripts/runfvp @@ -1,7 +1,9 @@ #! /usr/bin/env python3 +import asyncio import json import os +import re import sys import subprocess import pathlib @@ -153,6 +155,34 @@ def parse_config(args, config): return cli +async def start_fvp(cli, console_cb): + try: + fvp_process = await asyncio.create_subprocess_exec(*cli, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + async for line in fvp_process.stdout: + line = line.strip().decode("utf-8", errors="replace") + if console_cb: + logger.debug(f"FVP output: {line}") + else: + print(line) + + # Look for serial connections opening + if console_cb: + m = re.match(fr"^(\S+): Listening for serial connection on port (\d+)$", line) + if m: + terminal = m.group(1) + port = int(m.group(2)) + logger.debug(f"Console for {terminal} started on port {port}") + # When we can assume Py3.7+, this can be create_task + asyncio.ensure_future(console_cb(terminal, port)) + finally: + # If we get cancelled or throw an exception, kill the FVP + logger.debug(f"Killing FVP PID {fvp_process.pid}") + fvp_process.terminate() + + if await fvp_process.wait() != 0: + logger.info(f"{cli[0]} quit with code {fvp_process.returncode}") + def runfvp(cli_args): args, fvp_args = parse_args(cli_args) config_file = find_config(args) @@ -166,31 +196,24 @@ def runfvp(cli_args): if not expected_terminal: logger.error("--console used but FVP_CONSOLE not set in machine configuration") sys.exit(1) - - fvp_process = subprocess.Popen(cli, bufsize=1, universal_newlines=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - import selectors, re - selector = selectors.DefaultSelector() - selector.register(fvp_process.stdout, selectors.EVENT_READ) - output = "" - looking = True - while fvp_process.poll() is None: - # TODO some sort of timeout for 'input never appeared' - events = selector.select(timeout=10) - for key, mask in events: - line = key.fileobj.readline() - output += line - if looking: - m = re.match(fr"^{expected_terminal}: Listening.+port ([0-9]+)$", line) - if m: - port = m.group(1) - subprocess.run(["telnet", "localhost", port]) - looking = False - if fvp_process.returncode: - logger.error(f"{fvp_process.args[0]} quit with code {fvp_process.returncode}:") - logger.error(output) else: - sys.exit(subprocess.run(cli).returncode) + expected_terminal = None + + async def console_started(name, port): + if name == expected_terminal: + telnet = await asyncio.create_subprocess_exec("telnet", "localhost", str(port), stdin=sys.stdin, stdout=sys.stdout) + await telnet.wait() + logger.debug(f"Telnet quit, cancelling tasks") + for t in asyncio.all_tasks(): + logger.debug(f"Cancelling {t}") + t.cancel() + + try: + # When we can assume Py3.7+, this can simply be asyncio.run() + loop = asyncio.get_event_loop() + loop.run_until_complete(asyncio.gather(start_fvp(cli, console_cb=console_started))) + except asyncio.CancelledError: + pass if __name__ == "__main__": try: