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):