aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Newsome <tim@sifive.com>2023-06-23 17:08:28 -0700
committerTim Newsome <tim@sifive.com>2023-07-17 09:34:55 -0700
commite1cb5be2709f461459b07221a15587e388fa90be (patch)
treea0d879b802e6516ecab74e5676431df64b629d6f
parenta29522f3e4baec1a50beb01ec70d69a94ac0083c (diff)
downloadriscv-tests-e1cb5be2709f461459b07221a15587e388fa90be.zip
riscv-tests-e1cb5be2709f461459b07221a15587e388fa90be.tar.gz
riscv-tests-e1cb5be2709f461459b07221a15587e388fa90be.tar.bz2
Interact with OpenOCD CLI over stdin/stdout.
It's a bit messy to read the log file to get the output, but it seems to be flushed often so that this works. Also, added the `targets` method for retrieving the list of targets, and `wait_until_running` method to wait until all targets are in a running state.
-rw-r--r--debug/testlib.py141
1 files changed, 102 insertions, 39 deletions
diff --git a/debug/testlib.py b/debug/testlib.py
index 688829d..5c09f0a 100644
--- a/debug/testlib.py
+++ b/debug/testlib.py
@@ -293,6 +293,7 @@ class VcsSim:
pass
class Openocd:
+ # pylint: disable=too-many-instance-attributes
# pylint: disable-next=consider-using-with
logfile = tempfile.NamedTemporaryFile(prefix='openocd', suffix='.log')
logname = logfile.name
@@ -301,6 +302,7 @@ class Openocd:
freertos=False, debug_openocd=False):
self.timeout = timeout
self.debug_openocd = debug_openocd
+ self.command_count = 0
if server_cmd:
cmd = shlex.split(server_cmd)
@@ -313,15 +315,13 @@ class Openocd:
# line, since they are executed in order.
cmd += [
# Tell OpenOCD to bind gdb to an unused, ephemeral port.
- "--command",
- "gdb_port 0",
- # Disable tcl and telnet servers, since they are unused and because
- # the port numbers will conflict if multiple OpenOCD processes are
- # running on the same server.
- "--command",
- "tcl_port disabled",
- "--command",
- "telnet_port disabled",
+ "--command", "gdb_port 0",
+ # We don't use the TCL server.
+ "--command", "tcl_port disabled",
+ # Put the regular command prompt in stdin. Don't listen on a port
+ # because it will conflict if multiple OpenOCD instances are running
+ # at the same time.
+ "--command", "telnet_port pipe",
]
if config:
@@ -342,6 +342,9 @@ class Openocd:
# pylint: disable-next=consider-using-with
raw_logfile = open(Openocd.logname, "wb")
+ # pylint: disable-next=consider-using-with
+ self.read_log_fd = open(Openocd.logname, "rb")
+ self.log_buf = b""
try:
# pylint: disable-next=consider-using-with
spike_dasm = subprocess.Popen("spike-dasm", stdin=subprocess.PIPE,
@@ -363,12 +366,12 @@ class Openocd:
logfile.flush()
self.gdb_ports = []
- self.process = self.start(cmd, logfile, extra_env)
+ self.start(cmd, logfile, extra_env)
def start(self, cmd, logfile, extra_env):
combined_env = {**os.environ, **extra_env}
# pylint: disable-next=consider-using-with
- process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+ self.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=logfile, stderr=logfile, env=combined_env)
try:
@@ -376,38 +379,21 @@ class Openocd:
# using OpenOCD to communicate with a simulator this may take a
# long time, and gdb will time out when trying to connect if we
# attempt too early.
- start = time.time()
- messaged = False
- with open(Openocd.logname, "r", encoding='utf-8') as fd:
- while True:
- line = fd.readline()
- if not line:
- if not process.poll() is None:
- raise TestLibError("OpenOCD exited early.")
- time.sleep(0.1)
- continue
-
- m = re.search(
- r"Listening on port (\d+) for gdb connections", line)
- if m:
- self.gdb_ports.append(int(m.group(1)))
-
- if "telnet server disabled" in line:
- break
-
- if not messaged and time.time() - start > 1:
- messaged = True
- print("Waiting for OpenOCD to start...")
- if (time.time() - start) > self.timeout:
- raise TestLibError("Timed out waiting for OpenOCD to "
- "listen for gdb")
+
+ while True:
+ m = self.expect(
+ rb"(Listening on port (\d+) for gdb connections|"
+ rb"tcl server disabled)",
+ message="Waiting for OpenOCD to start up...")
+ if b"gdb" in m.group(1):
+ self.gdb_ports.append(int(m.group(2)))
+ else:
+ break
if self.debug_openocd:
# pylint: disable=consider-using-with
self.debugger = subprocess.Popen(["gnome-terminal", "-e",
- f"gdb --pid={process.pid}"])
- return process
-
+ f"gdb --pid={self.process.pid}"])
except Exception:
print_log(Openocd.logname)
raise
@@ -433,6 +419,83 @@ class Openocd:
return True
return False
+ def command(self, cmd):
+ """Write the command to OpenOCD's stdin. Return the output of the
+ command, minus the prompt."""
+ self.process.stdin.write(f"{cmd}\n".encode())
+ self.process.stdin.flush()
+ m = self.expect(re.escape(f"{cmd}\n".encode()))
+
+ # The prompt isn't flushed to the log, so send a unique command that
+ # lets us find where output of the last command ends.
+ magic = f"# {self.command_count}x".encode()
+ self.command_count += 1
+ self.process.stdin.write(magic + b"\n")
+ self.process.stdin.flush()
+ m = self.expect(rb"(.*)^> " + re.escape(magic))
+ return m.group(1)
+
+ def expect(self, regex, message=None):
+ """Wait for the regex to match the log, and return the match object. If
+ message is given, print it while waiting.
+ We read the logfile to tell us what OpenOCD has done."""
+ messaged = False
+ start = time.time()
+
+ while True:
+ for line in self.read_log_fd.readlines():
+ line = line.rstrip()
+ # Remove nulls, carriage returns, and newlines.
+ line = re.sub(rb"[\x00\r\n]+", b"", line)
+ # Remove debug messages.
+ debug_match = re.search(rb"Debug: \d+ \d+ .*", line)
+ if debug_match:
+ line = line[:debug_match.start()] + line[debug_match.end():]
+ self.log_buf += line
+ else:
+ self.log_buf += line + b"\n"
+
+ m = re.search(regex, self.log_buf, re.MULTILINE | re.DOTALL)
+ if m:
+ self.log_buf = self.log_buf[m.end():]
+ return m
+
+ if not self.process.poll() is None:
+ raise TestLibError("OpenOCD exited early.")
+
+ if message and not messaged and time.time() - start > 1:
+ messaged = True
+ print(message)
+
+ if (time.time() - start) > self.timeout:
+ raise TestLibError(f"Timed out waiting for {regex} in "
+ f"{Openocd.logname}")
+
+ time.sleep(0.1)
+
+ def targets(self):
+ """Run `targets` command."""
+ result = self.command("targets").decode()
+ # TargetName Type Endian TapName State
+ # -- ------------------ ---------- ------ ------------------ --------
+ # 0* riscv.cpu riscv little riscv.cpu halted
+ lines = result.splitlines()
+ headers = lines[0].split()
+ data = []
+ for line in lines[2:]:
+ data.append(dict(zip(headers, line.split()[1:])))
+ return data
+
+ def wait_until_running(self, harts):
+ """Wait until the given harts are running."""
+ start = time.time()
+ while True:
+ targets = self.targets()
+ if all(targets[hart.id]["State"] == "running" for hart in harts):
+ return
+ if time.time() - start > self.timeout:
+ raise TestLibError("Timed out waiting for targets to run.")
+
class OpenocdCli:
def __init__(self, port=4444):
self.child = pexpect.spawn(