Skip to content

Commit 3778cb9

Browse files
authored
run_cmd: add run subcommand (#1876)
- Add initial RunSubcommand to run targets until timelimit or EOT. - Create per-core RunServer threads with shared shutdown event. - Integrate semihosting console and SWV output with StdioHandler.
1 parent c8395b2 commit 3778cb9

File tree

2 files changed

+304
-0
lines changed

2 files changed

+304
-0
lines changed

pyocd/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from .subcommands.reset_cmd import ResetSubcommand
4444
from .subcommands.server_cmd import ServerSubcommand
4545
from .subcommands.rtt_cmd import RTTSubcommand
46+
from .subcommands.run_cmd import RunSubcommand
4647

4748
## @brief Logger for this module.
4849
LOG = logging.getLogger("pyocd.tool")
@@ -65,6 +66,7 @@ class PyOCDTool(SubcommandBase):
6566
ResetSubcommand,
6667
ServerSubcommand,
6768
RTTSubcommand,
69+
RunSubcommand,
6870
]
6971

7072
## @brief Logging level names.

pyocd/subcommands/run_cmd.py

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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

Comments
 (0)