diff --git a/docs/options.md b/docs/options.md index e72063dd8..0fe802913 100644 --- a/docs/options.md +++ b/docs/options.md @@ -632,6 +632,15 @@ applies while the target is running after a resume operation in the debugger and it to halt again. +live.status_fault_retry_timeout +float +No default + +Timeout in seconds for retrying target status reads that fail with a transfer error while a server +monitors a core. A successful read clears the retry period. If the timeout expires, the server +instance for that core exits. If the option is omitted, retries continue indefinitely. + + gdbserver_port int 3333 diff --git a/pyocd/core/options.py b/pyocd/core/options.py index cff12df16..fb358977c 100644 --- a/pyocd/core/options.py +++ b/pyocd/core/options.py @@ -165,6 +165,10 @@ class OptionInfo(NamedTuple): "Duration in seconds that a failed target status check will be retried before an error is raised. " "Only applies while the target is running after a resume operation in the debugger and pyOCD is waiting " "for it to halt again."), + OptionInfo('live.status_fault_retry_timeout', float, None, + "Timeout in seconds for retrying target status reads that fail with a transfer error while a server " + "monitors a core. A successful read clears the retry period. If the timeout expires, the server " + "instance for that core exits. If the option is omitted, retries continue indefinitely."), OptionInfo('gdbserver_port', (int, tuple), 3333, "Base TCP port for the gdbserver."), OptionInfo('persist', bool, False, diff --git a/pyocd/core/soc_target.py b/pyocd/core/soc_target.py index 276fe6800..2ffd159e0 100644 --- a/pyocd/core/soc_target.py +++ b/pyocd/core/soc_target.py @@ -264,6 +264,9 @@ def write_core_registers_raw(self, reg_list: Sequence[CoreRegisterNameOrNumberTy def find_breakpoint(self, addr: int) -> Optional[Breakpoint]: return self.selected_core_or_raise.find_breakpoint(addr) + def skip_breakpoint_instruction(self, exclude_semihosting: bool = False) -> bool: + return self.selected_core_or_raise.skip_breakpoint_instruction(exclude_semihosting) + def set_breakpoint(self, addr: int, type: Target.BreakpointType = Target.BreakpointType.AUTO) -> bool: return self.selected_core_or_raise.set_breakpoint(addr, type) diff --git a/pyocd/core/target.py b/pyocd/core/target.py index 4f7570558..8c437f860 100644 --- a/pyocd/core/target.py +++ b/pyocd/core/target.py @@ -273,6 +273,9 @@ def write_core_registers_raw(self, reg_list: Sequence[CoreRegisterNameOrNumberTy def find_breakpoint(self, addr: int) -> Optional[Breakpoint]: raise NotImplementedError() + def skip_breakpoint_instruction(self, exclude_semihosting: bool = False) -> bool: + raise NotImplementedError() + def set_breakpoint(self, addr: int, type: BreakpointType = BreakpointType.AUTO) -> bool: raise NotImplementedError() diff --git a/pyocd/coresight/cortex_m.py b/pyocd/coresight/cortex_m.py index cfbbe8059..e0b0c382f 100644 --- a/pyocd/coresight/cortex_m.py +++ b/pyocd/coresight/cortex_m.py @@ -1206,6 +1206,27 @@ def resume(self) -> None: def find_breakpoint(self, addr: int) -> Optional[Breakpoint]: return self.bp_manager.find_breakpoint(addr) + def skip_breakpoint_instruction(self, exclude_semihosting: bool = False) -> bool: + """@brief Skip an BKPT instruction. + + @return True if a BKPT instruction was skipped, otherwise False. + """ + + pc = self.read_core_register('pc') + + instruction = self.read16(pc) + if (instruction & 0xff00) != 0xbe00: + return False + + if exclude_semihosting and instruction == 0xbeab: + return False + + self.write_core_register('pc', pc + 2) + LOG.debug("Skipped breakpoint at 0x%08x", pc) + self._run_token += 1 + + return True + def check_reg_list(self, reg_list: Sequence[CoreRegisterNameOrNumberType]) -> None: """@brief Sanity check register values and raise helpful errors.""" for reg in reg_list: @@ -1621,9 +1642,7 @@ def get_halt_reason(self) -> Optional[Target.HaltReason]: @return @ref pyocd.core.target.Target.HaltReason "Target.HaltReason" enumerator or None. """ dfsr = self.read32(CortexM.DFSR) - if dfsr & CortexM.DFSR_HALTED: - reason = Target.HaltReason.DEBUG - elif dfsr & CortexM.DFSR_BKPT: + if dfsr & CortexM.DFSR_BKPT: reason = Target.HaltReason.BREAKPOINT elif dfsr & CortexM.DFSR_DWTTRAP: reason = Target.HaltReason.WATCHPOINT @@ -1631,6 +1650,8 @@ def get_halt_reason(self) -> Optional[Target.HaltReason]: reason = Target.HaltReason.VECTOR_CATCH elif dfsr & CortexM.DFSR_EXTERNAL: reason = Target.HaltReason.EXTERNAL + elif dfsr & CortexM.DFSR_HALTED: + reason = Target.HaltReason.DEBUG else: reason = None return reason diff --git a/pyocd/coresight/cortex_m_v8m.py b/pyocd/coresight/cortex_m_v8m.py index 3ff4a5c95..c7e132694 100644 --- a/pyocd/coresight/cortex_m_v8m.py +++ b/pyocd/coresight/cortex_m_v8m.py @@ -264,9 +264,7 @@ def get_halt_reason(self): @return @ref pyocd.core.target.Target.HaltReason "Target.HaltReason" enumerator or None. """ dfsr = self.read32(self.DFSR) - if dfsr & self.DFSR_HALTED: - reason = Target.HaltReason.DEBUG - elif dfsr & self.DFSR_BKPT: + if dfsr & self.DFSR_BKPT: reason = Target.HaltReason.BREAKPOINT elif dfsr & self.DFSR_DWTTRAP: reason = Target.HaltReason.WATCHPOINT @@ -276,6 +274,8 @@ def get_halt_reason(self): reason = Target.HaltReason.EXTERNAL elif dfsr & self.DFSR_PMU: reason = Target.HaltReason.PMU + elif dfsr & self.DFSR_HALTED: + reason = Target.HaltReason.DEBUG else: reason = None return reason diff --git a/pyocd/coresight/generic_mem_ap.py b/pyocd/coresight/generic_mem_ap.py index bba26dd56..09ea5a676 100644 --- a/pyocd/coresight/generic_mem_ap.py +++ b/pyocd/coresight/generic_mem_ap.py @@ -115,6 +115,9 @@ def resume(self): def find_breakpoint(self, addr): return None + def skip_breakpoint_instruction(self, exclude_semihosting: bool = False) -> bool: + return False + def read_core_register(self, reg): raise exceptions.CoreRegisterAccessError("GenericMemAPTarget does not support core register access") diff --git a/pyocd/gdbserver/gdbserver.py b/pyocd/gdbserver/gdbserver.py index 937a6c2f7..7d3aa85cf 100644 --- a/pyocd/gdbserver/gdbserver.py +++ b/pyocd/gdbserver/gdbserver.py @@ -16,8 +16,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from errno import ENOTCONN import logging import threading +import time from time import sleep import io from xml.etree.ElementTree import (Element, SubElement, tostring) @@ -31,6 +33,8 @@ from ..utility.compatibility import (to_bytes_safe, to_str_safe) from ..utility.timeout import Timeout from ..utility.rtt_server import RTTServer +from ..utility.rtt_manager import RTTConfig, RTTManager +from ..utility.systemview import SystemViewConfig from ..utility.sockets import ConnectedSocket, ListenerSocket from ..utility.stdio import StdioHandler from .syscall import GDBSyscallIOHandler @@ -55,8 +59,8 @@ TRACE_MEM = LOG.getChild("trace.mem") TRACE_MEM.setLevel(logging.CRITICAL) -# When a client thread sets the active index, this filter will -# prepend "Client: " to log messages emitted on that thread. +# Prepend the explicit client index supplied by a log record, or fall back to +# the active index set by a client thread. class _ClientLogFilter(logging.Filter): def __init__(self): super().__init__() @@ -69,7 +73,9 @@ def clear_client(self) -> None: self._tls.client_index = None def filter(self, record: logging.LogRecord) -> bool: - idx = getattr(self._tls, 'client_index', None) + idx = getattr(record, 'client_index', None) + if idx is None: + idx = getattr(self._tls, 'client_index', None) if idx is not None: try: msg = record.getMessage() @@ -143,44 +149,26 @@ def __init__(self, server: 'GDBServer', connected_socket: 'ConnectedSocket', ind self.gdb_features = [] self.target_facade = GDBDebugContextFacade(server.target_context) self.shutdown_event = threading.Event() + # A non-stop stop reply is awaiting the client's vStopped acknowledgement. + self._awaiting_vstopped = False + self._cleanup_lock = threading.Lock() + self._did_cleanup = False + def start_packet_io(self) -> None: + """@brief Start packet processing before attaching this client to the target.""" + self._packet_io = GDBServerPacketIOThread(self._connected_socket, self.index) def run(self) -> None: # Set the log filter to include the client index in messages from this thread. _client_log_filter.set_client(self.index) LOG.debug("Thread started") - try: - self._packet_io = GDBServerPacketIOThread(self._connected_socket, self.index) - except Exception as e: - LOG.error("Error starting packet I/O thread: %s", e, exc_info=self._server.session.log_tracebacks) - return - - self.shutdown_event.clear() - self.is_attached_to_target = True - try: while not self.shutdown_event.is_set() and not self._server.shutdown_event.is_set(): try: - if self.is_interrupted(): - if self.non_stop: - self._server.target.halt() - self._server.is_target_running = False - self._server.trace_flush() - self._server.send_stop_notification(self) - else: - LOG.warning("Unexpected Ctrl-C ignored in all-stop mode") - self.interrupt_clear() - - if self.non_stop and self._server.is_target_running: - try: - if self._server.target.get_state() == Target.State.HALTED: - LOG.debug("Target halted") - self._server.is_target_running = False - self._server.trace_flush() - self._server.send_stop_notification(self) - except Exception as e: - LOG.error("Unexpected exception: %s", e, exc_info=self._server.session.log_tracebacks) + # Non-stop clients poll for asynchronous stop notifications, while + if self.non_stop: + self._server.service_non_stop_client(self) # read command try: @@ -193,17 +181,16 @@ def run(self) -> None: break if self.non_stop and packet is None: - sleep(0.1) + self.wait_for_interrupt(0.01) continue - if packet is not None and len(packet) != 0: + if packet: # decode and prepare resp resp = self._server.handle_message(self, packet) if resp is not None: # send resp self.send(resp) - except Exception as e: LOG.error("Unexpected exception: %s", e, exc_info=self._server.session.log_tracebacks) finally: @@ -225,12 +212,17 @@ def run(self) -> None: LOG.info("Client %d disconnected from port %d", self.index, self._server.port) # packet_io wrapper methods - def send(self, data): - return self._packet_io.send(data) + def send(self, data) -> None: + self._packet_io.send(data) def receive(self, block=True): return self._packet_io.receive(block) + @property + def is_connection_closed(self) -> bool: + """@brief Return whether the packet I/O thread detected a closed connection.""" + return self._packet_io is not None and self._packet_io._closed + def interrupt_clear(self): self._packet_io.interrupt_event.clear() @@ -252,30 +244,34 @@ def wait_for_interrupt(self, timeout=None): # Cleanup resources def cleanup(self): """ - Gracefully stop the packet I/O handler and close the connected socket. + Gracefully stop the packet I/O handler. """ - try: - if self._packet_io is not None: - self._packet_io.stop() - except Exception as e: - LOG.debug("Error stopping packet I/O thread: %s", e, exc_info=self._server.session.log_tracebacks) + # Cleanup can be requested by both the client thread and the server thread. + # Serialize these paths and release the connection resources only once. + with self._cleanup_lock: + if self._did_cleanup: + return + self._did_cleanup = True - try: - if self._connected_socket is not None: + try: self._connected_socket.close() - except Exception as e: - LOG.debug("Error closing socket: %s", e, exc_info=self._server.session.log_tracebacks) - finally: - self.is_socket_connected = False - + if self._packet_io is not None: + self._packet_io._closed = True + self._packet_io.stop() + except Exception as e: + LOG.debug("Error stopping packet I/O or closing socket: %s", e, + exc_info=self._server.session.log_tracebacks, + extra={'client_index': self.index}) + finally: + self.is_socket_connected = False def stop(self, timeout: float = 1.0) -> None: self.shutdown_event.set() - - # Only attempt to join if not in the same thread current_thread = threading.current_thread() if current_thread is not self: - self.join(timeout) + self.cleanup() + if self.is_alive(): + self.join(timeout) class GDBServer(threading.Thread): """@brief GDB remote server thread. @@ -290,7 +286,15 @@ class GDBServer(threading.Thread): ## Timer delay for sending the notification that the server is listening. START_LISTENING_NOTIFY_DELAY = 0.03 # 30 ms - def __init__(self, session, core=None): + ## Interval for retrying automatic RTT discovery, in seconds. + _RTT_DISCOVERY_INTERVAL = 0.010 + ## RTT channel polling interval used by the service thread, in seconds. + _RTT_POLL_INTERVAL = 0.001 + ## Target state polling interval used by the service thread, in seconds. + _TARGET_STATE_INTERVAL = 0.010 + + def __init__(self, session, core=None, target_running: Optional[bool] = None, + rtt_config: Optional[RTTConfig] = None, systemview_config: Optional[SystemViewConfig] = None): super().__init__(daemon=True) self.session = session self.board = session.board @@ -338,7 +342,10 @@ def __init__(self, session, core=None): ]) self.packet_size = 2048 - self.is_target_running = (self.target.get_state() == Target.State.RUNNING) + if target_running is None: + self._is_halted = self.target.get_state() == Target.State.HALTED + else: + self._is_halted = not target_running self.flash_loader = None self.shutdown_event = threading.Event() if core is None: @@ -361,6 +368,12 @@ def __init__(self, session, core=None): # Coarse grain lock to synchronize activity self.lock = threading.RLock() + self._active_run_client: Optional[GDBClientSession] = None + self._cleanup_lock = threading.RLock() + self._did_cleanup = False + self.rtt_server: Optional[RTTServer] = None + self._rtt_manager: Optional[RTTManager] = None + self.session.subscribe(self.event_handler, Target.Event.POST_RESET) # Init semihosting and stdio. @@ -376,8 +389,20 @@ def __init__(self, session, core=None): self.semihost = semihost.SemihostAgent(self.target_context, io_handler=semihost_io_handler, console=semihost_console) self._semihosting_client = None - # Start with RTT disabled - self.rtt_server: Optional[RTTServer] = None + if rtt_config is None: + try: + rtt_config = RTTConfig(_session=session, _target=self.target, _core=self.core) + except Exception as error: + LOG.error("RTT configuration failed for core %d: %s", self.core, error, exc_info=self.session.log_tracebacks) + + if rtt_config is not None and rtt_config.channels is not None: + try: + self._rtt_manager = RTTManager(session=session, core=self.core, rtt_config=rtt_config, + systemview_config=systemview_config or SystemViewConfig(_session=session)) + except Exception as error: + LOG.error("RTT configuration failed for core %d: %s", self.core, error, exc_info=self.session.log_tracebacks) + + self._service_thread = threading.Thread(target=self._run_service_thread, daemon=True, name="gdb-service-%d" % self.core,) self._init_remote_commands() @@ -422,15 +447,23 @@ def __init__(self, session, core=None): b'Z' : (self.breakpoint, 1 ), # Remove breakpoint/watchpoint. } + self._service_thread.start() + # pylint: enable=invalid-name def trace_flush(self) -> None: if self.board.target.trace_enabled: - self.board.target.trace_flush(self.target) + try: + self.board.target.trace_flush(self.target) + except Exception as error: + LOG.debug("Trace flush failed for core %d: %s", self.core, error, exc_info=self.session.log_tracebacks) def trace_capture(self) -> None: if self.board.target.trace_enabled: - self.board.target.trace_capture(self.target) + try: + self.board.target.trace_capture(self.target) + except Exception as error: + LOG.debug("Trace capture failed for core %d: %s", self.core, error, exc_info=self.session.log_tracebacks) def _init_remote_commands(self): """@brief Initialize the remote command processor infrastructure.""" @@ -445,40 +478,287 @@ def _init_remote_commands(self): # Add the gdbserver command group. self._command_context.command_set.add_command_group('gdbserver') + def _start_rtt(self) -> None: + """@brief Discover and configure RTT.""" + try: + rtt_server = self._rtt_manager.start_server() + if rtt_server is not None: + if self.shutdown_event.is_set(): + rtt_server.stop() + return + self.rtt_server = rtt_server + self._rtt_manager.configure_channels(stdio_handler=self.stdio_handler) + except Exception as error: + LOG.debug("RTT discovery failed for core %d: %s", self.core, error, exc_info=self.session.log_tracebacks) + + def _claim_active_run_client(self, client: GDBClientSession) -> bool: + """@brief Claim exclusive run ownership.""" + with self.lock: + if self._active_run_client is not None: + LOG.warning("Cannot start execution while client %d has an active run", self._active_run_client.index) + return False + self._active_run_client = client + return True + + def _release_active_run_client(self, client: GDBClientSession) -> None: + """@brief Release the client's run ownership.""" + with self.lock: + if self._active_run_client is client: + self._active_run_client = None + + def _handle_semihosting(self, client: Optional[GDBClientSession] = None) -> bool: + """@brief Check for and service a semihost request. Called with self.lock held.""" + use_gdb_client = self.semihost_use_syscalls and threading.current_thread() is client + if use_gdb_client: + self._semihosting_client = client + try: + return self.semihost.check_and_handle_semihost_request() + finally: + if use_gdb_client: + self._semihosting_client = None + + def _read_target_state(self) -> Target.State: + """@brief Read physical state without publishing an unprocessed halt.""" + with self.lock: + return self.target.get_state() + + def _read_and_process_target_state(self, client: Optional[GDBClientSession] = None) -> None: + """@brief Process the observed execution state and transparently resume semihosting. + Called with self.lock held. + """ + if not self.target.get_state() == Target.State.HALTED: + return + + # Handle semihosting could raise an exception, so we mark the target as halted before. + self._is_halted = True + if self.enable_semihosting and self._handle_semihosting(client=client): + self._is_halted = False + self.target.resume() + else: + self.trace_flush() + + def _halt_target(self) -> None: + """@brief Halt the target, assuming a successful request leaves it halted.""" + with self.lock: + # Preserve the cached halt state if target.halt() raises. + self.target.halt() + self._is_halted = True + self.trace_flush() + + def _request_stop(self, client: GDBClientSession) -> bool: + """@brief Halt and classify its halt reason. + + Called with self.lock held. + """ + self._halt_target() + halt_reason = self.target.get_halt_reason() + return halt_reason == Target.HaltReason.DEBUG + + def service_non_stop_client(self, client: GDBClientSession) -> None: + """@brief Handle a non-stop client's interrupt and pending stop notification.""" + with self.lock: + if self._active_run_client is not client: + # Client is not the active run client; drop the interrupt request. + client.interrupt_clear() + return + + force_signal = None + stop_reply = None + if client.is_interrupted(): + try: + if not self._is_halted: + halted_by_request = self._request_stop(client) + force_signal = signals.SIGINT if halted_by_request else None + except exceptions.TransferError as error: + LOG.error("Error halting target: %s", error, exc_info=self.session.log_tracebacks) + # The target may still be running; report SIGINT to complete GDB's Ctrl-C. + stop_reply = ('S%02x' % signals.SIGINT).encode() + finally: + client.interrupt_clear() + + if (self._is_halted or stop_reply is not None) and not client._awaiting_vstopped: + try: + self._send_stop_notification(client, forceSignal=force_signal, data=stop_reply) + except Exception as error: + LOG.error("Unexpected exception: %s", error, exc_info=self.session.log_tracebacks) + + def _resume_target(self) -> None: + """@brief Resume the target. Called with self.lock held.""" + try: + self.target.skip_breakpoint_instruction(exclude_semihosting=self.enable_semihosting) + except Exception: + return + self.trace_capture() + # Clear the cached halt first; resume may raise after the core starts. + self._is_halted = False + self.target.resume() + + def _run_service_thread(self) -> None: + """@brief Poll RTT, retry discovery, and read target state.""" + fault_retry_timeout = Timeout(self.session.options.get('live.status_fault_retry_timeout')) + + now = time.monotonic() + if self._rtt_manager is not None and self.rtt_server is None: + rtt_discovery_time = now + else: + rtt_discovery_time = float('inf') + rtt_poll_time = now + target_state_time = now + + while not self.shutdown_event.is_set(): + with self.lock: + now = time.monotonic() + + # Discover RTT control block + if now >= rtt_discovery_time: + rtt_discovery_time = now + self._RTT_DISCOVERY_INTERVAL + if self._rtt_manager is not None and self.rtt_server is None: + self._start_rtt() + if self.rtt_server is not None: + rtt_discovery_time = float('inf') + + # Poll RTT + if now >= rtt_poll_time: + if self.rtt_server is not None and self.rtt_server.running: + rtt_poll_time = now + self._RTT_POLL_INTERVAL + try: + self.rtt_server.poll() + except Exception as error: + LOG.debug("RTT poll failed for core %d: %s", self.core, error, exc_info=self.session.log_tracebacks) + else: + # RTT server might be started by a command; increase the poll interval for discovery. + rtt_poll_time = now + self._RTT_DISCOVERY_INTERVAL + + # Check target and handle target state + if now >= target_state_time: + target_state_time = now + self._TARGET_STATE_INTERVAL + client = self._active_run_client + + # Active All-Stop clients are polling target state in their run loops. + # The service thread is polling target state for active Non-Stop clients or when no client is active. + if not self._is_halted and (client is None or client.non_stop): + if fault_retry_timeout.did_time_out: + # Transfer error persisted beyond the retry timeout. + # Shut down the GDB server instance for this core. + LOG.error("Timeout expired after repeated transfer errors for core %d", self.core) + self.shutdown_event.set() + return + try: + self._read_and_process_target_state(client=client) + if fault_retry_timeout.is_running: + # Target state read succeeded after a transfer error; clear the fault retry timeout. + LOG.info("Recovered after transfer error for core %d.", self.core) + fault_retry_timeout.clear() + except exceptions.TransferError as error: + if not fault_retry_timeout.is_running: + # Start the timeout on the first fault; the next scheduled poll retries. + LOG.warning("Transfer error detected for core %d", self.core) + fault_retry_timeout.start() + except Exception as error: + LOG.error("Unexpected service thread error for core %d: %s", + self.core, error, exc_info=self.session.log_tracebacks) + else: + if fault_retry_timeout.is_running: + fault_retry_timeout.clear() + + wait_time = min(rtt_discovery_time, rtt_poll_time, target_state_time) - time.monotonic() + if wait_time > 0.0: + self.shutdown_event.wait(wait_time) + + def _stop_service_thread(self) -> None: + """@brief Stop the background service thread.""" + self.shutdown_event.set() + service_thread = self._service_thread + if service_thread is None or threading.current_thread() is service_thread: + return + + if service_thread.is_alive(): + service_thread.join(1.0) + if service_thread.is_alive(): + LOG.debug("Timed out waiting for service thread to stop for core %d", self.core) + return + + self._service_thread = None + def stop(self, wait=True): + self.shutdown_event.set() if self.is_alive(): - self.shutdown_event.set() if wait: LOG.debug("GDB server on port %d shutdown event; waiting for thread exit", self.port) self.join() else: LOG.debug("GDB server on port %d shutdown event", self.port) LOG.info("GDB server on port %d stopped", self.port) - + else: + self._cleanup() def _cleanup_client_sessions(self): # Stop and clean client sessions with self.client_sessions_lock: clients = list(self.client_sessions) for client in clients: - client.stop() - with self.client_sessions_lock: - if client in self.client_sessions: - self.client_sessions.remove(client) + try: + client.stop() + except Exception as e: + LOG.debug("Error stopping client %d: %s", client.index, e, exc_info=self.session.log_tracebacks) + + # stop() normally performs cleanup; call it again in case stop() failed before closing the connection. + try: + client.cleanup() + except Exception as e: + LOG.debug("Error cleaning up client %d: %s", client.index, e, exc_info=self.session.log_tracebacks) + finally: + with self.client_sessions_lock: + if client in self.client_sessions: + self.client_sessions.remove(client) def _cleanup(self): - LOG.debug("GDB server on port %d cleaning up", self.port) - self._cleanup_client_sessions() - if self.semihost: - self.semihost.cleanup() - self.semihost = None - if self.stdio_handler: - self.stdio_handler.shutdown() - self.stdio_handler = None - if self.rtt_server: - self.rtt_server.stop() - self.rtt_server = None - self.listen_socket.close() + self.shutdown_event.set() + # stop() and the server thread can both request cleanup, so release resources only once. + with self._cleanup_lock: + if self._did_cleanup: + return + self._did_cleanup = True + + LOG.debug("GDB server on port %d cleaning up", self.port) + try: + self._cleanup_client_sessions() + except Exception as e: + LOG.debug("Error cleaning up client sessions on port %d: %s", self.port, e, exc_info=self.session.log_tracebacks) + + try: + self._stop_service_thread() + except Exception as e: + LOG.debug("Error stopping service thread for core %d: %s", self.core, e, exc_info=self.session.log_tracebacks) + + if self.rtt_server is not None: + try: + self.rtt_server.stop() + except Exception as e: + LOG.debug("Error stopping RTT server for core %d: %s", self.core, e, exc_info=self.session.log_tracebacks) + finally: + self.rtt_server = None + + if self.semihost is not None: + try: + self.semihost.cleanup() + except Exception as e: + LOG.debug("Error cleaning up semihosting for core %d: %s", self.core, e, exc_info=self.session.log_tracebacks) + finally: + self.semihost = None + + if self.stdio_handler is not None: + try: + self.stdio_handler.shutdown() + except Exception as e: + LOG.debug("Error shutting down stdio for core %d: %s", self.core, e, exc_info=self.session.log_tracebacks) + finally: + self.stdio_handler = None + + try: + self.listen_socket.close() + except Exception as e: + LOG.debug("Error closing listener socket on port %d: %s", self.port, e, exc_info=self.session.log_tracebacks) def run(self): LOG.info("GDB server listening on port %d (core %d)", self.port, self.core) @@ -508,21 +788,28 @@ def run(self): continue client = None + resume_on_failure = False try: with self.client_sessions_lock: self.client_last_index += 1 index = self.client_last_index # Open client session client = GDBClientSession(self, connected_socket, index) - with self.client_sessions_lock: - self.client_sessions.append(client) + client.start_packet_io() + with self.lock: + if self.shutdown_event.is_set(): + raise RuntimeError("GDB server is shutting down") + + client.is_attached_to_target = True + with self.client_sessions_lock: + self.client_sessions.append(client) - # Make sure the target is halted. Otherwise gdb gets easily confused. - self.target.halt() - self.is_target_running = False - self.trace_flush() + # Make sure the target is halted. Otherwise gdb gets easily confused. + if not self._is_halted: + resume_on_failure = True + self._halt_target() - # Start the per-client handler thread (server.run_session() will be invoked there). + # Start the client command loop after target attachment. client.start() if remote_address: LOG.info("Client %d connected on port %d from remote address %s", index, self.port, remote_address) @@ -532,40 +819,45 @@ def run(self): except Exception as e: LOG.error("Error starting client session on port %d: %s", self.port, e, exc_info=self.session.log_tracebacks) - try: - if client is not None: - if client.is_alive(): - client.stop() - else: - client.cleanup() - - # Remove from session list if present + if client is not None: + try: + client.stop() + except Exception as cleanup_error: + LOG.error("Error stopping client session on port %d: %s", self.port, cleanup_error, exc_info=self.session.log_tracebacks) + try: + client.cleanup() + except Exception as cleanup_error: + LOG.error("Error cleaning up client session on port %d: %s", self.port, cleanup_error, exc_info=self.session.log_tracebacks) + with self.lock: + if resume_on_failure: + try: + self._resume_target() + except Exception as cleanup_error: + LOG.error("Error restoring target after client startup failure on port %d: %s", self.port, cleanup_error, exc_info=self.session.log_tracebacks) + client.is_attached_to_target = False with self.client_sessions_lock: if client in self.client_sessions: self.client_sessions.remove(client) - else: - # client not created -> close accepted socket - try: - connected_socket.close() - except Exception: - pass - - except Exception as e: - LOG.error("Error cleaning up client session on port %d: %s", self.port, e, exc_info=self.session.log_tracebacks) + else: + # Client construction failed, so only the accepted socket needs cleanup. + try: + connected_socket.close() + except Exception: + pass finally: LOG.debug("GDB server on port %d exiting", self.port) self._cleanup() def notify_client_detached(self, client: GDBClientSession): """ - Called when a client session detaches from target + Called when a client session detaches from target. """ - with self.client_sessions_lock: + with self.lock, self.client_sessions_lock: # Mark client detached from program + was_attached = client.is_attached_to_target client.is_attached_to_target = False - - if client is self._semihosting_client: - self._semihosting_client = None + client._awaiting_vstopped = False + self._release_active_run_client(client) # Client is detached from the target. If its socket connection is closed, remove it from the session list. if client in self.client_sessions and not client.is_socket_connected: @@ -573,26 +865,16 @@ def notify_client_detached(self, client: GDBClientSession): self.client_sessions.remove(client) # Resume target if no client is attached to program - if not any(c.is_attached_to_target for c in self.client_sessions): + if was_attached and not any(c.is_attached_to_target for c in self.client_sessions): self.thread_provider = None self.did_init_thread_providers = False self.first_run_after_reset_or_flash = True - # Resume target when no clients are connected + # Resume target when no clients are connected. try: - # First check if it's halted - if self.target.get_state() == Target.State.HALTED: - # If the target is halted, flush the trace capture buffer. - if self.is_target_running: - self.is_target_running = False - self.trace_flush() - # Start trace capture before resuming. - self.trace_capture() - self.target.resume() + self._resume_target() except Exception as e: - LOG.error("Error resuming target after client detached: %s", - e, exc_info=self.session.log_tracebacks) - self.is_target_running = (self.target.get_state() == Target.State.RUNNING) + LOG.error("Error resuming target after client detached: %s", e, exc_info=self.session.log_tracebacks) # Decide server lifecycle on connected sessions if not self.client_sessions and not self.persist: @@ -653,9 +935,14 @@ def kill(self, client): def restart(self, client, data): LOG.debug("Command: Restart") + try: client.is_attached_to_target = True + was_halted = self._is_halted self.target.reset_and_halt() + self._is_halted = True + if not was_halted: + self.trace_flush() except Exception as e: LOG.error("Command: Restart: Error resetting and halting target: %s", e, exc_info=self.session.log_tracebacks) # No reply for 'R' command. @@ -773,10 +1060,19 @@ def stop_reason_query(self, client): LOG.debug("Command: Stop reason query") # In non-stop mode, if no threads are stopped we need to reply with OK. - if client.non_stop and self.is_target_running: + if client.non_stop and not self._is_halted: + # This query completes any previous stop sequence. Preserve ownership + # of an ongoing run that has not reported a stop yet. + if client._awaiting_vstopped: + client._awaiting_vstopped = False + self._release_active_run_client(client) return self.create_rsp_packet(b"OK") - return self.create_rsp_packet(self.get_t_response(client)) + response = self.create_rsp_packet(self.get_t_response(client)) + if client.non_stop: + # The synchronous stop reply starts a sequence completed by vStopped. + client._awaiting_vstopped = True + return response def _get_resume_step_addr(self, data): data = data.split(b'#')[0] @@ -794,6 +1090,18 @@ def _get_resume_step_addr(self, data): return addr def resume(self, client, data): + if client.non_stop: + return self.create_rsp_packet(b'') + if not self._claim_active_run_client(client): + return self.create_rsp_packet(b'E01') + + try: + return self._resume(client, data) + finally: + self._release_active_run_client(client) + + def _resume(self, client, data): + """@brief Continue with run ownership claimed and self.lock held.""" if data and data[0:1] in (b'c', b'C'): addr = self._get_resume_step_addr(data) if addr: @@ -801,9 +1109,7 @@ def resume(self, client, data): else: LOG.debug("Command: Continue") - self.trace_capture() - self.target.resume() - self.is_target_running = True + self._resume_target() LOG.debug("Target resumed") if self.first_run_after_reset_or_flash: @@ -811,7 +1117,7 @@ def resume(self, client, data): if self.thread_provider is not None: self.thread_provider.read_from_target = True - val = b'' + rsp = b'' # Timeout used only if the target starts returning faults. The is_running property of this timeout # also serves as a flag that a fault occurred and we're attempting to retry. @@ -820,122 +1126,130 @@ def resume(self, client, data): while fault_retry_timeout.check(): if self.shutdown_event.is_set(): client.interrupt_clear() - return self.create_rsp_packet(val) + return self.create_rsp_packet(rsp) + # Yield between polls so other clients and RTT can use the target, and wake immediately on Ctrl-C. self.lock.release() + client.wait_for_interrupt(0.01) + self.lock.acquire() + + if client.shutdown_event.is_set() or client.is_connection_closed: + return None - # Wait for a ctrl-c to be received. - if client.wait_for_interrupt(0.01): - self.lock.acquire() + if client.is_interrupted(): LOG.debug("Ctrl-C received, halting target") - client.interrupt_clear() # Be careful about reading the target state. If we previously got a fault (the timeout # is running) then ignore the error. In all cases we still return SIGINT. try: - self.target.halt() - self.is_target_running = False - self.trace_flush() - val = self.get_t_response(client, forceSignal=signals.SIGINT) + halted_by_request = self._request_stop(client) + force_signal = signals.SIGINT if halted_by_request else None + rsp = self.get_t_response(client, forceSignal=force_signal) except exceptions.TransferError as e: # Note: if the target is not actually halted, gdb can get confused from this point on. # But there's not much we can do if we're getting faults attempting to control it. if not fault_retry_timeout.is_running: - LOG.error("Error reading target status after halt: %s", e, exc_info=self.session.log_tracebacks) - val = ('S%02x' % signals.SIGINT).encode() + LOG.error("Error halting target: %s", e, exc_info=self.session.log_tracebacks) + rsp = ('S%02x' % signals.SIGINT).encode() + finally: + client.interrupt_clear() break - self.lock.acquire() - try: - state = self.target.get_state() - - if self.rtt_server: - self.rtt_server.poll() - - # If we were able to successfully read the target state after previously receiving a fault, - # then clear the timeout. + if not self._is_halted: + self._read_and_process_target_state(client=client) + if client.shutdown_event.is_set() or client.is_connection_closed: + return None + if self._is_halted: + pc = self.target_context.read_core_register('pc') + LOG.debug("Target halted at pc=0x%08x", pc) + rsp = self.get_t_response(client) if fault_retry_timeout.is_running: LOG.info("Target control re-established.") fault_retry_timeout.clear() - - if state == Target.State.HALTED: - # Handle semihosting - if self.enable_semihosting and self._semihosting_client is None: - self._semihosting_client = client - self.lock.release() - try: - was_semihost = self.semihost.check_and_handle_semihost_request() - finally: - self.lock.acquire() - self._semihosting_client = None - - if was_semihost: - self.target.resume() - continue - - self.is_target_running = False - self.trace_flush() - pc = self.target_context.read_core_register('pc') - LOG.debug("Target halted at pc=0x%08x", pc) - val = self.get_t_response(client) + if self._is_halted: break except exceptions.TransferError as e: # If we get any sort of transfer error or fault while checking target status, then start - # a timeout running. Upon a later successful status check, the timeout is cleared. In the event + # a timeout running. Upon a later successful target access, the timeout is cleared. In the event # that the timeout expires, this loop is exited and an error raised to gdb. if not fault_retry_timeout.is_running: LOG.warning("Transfer error while checking target status; retrying: %s", e, exc_info=self.session.log_tracebacks) - fault_retry_timeout.start() + fault_retry_timeout.start() except exceptions.Error as e: - try: - self.target.halt() - except exceptions.Error: - pass + if not self._is_halted: + try: + self._halt_target() + except exceptions.Error: + pass LOG.warning("Error while target running: %s", e, exc_info=self.session.log_tracebacks) # This exception was not a transfer error, so reading the target state should be ok. - val = ('S%02x' % client.target_facade.get_signal_value()).encode() + rsp = ('S%02x' % client.target_facade.get_signal_value()).encode() break # Check if we exited the above loop due to a timeout after a fault. if fault_retry_timeout.did_time_out: LOG.error("Timeout re-establishing target control.") - val = ('S%02x' % signals.SIGSEGV).encode() + rsp = ('S%02x' % signals.SIGSEGV).encode() - return self.create_rsp_packet(val) + return self.create_rsp_packet(rsp) - def step(self, client, data, start=0, end=0): - if data and data[0:1] in (b's', b'S'): + def _execute_step(self, client: GDBClientSession, start=0, end=0) -> None: + """@brief Step until a visible halt or the end of a range. Called with self.lock held.""" + try: + if self.target.skip_breakpoint_instruction(exclude_semihosting=self.enable_semihosting): + if start == end or not (start <= self.target.read_core_register('pc') < end): + return + except Exception: + return + + self.trace_capture() + while not (client.is_connection_closed or client.shutdown_event.is_set() or client.is_interrupted()): + self.target.step(not self.step_into_interrupt, start, end, hook_cb=client.is_interrupted) + if not (self.enable_semihosting and self._handle_semihosting(client=client)): + break + pc = self.target.read_core_register('pc') + if start == end or not (start <= pc < end): + break + self.trace_flush() + + def step(self, client, data): + if not self._claim_active_run_client(client): + return self.create_rsp_packet(b'E01') + + try: addr = self._get_resume_step_addr(data) if addr: LOG.debug("Command: Step (addr=%d): Address is ignored", addr) else: LOG.debug("Command: Step") - # Use the step hook to check for an interrupt event. - def step_hook(): - # Note we don't clear the interrupt event here! - return client.is_interrupted() - self.trace_capture() - self.target.step(not self.step_into_interrupt, start, end, hook_cb=step_hook) - self.trace_flush() + self._execute_step(client) - # Clear and handle an interrupt. - if client.is_interrupted(): - LOG.debug("Ctrl-C received during step") - client.interrupt_clear() - response = self.get_t_response(client, forceSignal=signals.SIGINT) - else: - response = self.get_t_response(client) + if client.is_interrupted(): + LOG.debug("Ctrl-C received during step") + force_signal = signals.SIGINT + client.interrupt_clear() + else: + force_signal = None + return self.create_rsp_packet(self.get_t_response(client, forceSignal=force_signal)) + finally: + self._release_active_run_client(client) - return self.create_rsp_packet(response) + def _send_stop_notification(self, client, forceSignal=None, data=None) -> None: + """@brief Send a stop notification and mark its vStopped acknowledgement pending. - def send_stop_notification(self, client, forceSignal=None): - LOG.debug("Notification: Stop") - data = self.get_t_response(client, forceSignal=forceSignal) - packet = b'%Stop:' + data + b'#' + checksum(data) - client.send(packet) + The caller must verify run ownership and that no stop reply is pending. + """ + with self.lock: + LOG.debug("Notification: Stop") + if data is None: + data = self.get_t_response(client, forceSignal=forceSignal) + payload = b'Stop:' + data + packet = b'%' + payload + b'#' + checksum(payload) + client._awaiting_vstopped = True + client.send(packet) def v_command(self, client, data): cmd = data.split(b'#')[0] @@ -958,6 +1272,9 @@ def v_command(self, client, data): elif b'Stopped' in cmd: # Because we only support one thread for now, we can just reply OK to vStopped. LOG.debug("Command: vStopped notification") + if client._awaiting_vstopped: + client._awaiting_vstopped = False + self._release_active_run_client(client) return self.create_rsp_packet(b"OK") LOG.debug("Command: v%s: Unknown command", to_str_safe(cmd)) @@ -1001,45 +1318,95 @@ def v_cont(self, client, cmd): return self.create_rsp_packet(b'E01') thread_actions[currentThread] = default_action - if thread_actions[currentThread][0:1] in (b'c', b'C'): + action = thread_actions[currentThread] + if (client.non_stop and self._active_run_client is client and + action[0:1] in (b'c', b'C', b's', b'S', b'r')): + action_name = "continue" if action[0:1] in (b'c', b'C') else "step" + LOG.debug("Command: vCont (threadId=0x%08x, action=%s): Skipping; run already active for this client", + currentThread, action_name) + return self.create_rsp_packet(b"OK") + + if action[0:1] in (b'c', b'C'): LOG.debug("Command: vCont (threadId=0x%08x, action=continue)", currentThread) if client.non_stop: - self.trace_capture() - self.target.resume() - self.is_target_running = True + if not self._claim_active_run_client(client): + return self.create_rsp_packet(b'E01') + try: + self._resume_target() + except Exception: + self._release_active_run_client(client) + raise return self.create_rsp_packet(b"OK") else: return self.resume(client, None) - elif thread_actions[currentThread][0:1] in (b's', b'S', b'r'): + elif action[0:1] in (b's', b'S', b'r'): start = 0 end = 0 - if thread_actions[currentThread][0:1] == b'r': - start, end = [int(addr, base=16) for addr in thread_actions[currentThread][1:].split(b',')] + if action[0:1] == b'r': + start, end = [int(addr, base=16) for addr in action[1:].split(b',')] LOG.debug("Command: vCont (threadId=0x%08x, action=step, start=0x%08x, end=0x%08x)", currentThread, start, end) else: LOG.debug("Command: vCont (threadId=0x%08x, action=step)", currentThread) + if not self._claim_active_run_client(client): + return self.create_rsp_packet(b'E01') + release_run_client = True + try: + self._execute_step(client, start, end) + + if client.is_interrupted(): + force_signal = signals.SIGINT + client.interrupt_clear() + else: + force_signal = None + + if not client.non_stop: + return self.create_rsp_packet(self.get_t_response(client, forceSignal=force_signal)) - if client.non_stop: - self.trace_capture() - self.target.step(not self.step_into_interrupt, start, end) - self.trace_flush() client.send(self.create_rsp_packet(b"OK")) - self.send_stop_notification(client) + # Keep ownership until GDB acknowledges the stop with vStopped. + release_run_client = False + if not client._awaiting_vstopped: + try: + self._send_stop_notification(client, forceSignal=force_signal) + except Exception as error: + LOG.error("Error sending step stop notification: %s", error, exc_info=self.session.log_tracebacks) return None - else: - return self.step(client, None, start, end) - elif thread_actions[currentThread] == b't': + finally: + if release_run_client: + self._release_active_run_client(client) + elif action == b't': LOG.debug("Command: vCont (threadId=0x%08x, action=stop)", currentThread) # Must ignore t command in all-stop mode. if not client.non_stop: return self.create_rsp_packet(b"") + if self._is_halted or client._awaiting_vstopped: + return self.create_rsp_packet(b"OK") + if client != self._active_run_client: + return self.create_rsp_packet(b'E01') + + force_signal = None + stop_reply = None + try: + halted_by_request = self._request_stop(client) + force_signal = 0 if halted_by_request else None + except exceptions.TransferError as error: + LOG.error("Command: vCont (threadId=0x%08x, action=stop): Error stopping target: %s", currentThread, error, exc_info=self.session.log_tracebacks) + # The target may still be running; report signal zero to complete GDB's stop request. + stop_reply = b'S00' + except exceptions.Error as error: + LOG.error("Command: vCont (threadId=0x%08x, action=stop): Error stopping target: %s", currentThread, error, exc_info=self.session.log_tracebacks) + return self.create_rsp_packet(b'E01') + + # Acknowledge vCont;t first; report the stop separately with %Stop. client.send(self.create_rsp_packet(b"OK")) - self.target.halt() - self.is_target_running = False - self.trace_flush() - self.send_stop_notification(client, forceSignal=0) + try: + self._send_stop_notification(client, forceSignal=force_signal, data=stop_reply) + except Exception as error: + # The command was already acknowledged, so do not return a second response. + LOG.error("Error sending stop notification: %s", error, exc_info=self.session.log_tracebacks) + return None else: - LOG.error("Command: vCont (threadId=0x%08x, action='%s'): Unsupported action", currentThread, to_str_safe(thread_actions[currentThread])) + LOG.error("Command: vCont (threadId=0x%08x, action='%s'): Unsupported action", currentThread, to_str_safe(action)) def flash_op(self, data): ops = data.split(b':')[0] @@ -1179,7 +1546,7 @@ def set_registers(self, client, data): return self.create_rsp_packet(b"OK") def handle_query(self, client, msg): - query = msg.split(b':') + query = msg.split(b'#', 1)[0].split(b':') if query is None: LOG.error("Command: General query: Malformed packet received") @@ -1190,7 +1557,9 @@ def handle_query(self, client, msg): client.gdb_features = query[1].split(b';') # Build our list of features. - features = [b'qXfer:features:read+', b'QStartNoAckMode+', b'qXfer:threads:read+', b'QNonStop+'] + features = [b'qXfer:features:read+', b'QStartNoAckMode+', b'qXfer:threads:read+'] + if not self.semihost_use_syscalls: + features.append(b'QNonStop+') features.append(b'PacketSize=' + (hex(self.packet_size).encode())[2:]) if client.target_facade.get_memory_map_xml() is not None: features.append(b'qXfer:memory-map:read+') @@ -1367,6 +1736,9 @@ def handle_general_set(self, client, msg): elif feature.startswith(b'NonStop'): enable = feature.split(b':')[1] + if enable == b'1' and self.semihost_use_syscalls: + LOG.debug("Command: General set NonStop rejected because GDB File-I/O requires all-stop mode") + return self.create_rsp_packet(b"E01") client.non_stop = (enable == b'1') LOG.debug("Command: General set NonStop=%s", (enable == b'1')) return self.create_rsp_packet(b"OK") @@ -1428,9 +1800,22 @@ def create_rsp_packet(self, data): return resp def syscall(self, op: str) -> Tuple[int, int]: + """@brief Run GDB File-I/O with the server lock held by the caller.""" client = self._semihosting_client LOG.debug("Syscall request: %s", op) + if client is None: + LOG.debug("Skipping GDB syscall because no client is available: %s", op) + return -1, ENOTCONN + + self.lock.release() + try: + return self._exchange_syscall_packets(client, op) + finally: + self.lock.acquire() + + def _exchange_syscall_packets(self, client: GDBClientSession, op: str) -> Tuple[int, int]: + """@brief Exchange File-I/O packets without holding the server lock.""" request = self.create_rsp_packet(b'F' + op.encode()) client.send(request) @@ -1441,12 +1826,12 @@ def syscall(self, op: str) -> Tuple[int, int]: except ConnectionClosedException: LOG.error("Connection closed during syscall") client.is_socket_connected = False - break + return -1, ENOTCONN if packet is None: sleep(0.1) continue - # Check for file I/O response. + # Check for GDB syscall response. if packet[0:1] == b'$' and packet[1:2] == b'F': LOG.debug("Syscall response received: %r", packet) args = packet[2:packet.index(b'#')].split(b',') @@ -1470,6 +1855,8 @@ def syscall(self, op: str) -> Tuple[int, int]: LOG.warning("Detach received during syscall") break + if client.is_connection_closed or client.shutdown_event.is_set(): + return -1, ENOTCONN return -1, 0 def get_t_response(self, client, forceSignal=None): @@ -1517,7 +1904,7 @@ def is_threading_enabled(self): and (self.thread_provider.current_thread is not None) def is_target_in_reset(self): - return self.target.get_state() == Target.State.RESET + return self._read_target_state() == Target.State.RESET def exception_name(self): try: @@ -1530,6 +1917,8 @@ def event_handler(self, notification): if notification.event == Target.Event.POST_RESET: # Invalidate threads list if flash is reprogrammed. LOG.debug("POST_RESET event received") + with self.lock: + self._is_halted = False self.first_run_after_reset_or_flash = True if self.thread_provider is not None: self.thread_provider.read_from_target = False diff --git a/pyocd/gdbserver/syscall.py b/pyocd/gdbserver/syscall.py index 649602947..4dafc3346 100644 --- a/pyocd/gdbserver/syscall.py +++ b/pyocd/gdbserver/syscall.py @@ -78,14 +78,14 @@ def close(self, fd): def write(self, fd, ptr, length): fd -= FD_OFFSET result, self._errno = self._server.syscall('write,%x,%x,%x' % (fd, ptr, length)) - return length - result + return length if result < 0 else length - result # syscall return: number of bytes read # semihost return: 0 is success, length is EOF, number of bytes not read def read(self, fd, ptr, length): fd -= FD_OFFSET result, self._errno = self._server.syscall('read,%x,%x,%x' % (fd, ptr, length)) - return length - result + return length if result < 0 else length - result def readc(self): assert self.agent diff --git a/pyocd/subcommands/gdbserver_cmd.py b/pyocd/subcommands/gdbserver_cmd.py index 33c6e88e0..b324caa63 100644 --- a/pyocd/subcommands/gdbserver_cmd.py +++ b/pyocd/subcommands/gdbserver_cmd.py @@ -25,16 +25,15 @@ from .base import SubcommandBase from ..core.helpers import ConnectHelper from ..core.session import Session -from ..utility.cmdline import ( - convert_session_options, - split_command_line, - ) +from ..utility.cmdline import (convert_session_options, split_command_line) from ..probe.shared_probe_proxy import SharedDebugProbeProxy from ..gdbserver import GDBServer from ..trace.swv import SWVReader from ..probe.tcp_probe_server import DebugProbeServer from ..coresight.generic_mem_ap import GenericMemAPTarget from ..utility.notification import Notification +from ..utility.rtt_manager import RTTConfig +from ..utility.systemview import SystemViewConfig, SystemViewSVDat LOG = logging.getLogger(__name__) @@ -138,35 +137,37 @@ def invoke(self) -> int: probe_server = None swv_reader = None + systemview = None gdbs = [] - try: - # Build dict of session options. - sessionOptions = convert_session_options(self._args.options) - modifiable_options = { - 'gdbserver_port' : self._args.port_number, - 'telnet_port' : self._args.telnet_port, - 'persist' : self._args.persist, - 'step_into_interrupt' : self._args.step_into_interrupt, - 'chip_erase': self._args.erase, - 'fast_program' : self._args.trust_crc, - 'enable_semihosting' : self._args.enable_semihosting, - 'serve_local_only' : self._args.serve_local_only, - 'vector_catch' : self._args.vector_catch, - 'soft_bkpt_as_hard' : self._args.soft_bkpt_as_hard, - } - modified_options = {k: v for k, v in modifiable_options.items() if v is not None} - sessionOptions.update(modified_options) - - # Split list of cores to serve. - if self._args.core is not None: - try: - core_list = {int(x) for x in self._args.core.split(',')} - except ValueError as err: - LOG.error("Invalid value passed to --core") - return 1 - else: - core_list = None + # Build dict of session options. + sessionOptions = convert_session_options(self._args.options) + modifiable_options = { + 'gdbserver_port' : self._args.port_number, + 'telnet_port' : self._args.telnet_port, + 'persist' : self._args.persist, + 'step_into_interrupt' : self._args.step_into_interrupt, + 'chip_erase': self._args.erase, + 'fast_program' : self._args.trust_crc, + 'enable_semihosting' : self._args.enable_semihosting, + 'serve_local_only' : self._args.serve_local_only, + 'vector_catch' : self._args.vector_catch, + 'soft_bkpt_as_hard' : self._args.soft_bkpt_as_hard, + } + modified_options = {k: v for k, v in modifiable_options.items() if v is not None} + sessionOptions.update(modified_options) + + # Split list of cores to serve. + if self._args.core is not None: + try: + core_list = {int(x) for x in self._args.core.split(',')} + except ValueError as err: + LOG.error("Invalid value passed to --core") + return 1 + else: + core_list = None + + try: # Get the probe. probe = ConnectHelper.choose_probe( blocking=(not self._args.no_wait), @@ -198,22 +199,50 @@ def invoke(self) -> int: if session is None: LOG.error("No probe selected.") return 1 - with session: - # Validate the core selection. - all_cores = set(session.target.cores.keys()) - if core_list is None: - core_list = all_cores - bad_cores = core_list.difference(all_cores) - if len(bad_cores): - LOG.error("Invalid core number%s: %s", - "s" if len(bad_cores) > 1 else "", - ", ".join(str(x) for x in bad_cores)) - return 1 - - # Set ELF if provided. - if self._args.elf: - session.board.target.elf = os.path.expanduser(self._args.elf) + except Exception as e: + LOG.error("Exception occurred while creating session: %s", e) + return 1 + + with session: + # Validate the core selection. + all_cores = set(session.target.cores.keys()) + if core_list is None: + core_list = all_cores + bad_cores = core_list.difference(all_cores) + if len(bad_cores): + LOG.error("Invalid core number%s: %s", + "s" if len(bad_cores) > 1 else "", + ", ".join(str(x) for x in bad_cores)) + return 1 + + # Set ELF if provided. + if self._args.elf: + session.board.target.elf = os.path.expanduser(self._args.elf) + + try: + # Process RTT configuration + rtt_config_list = { + core_number: RTTConfig(_session=session, _target=core, _core=core_number) + for core_number, core in session.board.target.cores.items() + if core_number in core_list and not isinstance(core, GenericMemAPTarget) + } + except Exception as e: + LOG.warning("Exception occurred while processing RTT configuration: %s", e) + + try: + # Process SystemView configuration + systemview_config = SystemViewConfig(_session=session) + if any(cfg.has_rtt_config and cfg.num_systemview_channels > 0 for cfg in rtt_config_list.values()): + systemview = SystemViewSVDat(session=session, rtt_configs=rtt_config_list, + systemview_config=systemview_config) + except Exception as e: + LOG.warning("Exception occurred while processing SystemView configuration: %s", e) + + # Check whether we are using the attach mode + attached = session.options.get("connect_mode") == 'attach' + + try: # Run the probe server is requested. if self._args.enable_probe_server: probe_server = DebugProbeServer(session, session.probe, @@ -221,6 +250,11 @@ def invoke(self) -> int: session.probeserver = probe_server probe_server.start() + # Halt all cores if required + if attached and (self._args.reset_run or session.board.target.trace_enabled): + for core in session.board.target.cores.values(): + core.halt() + # Initialize SWV reader before any GDB activity. if session.options.get("enable_swv"): if "swv_system_clock" not in session.options: @@ -231,9 +265,25 @@ def invoke(self) -> int: swv_reader = SWVReader(session) swv_reader.init(sys_clock, swo_clock, sys.stdout) - # Reset and run the target + # Reset the cores if requested before starting the GDB servers. if self._args.reset_run: + for core in session.board.target.cores.values(): + core.set_reset_catch() session.board.target.reset() + for core in session.board.target.cores.values(): + core.clear_reset_catch() + + # Start trace capture before running the target + if session.board.target.trace_enabled: + session.board.target.trace_capture() + + # Resume target if it was previously attached or if a reset_run was requested. + if attached and (self._args.reset_run or session.board.target.trace_enabled): + for core in session.board.target.cores.values(): + core.resume() + + # Determine if the target is running based on attach mode or reset_run argument + target_running = attached or self._args.reset_run # Start up the gdbservers. for core_number, core in session.board.target.cores.items(): @@ -243,7 +293,11 @@ def invoke(self) -> int: # Don't create a server if this core is not listed by the user. if core_number not in core_list: continue - gdb = GDBServer(session, core=core_number) + gdb = GDBServer(session=session, + core=core_number, + target_running=target_running, + rtt_config=rtt_config_list[core_number], + systemview_config=systemview_config) # Only subscribe to the server for the first core, so echo messages aren't printed # multiple times. if not gdbs: @@ -254,17 +308,25 @@ def invoke(self) -> int: while any(g.is_alive() for g in gdbs): sleep(0.1) + + except KeyboardInterrupt: + LOG.info("KeyboardInterrupt received; shutting down GDB servers") + for server in gdbs: + server.stop() + return 0 + except Exception: + LOG.exception("Unhandled exception in 'gdbserver' subcommand") + for server in gdbs: + server.stop() + raise + finally: if probe_server: probe_server.stop() if swv_reader: swv_reader.stop() - except (KeyboardInterrupt, Exception): - for server in gdbs: - server.stop() - if swv_reader: - swv_reader.stop() - if probe_server: - probe_server.stop() - raise + if session.board.target.trace_enabled: + session.board.target.trace_flush() + if systemview: + systemview.assemble_file() return 0 diff --git a/pyocd/subcommands/run_cmd.py b/pyocd/subcommands/run_cmd.py index 589caa825..b7d26125c 100644 --- a/pyocd/subcommands/run_cmd.py +++ b/pyocd/subcommands/run_cmd.py @@ -63,6 +63,9 @@ def get_args(cls) -> List[argparse.ArgumentParser]: def invoke(self) -> int: """@brief Handle 'run' subcommand.""" + swv_reader = None + systemview = None + # Create shared shutdown event for all RunServer threads self.shared_shutdown = threading.Event() @@ -93,38 +96,52 @@ def invoke(self) -> int: LOG.error("Exception occurred while creating session: %s", e) return 1 - timelimit_triggered = False with session: - # Increase log level to INFO if it is still at the default WARNING level root_logger = logging.getLogger() if root_logger.level == logging.WARNING: root_logger.setLevel(logging.INFO) - # - # ToDo: load support - # - # To simulate state after load, stop all cores before starting run servers - for _, core in session.board.target.cores.items(): - core.halt() - - rtt_config_list = { - core_number: RTTConfig(_session=session, _target=core_target, _core=core_number) - for core_number, core_target in session.board.target.cores.items() - } - - systemview_config = SystemViewConfig(_session=session) - systemview_enabled = any( - cfg.has_rtt_config and cfg.num_systemview_channels > 0 - for cfg in rtt_config_list.values() - ) - if systemview_enabled: - self._systemview = SystemViewSVDat(session=session, rtt_configs=rtt_config_list, systemview_config=systemview_config) - else: - self._systemview = None + # TODO: load support - swv_reader = None try: + # Process RTT configuration + rtt_config_list = { + core_number: RTTConfig(_session=session, _target=core_target, _core=core_number) + for core_number, core_target in session.board.target.cores.items() + if not isinstance(core_target, GenericMemAPTarget) + } + except Exception as e: + LOG.warning("Exception occurred while processing RTT configuration: %s", e) + + try: + # Process SystemView configuration + systemview_config = SystemViewConfig(_session=session) + if any(cfg.has_rtt_config and cfg.num_systemview_channels > 0 for cfg in rtt_config_list.values()): + systemview = SystemViewSVDat(session=session, rtt_configs=rtt_config_list, + systemview_config=systemview_config) + except Exception as e: + LOG.warning("Exception occurred while processing SystemView configuration: %s", e) + + # Check whether we are using the attach mode + attached = session.options.get("connect_mode") == 'attach' + + try: + # Halt all cores if required + if attached: + for core in session.board.target.cores.values(): + core.halt() + + # Initialize SWV reader + if session.options.get("enable_swv"): + if "swv_system_clock" not in session.options: + LOG.warning("SWV not enabled; swv_system_clock option missing") + else: + sys_clock = int(session.options.get("swv_system_clock")) + swo_clock = int(session.options.get("swv_clock")) + swv_reader = SWVReader(session) + swv_reader.init(sys_clock, swo_clock, sys.stdout) + # Start up the run servers for core_number, core in session.board.target.cores.items(): # Don't create a server for CPU-less memory Access Port @@ -139,35 +156,35 @@ def invoke(self) -> int: shutdown_event=self.shared_shutdown) self._run_servers.append(run_server) - # Initialize SWVReader - if session.options.get("enable_swv"): - if "swv_system_clock" not in session.options: - LOG.warning("SWV not enabled; swv_system_clock option missing") - else: - sys_clock = int(session.options.get("swv_system_clock")) - swo_clock = int(session.options.get("swv_clock")) - swv_reader = SWVReader(session) - swv_reader.init(sys_clock, swo_clock, sys.stdout) - - # Reset the target and start RunServers - session.target.reset() - for run_server in self._run_servers: - run_server.start() + # Reset the cores before starting the Run servers. + for core in session.board.target.cores.values(): + core.set_reset_catch() + session.board.target.reset() + for core in session.board.target.cores.values(): + core.clear_reset_catch() - # Trace Capture + # Start trace capture before running the target if session.board.target.trace_enabled: session.board.target.trace_capture() + # Resume target + for core in session.board.target.cores.values(): + core.resume() + + # Start the Run servers + for run_server in self._run_servers: + run_server.start() + # Wait for all servers to complete or timelimit to expire start_time = time() timelimit = self._args.timelimit + while any(server.is_alive() for server in self._run_servers): # Check if timelimit has been exceeded if timelimit is not None: elapsed = time() - start_time if elapsed >= timelimit: LOG.info("Time limit of %.1f seconds reached; shutting down all Run servers", timelimit) - timelimit_triggered = True self.shutdown() break sleep(0.1) @@ -179,20 +196,16 @@ def invoke(self) -> int: except Exception: LOG.exception("Unhandled exception in 'run' subcommand") self.shutdown() - return 1 + raise finally: - if session.board.target.trace_enabled: - session.board.target.trace_flush() if swv_reader: swv_reader.stop() + if session.board.target.trace_enabled: + session.board.target.trace_flush() + if systemview: + systemview.assemble_file() - if timelimit_triggered: - return 0 - if any(getattr(server, "eot_flag", False) for server in self._run_servers): - return 0 - - LOG.warning("Run servers exited without EOT, reached timelimit or error; this is unexpected") - return 1 + return 0 def shutdown(self): self.shared_shutdown.set() @@ -203,14 +216,12 @@ def shutdown(self): if server.is_alive(): LOG.warning("Run server for core %d did not terminate cleanly", server.core) - # Generate SystemView output file - if self._systemview is not None: - self._systemview.assemble_file() class RunServer(threading.Thread): def __init__(self, session: Session, core: Optional[int] = None, rtt_config: "RTTConfig" = None, - systemview_config: "SystemViewConfig" = None, enable_eot: bool = False, shutdown_event: Optional[threading.Event] = None): + systemview_config: "SystemViewConfig" = None, enable_eot: bool = False, + shutdown_event: Optional[threading.Event] = None): super().__init__(daemon=True) self._session = session self.eot_flag = False @@ -327,7 +338,8 @@ def run(self): exc_info=self._session.log_tracebacks) fault_retry_timeout.start() except exceptions.Error as e: - LOG.error("Error while target core %d running: %s; exiting Run server for core %d", self.core, e, self.core, exc_info=self._session.log_tracebacks) + LOG.error("Error while target core %d running: %s; exiting Run server for core %d", + self.core, e, self.core, exc_info=self._session.log_tracebacks) break finally: sleep(0.001) diff --git a/pyocd/utility/rtt_manager.py b/pyocd/utility/rtt_manager.py index c91434dff..42c64163a 100644 --- a/pyocd/utility/rtt_manager.py +++ b/pyocd/utility/rtt_manager.py @@ -186,6 +186,7 @@ def __init__(self, session: Session, core: Optional[int] = None, rtt_config: RTT else: self._core = core self._target = self._board.target.cores[core] + self._warning_logged = False # RTT configuration self._rtt_config = rtt_config @@ -195,6 +196,14 @@ def __init__(self, session: Session, core: Optional[int] = None, rtt_config: RTT self._rtt_server: Optional[RTTServer] = None + def _log_warning_once(self, message: str, *args) -> None: + """@brief Log only the first warning passed to this helper.""" + if self._warning_logged: + return + + LOG.warning(message, *args) + self._warning_logged = True + def _start_rtt_server(self, address: Optional[int], size: Optional[int]) -> Optional[RTTServer]: """@brief Create and start RTT server with the given address and size. If address is None, auto-detect will be attempted.""" @@ -229,13 +238,13 @@ def _find_segger_rtt_symbol(self) -> Optional[int]: return symbol_info.address return None except Exception as e: - LOG.warning("RTT for core %d: failed to get _SEGGER_RTT symbol address from ELF: %s", self._core, e) + self._log_warning_once("RTT for core %d: failed to get _SEGGER_RTT symbol address from ELF: %s", self._core, e) return None def start_server(self) -> Optional[RTTServer]: """@brief Create and start RTT server.""" - if not self._rtt_config.has_rtt_config: + if self._rtt_config is None or not self._rtt_config.has_rtt_config: LOG.debug("RTT for core %d: no RTT configuration; RTT disabled", self._core) return None @@ -253,7 +262,7 @@ def start_server(self) -> Optional[RTTServer]: if rtt_cb is not None: address, size, auto_detect = rtt_cb if address is None and auto_detect is False: - LOG.warning("RTT for core %d: control block configuration is missing address while auto-detect is disabled; RTT disabled", self._core) + self._log_warning_once("RTT for core %d: control block configuration is missing address while auto-detect is disabled; RTT disabled", self._core) return None if address is not None: self._rtt_server = self._start_rtt_server(address, size) @@ -265,9 +274,9 @@ def start_server(self) -> Optional[RTTServer]: return self._rtt_server else: if size: - LOG.warning("RTT for core %d: failed to find RTT control block with specified address 0x%X and size 0x%X", self._core, address, size) + self._log_warning_once("RTT for core %d: failed to find RTT control block with specified address 0x%X and size 0x%X", self._core, address, size) else: - LOG.warning("RTT for core %d: failed to find RTT control block with specified address 0x%X", self._core, address) + self._log_warning_once("RTT for core %d: failed to find RTT control block with specified address 0x%X", self._core, address) if auto_detect: # Fallback: auto-detect via memory scan in default memory region if no address specified self._rtt_server = self._start_rtt_server(None, None) @@ -275,20 +284,20 @@ def start_server(self) -> Optional[RTTServer]: LOG.debug("RTT for core %d: RTT control block found via auto-detect memory scan in default memory region", self._core) return self._rtt_server else: - LOG.warning("RTT for core %d: failed to find RTT control block with auto-detected address", self._core) + self._log_warning_once("RTT for core %d: failed to find RTT control block with auto-detected address", self._core) return None else: # Auto-detect via symbol "_SEGGER_RTT" lookup in the ELF file address = self._find_segger_rtt_symbol() if address is None: - LOG.warning("RTT for core %d: failed to find _SEGGER_RTT symbol in ELF; cannot auto-detect RTT control block", self._core) + self._log_warning_once("RTT for core %d: failed to find _SEGGER_RTT symbol in ELF; cannot auto-detect RTT control block", self._core) return None self._rtt_server = self._start_rtt_server(address, None) if self._rtt_server is not None: LOG.debug("RTT for core %d: RTT control block found via _SEGGER_RTT symbol lookup at address 0x%X", self._core, address) return self._rtt_server - LOG.warning("RTT for core %d: failed to find RTT control block with _SEGGER_RTT symbol address 0x%X", self._core, address) + self._log_warning_once("RTT for core %d: failed to find RTT control block with _SEGGER_RTT symbol address 0x%X", self._core, address) return None def configure_channels(self, stdio_handler: Optional[StdioHandler] = None):