Skip to content

Commit e1cb5be

Browse files
committed
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.
1 parent a29522f commit e1cb5be

File tree

1 file changed

+102
-39
lines changed

1 file changed

+102
-39
lines changed

debug/testlib.py

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ def __del__(self):
293293
pass
294294

295295
class Openocd:
296+
# pylint: disable=too-many-instance-attributes
296297
# pylint: disable-next=consider-using-with
297298
logfile = tempfile.NamedTemporaryFile(prefix='openocd', suffix='.log')
298299
logname = logfile.name
@@ -301,6 +302,7 @@ def __init__(self, server_cmd=None, config=None, debug=False, timeout=60,
301302
freertos=False, debug_openocd=False):
302303
self.timeout = timeout
303304
self.debug_openocd = debug_openocd
305+
self.command_count = 0
304306

305307
if server_cmd:
306308
cmd = shlex.split(server_cmd)
@@ -313,15 +315,13 @@ def __init__(self, server_cmd=None, config=None, debug=False, timeout=60,
313315
# line, since they are executed in order.
314316
cmd += [
315317
# Tell OpenOCD to bind gdb to an unused, ephemeral port.
316-
"--command",
317-
"gdb_port 0",
318-
# Disable tcl and telnet servers, since they are unused and because
319-
# the port numbers will conflict if multiple OpenOCD processes are
320-
# running on the same server.
321-
"--command",
322-
"tcl_port disabled",
323-
"--command",
324-
"telnet_port disabled",
318+
"--command", "gdb_port 0",
319+
# We don't use the TCL server.
320+
"--command", "tcl_port disabled",
321+
# Put the regular command prompt in stdin. Don't listen on a port
322+
# because it will conflict if multiple OpenOCD instances are running
323+
# at the same time.
324+
"--command", "telnet_port pipe",
325325
]
326326

327327
if config:
@@ -342,6 +342,9 @@ def __init__(self, server_cmd=None, config=None, debug=False, timeout=60,
342342

343343
# pylint: disable-next=consider-using-with
344344
raw_logfile = open(Openocd.logname, "wb")
345+
# pylint: disable-next=consider-using-with
346+
self.read_log_fd = open(Openocd.logname, "rb")
347+
self.log_buf = b""
345348
try:
346349
# pylint: disable-next=consider-using-with
347350
spike_dasm = subprocess.Popen("spike-dasm", stdin=subprocess.PIPE,
@@ -363,51 +366,34 @@ def __init__(self, server_cmd=None, config=None, debug=False, timeout=60,
363366
logfile.flush()
364367

365368
self.gdb_ports = []
366-
self.process = self.start(cmd, logfile, extra_env)
369+
self.start(cmd, logfile, extra_env)
367370

368371
def start(self, cmd, logfile, extra_env):
369372
combined_env = {**os.environ, **extra_env}
370373
# pylint: disable-next=consider-using-with
371-
process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
374+
self.process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
372375
stdout=logfile, stderr=logfile, env=combined_env)
373376

374377
try:
375378
# Wait for OpenOCD to have made it through riscv_examine(). When
376379
# using OpenOCD to communicate with a simulator this may take a
377380
# long time, and gdb will time out when trying to connect if we
378381
# attempt too early.
379-
start = time.time()
380-
messaged = False
381-
with open(Openocd.logname, "r", encoding='utf-8') as fd:
382-
while True:
383-
line = fd.readline()
384-
if not line:
385-
if not process.poll() is None:
386-
raise TestLibError("OpenOCD exited early.")
387-
time.sleep(0.1)
388-
continue
389-
390-
m = re.search(
391-
r"Listening on port (\d+) for gdb connections", line)
392-
if m:
393-
self.gdb_ports.append(int(m.group(1)))
394-
395-
if "telnet server disabled" in line:
396-
break
397-
398-
if not messaged and time.time() - start > 1:
399-
messaged = True
400-
print("Waiting for OpenOCD to start...")
401-
if (time.time() - start) > self.timeout:
402-
raise TestLibError("Timed out waiting for OpenOCD to "
403-
"listen for gdb")
382+
383+
while True:
384+
m = self.expect(
385+
rb"(Listening on port (\d+) for gdb connections|"
386+
rb"tcl server disabled)",
387+
message="Waiting for OpenOCD to start up...")
388+
if b"gdb" in m.group(1):
389+
self.gdb_ports.append(int(m.group(2)))
390+
else:
391+
break
404392

405393
if self.debug_openocd:
406394
# pylint: disable=consider-using-with
407395
self.debugger = subprocess.Popen(["gnome-terminal", "-e",
408-
f"gdb --pid={process.pid}"])
409-
return process
410-
396+
f"gdb --pid={self.process.pid}"])
411397
except Exception:
412398
print_log(Openocd.logname)
413399
raise
@@ -433,6 +419,83 @@ def smp(self):
433419
return True
434420
return False
435421

422+
def command(self, cmd):
423+
"""Write the command to OpenOCD's stdin. Return the output of the
424+
command, minus the prompt."""
425+
self.process.stdin.write(f"{cmd}\n".encode())
426+
self.process.stdin.flush()
427+
m = self.expect(re.escape(f"{cmd}\n".encode()))
428+
429+
# The prompt isn't flushed to the log, so send a unique command that
430+
# lets us find where output of the last command ends.
431+
magic = f"# {self.command_count}x".encode()
432+
self.command_count += 1
433+
self.process.stdin.write(magic + b"\n")
434+
self.process.stdin.flush()
435+
m = self.expect(rb"(.*)^> " + re.escape(magic))
436+
return m.group(1)
437+
438+
def expect(self, regex, message=None):
439+
"""Wait for the regex to match the log, and return the match object. If
440+
message is given, print it while waiting.
441+
We read the logfile to tell us what OpenOCD has done."""
442+
messaged = False
443+
start = time.time()
444+
445+
while True:
446+
for line in self.read_log_fd.readlines():
447+
line = line.rstrip()
448+
# Remove nulls, carriage returns, and newlines.
449+
line = re.sub(rb"[\x00\r\n]+", b"", line)
450+
# Remove debug messages.
451+
debug_match = re.search(rb"Debug: \d+ \d+ .*", line)
452+
if debug_match:
453+
line = line[:debug_match.start()] + line[debug_match.end():]
454+
self.log_buf += line
455+
else:
456+
self.log_buf += line + b"\n"
457+
458+
m = re.search(regex, self.log_buf, re.MULTILINE | re.DOTALL)
459+
if m:
460+
self.log_buf = self.log_buf[m.end():]
461+
return m
462+
463+
if not self.process.poll() is None:
464+
raise TestLibError("OpenOCD exited early.")
465+
466+
if message and not messaged and time.time() - start > 1:
467+
messaged = True
468+
print(message)
469+
470+
if (time.time() - start) > self.timeout:
471+
raise TestLibError(f"Timed out waiting for {regex} in "
472+
f"{Openocd.logname}")
473+
474+
time.sleep(0.1)
475+
476+
def targets(self):
477+
"""Run `targets` command."""
478+
result = self.command("targets").decode()
479+
# TargetName Type Endian TapName State
480+
# -- ------------------ ---------- ------ ------------------ --------
481+
# 0* riscv.cpu riscv little riscv.cpu halted
482+
lines = result.splitlines()
483+
headers = lines[0].split()
484+
data = []
485+
for line in lines[2:]:
486+
data.append(dict(zip(headers, line.split()[1:])))
487+
return data
488+
489+
def wait_until_running(self, harts):
490+
"""Wait until the given harts are running."""
491+
start = time.time()
492+
while True:
493+
targets = self.targets()
494+
if all(targets[hart.id]["State"] == "running" for hart in harts):
495+
return
496+
if time.time() - start > self.timeout:
497+
raise TestLibError("Timed out waiting for targets to run.")
498+
436499
class OpenocdCli:
437500
def __init__(self, port=4444):
438501
self.child = pexpect.spawn(

0 commit comments

Comments
 (0)