@@ -293,6 +293,7 @@ def __del__(self):
293293 pass
294294
295295class 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+
436499class OpenocdCli :
437500 def __init__ (self , port = 4444 ):
438501 self .child = pexpect .spawn (
0 commit comments