|
| 1 | +# pyOCD debugger |
| 2 | +# Copyright (c) 2025 Arm Limited |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | +# |
| 5 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | +# you may not use this file except in compliance with the License. |
| 7 | +# You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, software |
| 12 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +# See the License for the specific language governing permissions and |
| 15 | +# limitations under the License. |
| 16 | + |
| 17 | +import argparse |
| 18 | +from typing import List |
| 19 | +import logging |
| 20 | +import threading |
| 21 | +from time import sleep, time |
| 22 | + |
| 23 | +from pyocd.core import exceptions |
| 24 | + |
| 25 | +from .base import SubcommandBase |
| 26 | +from ..core.helpers import ConnectHelper |
| 27 | +from ..core.session import Session |
| 28 | +from ..utility.cmdline import convert_session_options |
| 29 | +from ..probe.shared_probe_proxy import SharedDebugProbeProxy |
| 30 | +from ..coresight.generic_mem_ap import GenericMemAPTarget |
| 31 | + |
| 32 | +from ..core.target import Target |
| 33 | +from ..debug import semihost |
| 34 | +from ..utility.timeout import Timeout |
| 35 | +from ..trace.swv import SWVReader |
| 36 | + |
| 37 | +from ..utility.stdio import StdioHandler |
| 38 | + |
| 39 | +LOG = logging.getLogger(__name__) |
| 40 | + |
| 41 | +class RunSubcommand(SubcommandBase): |
| 42 | + """@brief `pyocd run` subcommand.""" |
| 43 | + |
| 44 | + NAMES = ['run'] |
| 45 | + HELP = "Load and run the target" |
| 46 | + DEFAULT_LOG_LEVEL = logging.WARNING |
| 47 | + |
| 48 | + @classmethod |
| 49 | + def get_args(cls) -> List[argparse.ArgumentParser]: |
| 50 | + """@brief Add this subcommand to the subparsers object.""" |
| 51 | + run_parser = argparse.ArgumentParser(description=cls.HELP, add_help=False) |
| 52 | + |
| 53 | + run_options = run_parser.add_argument_group("run options") |
| 54 | + run_options.add_argument("--eot", dest="eot", action="store_true", default=False, |
| 55 | + help="Terminate execution when EOT character (0x04) is detected on stdout (default disabled).") |
| 56 | + run_options.add_argument("--timelimit", metavar="SECONDS", dest="timelimit", type=float, default=None, |
| 57 | + help="Maximum execution time in seconds before terminating (default no time limit).") |
| 58 | + |
| 59 | + return [cls.CommonOptions.COMMON, cls.CommonOptions.CONNECT, run_parser] |
| 60 | + |
| 61 | + def invoke(self) -> int: |
| 62 | + """@brief Handle 'run' subcommand.""" |
| 63 | + |
| 64 | + self._increase_logging(["pyocd.subcommands.run_cmd", __name__]) |
| 65 | + |
| 66 | + # Create shared shutdown event for all RunServer threads |
| 67 | + self.shared_shutdown = threading.Event() |
| 68 | + |
| 69 | + self._run_servers = [] |
| 70 | + |
| 71 | + try: |
| 72 | + # Create session |
| 73 | + session = ConnectHelper.session_with_chosen_probe( |
| 74 | + project_dir=self._args.project_dir, |
| 75 | + config_file=self._args.config, |
| 76 | + user_script=self._args.script, |
| 77 | + no_config=self._args.no_config, |
| 78 | + pack=self._args.pack, |
| 79 | + cbuild_run=self._args.cbuild_run, |
| 80 | + unique_id=self._args.unique_id, |
| 81 | + target_override=self._args.target_override, |
| 82 | + frequency=self._args.frequency, |
| 83 | + blocking=(not self._args.no_wait), |
| 84 | + connect_mode=self._args.connect_mode, |
| 85 | + options=convert_session_options(self._args.options), |
| 86 | + option_defaults=self._modified_option_defaults(), |
| 87 | + ) |
| 88 | + if session is None: |
| 89 | + LOG.error("No probe selected.") |
| 90 | + return 1 |
| 91 | + |
| 92 | + except Exception as e: |
| 93 | + LOG.error("Exception occurred while creating session: %s", e) |
| 94 | + return 1 |
| 95 | + |
| 96 | + timelimit_triggered = False |
| 97 | + with session: |
| 98 | + # |
| 99 | + # ToDo: load support |
| 100 | + # |
| 101 | + # To simulate state after load, stop all cores before starting run servers |
| 102 | + for _, core in session.board.target.cores.items(): |
| 103 | + core.halt() |
| 104 | + |
| 105 | + try: |
| 106 | + # Start up the run servers. |
| 107 | + for core_number, core in session.board.target.cores.items(): |
| 108 | + # Don't create a server for CPU-less memory Access Port. |
| 109 | + if isinstance(session.board.target.cores[core_number], GenericMemAPTarget): |
| 110 | + continue |
| 111 | + |
| 112 | + run_server = RunServer(session, core=core_number, enable_eot=self._args.eot, shutdown_event=self.shared_shutdown) |
| 113 | + self._run_servers.append(run_server) |
| 114 | + |
| 115 | + # Reset the target and start RunServers |
| 116 | + session.target.reset() |
| 117 | + for run_server in self._run_servers: |
| 118 | + run_server.start() |
| 119 | + |
| 120 | + # Wait for all servers to complete or timelimit to expire |
| 121 | + start_time = time() |
| 122 | + timelimit = self._args.timelimit |
| 123 | + while any(server.is_alive() for server in self._run_servers): |
| 124 | + # Check if timelimit has been exceeded |
| 125 | + if timelimit is not None: |
| 126 | + elapsed = time() - start_time |
| 127 | + if elapsed >= timelimit: |
| 128 | + LOG.info("Time limit of %.1f seconds reached. Shutting down Run-servers", timelimit) |
| 129 | + timelimit_triggered = True |
| 130 | + self.ShutDownRunServers() |
| 131 | + break |
| 132 | + sleep(0.1) |
| 133 | + |
| 134 | + except KeyboardInterrupt: |
| 135 | + LOG.info("KeyboardInterrupt received. Shutting down Run-servers") |
| 136 | + self.ShutDownRunServers() |
| 137 | + return 0 |
| 138 | + except Exception: |
| 139 | + LOG.exception("Unhandled exception in 'run' subcommand") |
| 140 | + self.ShutDownRunServers() |
| 141 | + return 1 |
| 142 | + |
| 143 | + if timelimit_triggered: |
| 144 | + return 0 |
| 145 | + if any(getattr(server, "eot_flag", False) for server in self._run_servers): |
| 146 | + return 0 |
| 147 | + if any(getattr(server, "error_flag", False) for server in self._run_servers): |
| 148 | + return 1 |
| 149 | + |
| 150 | + LOG.warning("Run servers exited without EOT, reached timelimit or error. This is unexpected.") |
| 151 | + return 1 |
| 152 | + |
| 153 | + def ShutDownRunServers(self): |
| 154 | + self.shared_shutdown.set() |
| 155 | + # Wait for servers to finish |
| 156 | + for server in self._run_servers: |
| 157 | + server.join(timeout=5.0) |
| 158 | + if server.is_alive(): |
| 159 | + LOG.warning("Run server for core %d did not terminate cleanly", server.core) |
| 160 | + |
| 161 | +class RunServer(threading.Thread): |
| 162 | + |
| 163 | + def __init__(self, session: Session, core=None, enable_eot: bool=False, shutdown_event: threading.Event=None): |
| 164 | + super().__init__(daemon=True) |
| 165 | + self.session = session |
| 166 | + self.error_flag = False |
| 167 | + self.eot_flag = False |
| 168 | + self.board = session.board |
| 169 | + if core is None: |
| 170 | + self.core = 0 |
| 171 | + self.target = self.board.target |
| 172 | + else: |
| 173 | + self.core = core |
| 174 | + self.target = self.board.target.cores[core] |
| 175 | + self.target_context = self.target.get_target_context() |
| 176 | + |
| 177 | + self.shutdown_event = shutdown_event or threading.Event() |
| 178 | + self.enable_eot = enable_eot |
| 179 | + |
| 180 | + self.name = "run-server-%d" % self.core |
| 181 | + |
| 182 | + # Semihosting always enabled |
| 183 | + self.enable_semihosting = True |
| 184 | + |
| 185 | + # Lock to synchronize SWO with other activity |
| 186 | + self.lock = threading.RLock() |
| 187 | + |
| 188 | + # Use internal IO handler. |
| 189 | + semihost_io_handler = semihost.InternalSemihostIOHandler() |
| 190 | + |
| 191 | + self._stdio_handler = StdioHandler(session=session, core=self.core, eot_enabled=self.enable_eot) |
| 192 | + semihost_console = semihost.ConsoleIOHandler(self._stdio_handler) |
| 193 | + self.semihost = semihost.SemihostAgent(self.target_context, io_handler=semihost_io_handler, console=semihost_console) |
| 194 | + |
| 195 | + |
| 196 | + # # Start with RTT disabled |
| 197 | + # self.rtt_server: Optional[RTTServer] = None |
| 198 | + |
| 199 | + # |
| 200 | + # If SWV is enabled, create a SWVReader thread. Note that we only do |
| 201 | + # this if the core is 0: SWV is not a per-core construct, and can't |
| 202 | + # be meaningfully read by multiple threads concurrently. |
| 203 | + # |
| 204 | + self._swv_reader = None |
| 205 | + if self._stdio_handler and session.options.get("enable_swv") and self.core == 0: |
| 206 | + if "swv_system_clock" not in session.options: |
| 207 | + LOG.warning("SWV not enabled; swv_system_clock option missing") |
| 208 | + else: |
| 209 | + sys_clock = int(session.options.get("swv_system_clock")) |
| 210 | + swo_clock = int(session.options.get("swv_clock")) |
| 211 | + self._swv_reader = SWVReader(session, self.core, self.lock) |
| 212 | + self._swv_reader.init(sys_clock, swo_clock, self._stdio_handler) |
| 213 | + |
| 214 | + def run(self): |
| 215 | + stdio_info = self._stdio_handler.info |
| 216 | + LOG.info("Run-server started for core %d. STDIO mode = %s", self.core, stdio_info) |
| 217 | + |
| 218 | + # Timeout used only if the target starts returning faults. The is_running property of this timeout |
| 219 | + # also serves as a flag that a fault occurred and we're attempting to retry. |
| 220 | + fault_retry_timeout = Timeout(self.session.options.get('debug.status_fault_retry_timeout')) |
| 221 | + |
| 222 | + while fault_retry_timeout.check(): |
| 223 | + if self.shutdown_event.is_set(): |
| 224 | + # Exit the thread |
| 225 | + LOG.debug("Exit Run-server for core %d", self.core) |
| 226 | + break |
| 227 | + |
| 228 | + # Check for EOT (0x04) |
| 229 | + if self._stdio_handler and self.enable_eot: |
| 230 | + try: |
| 231 | + if self._stdio_handler.eot_seen: |
| 232 | + # EOT received, terminate execution |
| 233 | + LOG.info("EOT (0x04) character received for core %d. Shutting down Run-servers", self.core) |
| 234 | + self.eot_flag = True |
| 235 | + self.shutdown_event.set() |
| 236 | + continue |
| 237 | + except Exception as e: |
| 238 | + LOG.debug("Error while waiting for EOT (0x04): %s", e) |
| 239 | + |
| 240 | + self.lock.acquire() |
| 241 | + |
| 242 | + try: |
| 243 | + state = self.target.get_state() |
| 244 | + |
| 245 | + # if self.rtt_server: |
| 246 | + # self.rtt_server.poll() |
| 247 | + |
| 248 | + # If we were able to successfully read the target state after previously receiving a fault, |
| 249 | + # then clear the timeout. |
| 250 | + if fault_retry_timeout.is_running: |
| 251 | + LOG.debug("Target control re-established.") |
| 252 | + fault_retry_timeout.clear() |
| 253 | + |
| 254 | + if state == Target.State.HALTED: |
| 255 | + # Handle semihosting |
| 256 | + if self.enable_semihosting: |
| 257 | + was_semihost = self.semihost.check_and_handle_semihost_request() |
| 258 | + if was_semihost: |
| 259 | + self.target.resume() |
| 260 | + continue |
| 261 | + |
| 262 | + pc = self.target_context.read_core_register('pc') |
| 263 | + LOG.error("Target unexpectedly halted at pc=0x%08x. Shutting down Run server for core %d.", pc, self.core) |
| 264 | + self.error_flag = True |
| 265 | + self.shutdown_event.set() |
| 266 | + break |
| 267 | + |
| 268 | + except exceptions.TransferError as e: |
| 269 | + # If we get any sort of transfer error or fault while checking target status, then start |
| 270 | + # a timeout running. Upon a later successful status check, the timeout is cleared. In the event |
| 271 | + # that the timeout expires, this loop is exited and an error raised. |
| 272 | + if not fault_retry_timeout.is_running: |
| 273 | + LOG.warning("Transfer error while checking target status; retrying: %s", e, |
| 274 | + exc_info=self.session.log_tracebacks) |
| 275 | + fault_retry_timeout.start() |
| 276 | + except exceptions.Error as e: |
| 277 | + LOG.error("Error while target running: %s. Exit Run server for core %d.", e, self.core, exc_info=self.session.log_tracebacks) |
| 278 | + self.error_flag = True |
| 279 | + self.shutdown_event.set() |
| 280 | + break |
| 281 | + finally: |
| 282 | + self.lock.release() |
| 283 | + sleep(0.01) |
| 284 | + |
| 285 | + # Check if we exited the above loop due to a timeout after a fault. |
| 286 | + if fault_retry_timeout.did_time_out: |
| 287 | + LOG.error("Timeout re-establishing target control. Exit Run server for core %d.", self.core) |
| 288 | + self.error_flag = True |
| 289 | + self.shutdown_event.set() |
| 290 | + |
| 291 | + # Cleanup resources for this RunServer. |
| 292 | + try: |
| 293 | + if self._swv_reader is not None: |
| 294 | + self._swv_reader.stop() |
| 295 | + except Exception as e: |
| 296 | + LOG.debug("Error stopping SWV reader for core %d: %s", self.core, e) |
| 297 | + |
| 298 | + try: |
| 299 | + if self._stdio_handler is not None: |
| 300 | + self._stdio_handler.shutdown() |
| 301 | + except Exception as e: |
| 302 | + LOG.debug("Error closing stdio handler for core %d: %s", self.core, e) |
0 commit comments