blob: 28351a39ed371c72233d5eb616540647ab1551da [file] [log] [blame]
Brad Bishopbec4ebc2022-08-03 09:55:16 -04001import asyncio
2import re
3import subprocess
4import os
5import shutil
6import sys
7
8from .terminal import terminals
9
10
11def cli_from_config(config, terminal_choice):
12 cli = []
13 if config["fvp-bindir"]:
14 cli.append(os.path.join(config["fvp-bindir"], config["exe"]))
15 else:
16 cli.append(config["exe"])
17
18 for param, value in config["parameters"].items():
19 cli.extend(["--parameter", f"{param}={value}"])
20
21 for value in config["data"]:
22 cli.extend(["--data", value])
23
24 for param, value in config["applications"].items():
25 cli.extend(["--application", f"{param}={value}"])
26
27 for terminal, name in config["terminals"].items():
28 # If terminals are enabled and this terminal has been named
29 if terminal_choice != "none" and name:
30 # TODO if raw mode
31 # cli.extend(["--parameter", f"{terminal}.mode=raw"])
32 # TODO put name into terminal title
33 cli.extend(["--parameter", f"{terminal}.terminal_command={terminals[terminal_choice].command}"])
34 else:
35 # Disable terminal
36 cli.extend(["--parameter", f"{terminal}.start_telnet=0"])
37
38 cli.extend(config["args"])
39
40 return cli
41
42def check_telnet():
43 # Check that telnet is present
44 if not bool(shutil.which("telnet")):
45 raise RuntimeError("Cannot find telnet, this is needed to connect to the FVP.")
46
47class FVPRunner:
48 def __init__(self, logger):
49 self._terminal_ports = {}
50 self._line_callbacks = []
51 self._logger = logger
52 self._fvp_process = None
53 self._telnets = []
54 self._pexpects = []
55
56 def add_line_callback(self, callback):
57 self._line_callbacks.append(callback)
58
59 async def start(self, config, extra_args=[], terminal_choice="none"):
60 cli = cli_from_config(config, terminal_choice)
61 cli += extra_args
Patrick Williams8dd68482022-10-04 07:57:18 -050062
63 # Pass through environment variables needed for GUI applications, such
64 # as xterm, to work.
65 env = config['env']
66 for name in ('DISPLAY', 'WAYLAND_DISPLAY'):
67 if name in os.environ:
68 env[name] = os.environ[name]
69
Brad Bishopbec4ebc2022-08-03 09:55:16 -040070 self._logger.debug(f"Constructed FVP call: {cli}")
Patrick Williams8dd68482022-10-04 07:57:18 -050071 self._fvp_process = await asyncio.create_subprocess_exec(
72 *cli,
73 stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
74 env=env)
Brad Bishopbec4ebc2022-08-03 09:55:16 -040075
76 def detect_terminals(line):
77 m = re.match(r"^(\S+): Listening for serial connection on port (\d+)$", line)
78 if m:
79 terminal = m.group(1)
80 port = int(m.group(2))
81 self._terminal_ports[terminal] = port
82 self.add_line_callback(detect_terminals)
83
84 async def stop(self):
85 if self._fvp_process:
86 self._logger.debug(f"Terminating FVP PID {self._fvp_process.pid}")
87 try:
88 self._fvp_process.terminate()
89 await asyncio.wait_for(self._fvp_process.wait(), 10.0)
90 except asyncio.TimeoutError:
91 self._logger.debug(f"Killing FVP PID {self._fvp_process.pid}")
92 self._fvp_process.kill()
93 except ProcessLookupError:
94 pass
95
96 for telnet in self._telnets:
97 try:
98 telnet.terminate()
99 await asyncio.wait_for(telnet.wait(), 10.0)
100 except asyncio.TimeoutError:
101 telnet.kill()
102 except ProcessLookupError:
103 pass
104
105 for console in self._pexpects:
106 import pexpect
107 # Ensure pexpect logs all remaining output to the logfile
108 console.expect(pexpect.EOF, timeout=5.0)
109 console.close()
110
Patrick Williams2194f502022-10-16 14:26:09 -0500111 if self._fvp_process and self._fvp_process.returncode and \
112 self._fvp_process.returncode > 0:
113 # Return codes < 0 indicate that the process was explicitly
114 # terminated above.
Brad Bishopbec4ebc2022-08-03 09:55:16 -0400115 self._logger.info(f"FVP quit with code {self._fvp_process.returncode}")
116 return self._fvp_process.returncode
117 else:
118 return 0
119
120 async def run(self, until=None):
121 if until and until():
122 return
123
124 async for line in self._fvp_process.stdout:
125 line = line.strip().decode("utf-8", errors="replace")
126 for callback in self._line_callbacks:
127 callback(line)
128 if until and until():
129 return
130
131 async def _get_terminal_port(self, terminal, timeout):
132 def terminal_exists():
133 return terminal in self._terminal_ports
134 await asyncio.wait_for(self.run(terminal_exists), timeout)
135 return self._terminal_ports[terminal]
136
137 async def create_telnet(self, terminal, timeout=15.0):
138 check_telnet()
139 port = await self._get_terminal_port(terminal, timeout)
140 telnet = await asyncio.create_subprocess_exec("telnet", "localhost", str(port), stdin=sys.stdin, stdout=sys.stdout)
141 self._telnets.append(telnet)
142 return telnet
143
144 async def create_pexpect(self, terminal, timeout=15.0, **kwargs):
145 check_telnet()
146 import pexpect
147 port = await self._get_terminal_port(terminal, timeout)
148 instance = pexpect.spawn(f"telnet localhost {port}", **kwargs)
149 self._pexpects.append(instance)
150 return instance
151
152 def pid(self):
153 return self._fvp_process.pid