diff --git a/debug/gdbserver.py b/debug/gdbserver.py index ea3bb0c5e..97e49d890 100755 --- a/debug/gdbserver.py +++ b/debug/gdbserver.py @@ -19,7 +19,8 @@ from testlib import GdbTest, GdbSingleHartTest, TestFailed from testlib import TestNotApplicable, CompileError from testlib import UnknownThread -from testlib import CouldNotReadRegisters +from testlib import CouldNotReadRegisters, CommandException +from testlib import ThreadTerminated MSTATUS_UIE = 0x00000001 MSTATUS_SIE = 0x00000002 @@ -1807,22 +1808,29 @@ def test(self): output = self.gdb.c() assertIn("_exit", output) -class CeaseMultiTest(GdbTest): - """Test that we work correctly when a hart ceases to respond (e.g. because +class UnavailableMultiTest(GdbTest): + """Test that we work correctly when a hart becomes unavailable (e.g. because it's powered down).""" compile_args = ("programs/counting_loop.c", "-DDEFINE_MALLOC", "-DDEFINE_FREE") def early_applicable(self): - return self.hart.support_cease and len(self.target.harts) > 1 + return (self.hart.support_cease or + self.target.support_unavailable_control) \ + and len(self.target.harts) > 1 def setup(self): ProgramTest.setup(self) - self.parkOtherHarts("precease") + self.parkOtherHarts() def test(self): # Run all the way to the infinite loop in exit - self.gdb.c(wait=False) + self.gdb.c_all(wait=False) + # Other hart should have become unavailable. + if self.target.support_unavailable_control: + self.server.wait_until_running(self.target.harts) + self.server.command( + f"riscv dmi_write 0x1f 0x{(1< 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 @@ -434,6 +419,83 @@ def smp(self): 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( @@ -493,6 +555,9 @@ class UnknownThread(Exception): def __init__(self, explanation): Exception.__init__(self, explanation) +class ThreadTerminated(Exception): + pass + Thread = collections.namedtuple('Thread', ('id', 'description', 'target_id', 'name', 'frame')) @@ -590,6 +655,15 @@ def parse_rhs(text): raise TestLibError(f"Unexpected input: {tokens!r}") return result +class CommandException(Exception): + pass + +class CommandSendTimeout(CommandException): + pass + +class CommandCompleteTimeout(CommandException): + pass + class Gdb: """A single gdb class which can interact with one or more gdb instances.""" @@ -691,6 +765,8 @@ def select_hart(self, hart): output = self.command(f"thread {h['thread'].id}", ops=5) if "Unknown" in output: raise UnknownThread(output) + if f"Thread ID {h['thread'].id} has terminated" in output: + raise ThreadTerminated(output) def push_state(self): self.stack.append({ @@ -718,8 +794,14 @@ def command(self, command, ops=1, reset_delays=0): reset_delays=None) timeout = max(1, ops) * self.timeout self.active_child.sendline(command) - self.active_child.expect("\n", timeout=timeout) - self.active_child.expect(r"\(gdb\)", timeout=timeout) + try: + self.active_child.expect("\n", timeout=timeout) + except pexpect.exceptions.TIMEOUT as exc: + raise CommandSendTimeout(command) from exc + try: + self.active_child.expect(r"\(gdb\)", timeout=timeout) + except pexpect.exceptions.TIMEOUT as exc: + raise CommandCompleteTimeout(command) from exc output = self.active_child.before.decode("utf-8", errors="ignore") ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') return ansi_escape.sub('', output).strip() @@ -1136,7 +1218,9 @@ def __init__(self, target, hart=None): if hart: self.hart = hart else: + import random # pylint: disable=import-outside-toplevel self.hart = random.choice(target.harts) + #self.hart = target.harts[-1] self.server = None self.target_process = None self.binary = None @@ -1308,6 +1392,7 @@ def parkOtherHarts(self, symbol=None): self.gdb.p(f"$pc={symbol}") self.gdb.select_hart(self.hart) + self.gdb.command(f"monitor targets {self.hart.id}") def disable_pmp(self): # Disable physical memory protection by allowing U mode access to all