From cf2b6872da79e23cf8346837aaf0f830a137777e Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Fri, 10 Jul 2026 14:56:42 +0200 Subject: [PATCH 1/3] options: replace telnet with stdio session options - keep legacy telnet_* session option names as aliases - accept the stdio cbuild-run debugger node and prefer it over legacy telnet --- docs/command_reference.md | 2 +- docs/options.md | 14 ++--- docs/semihosting.md | 6 +- pyocd/core/options.py | 24 +++++--- pyocd/core/options_manager.py | 11 ++-- pyocd/core/session.py | 10 ++-- pyocd/target/pack/cbuild_run.py | 70 ++++++++++++---------- pyocd/utility/cmdline.py | 3 +- pyocd/utility/stdio.py | 98 +++++++++++++++---------------- test/unit/test_cmdline.py | 6 +- test/unit/test_options_manager.py | 28 ++++++++- 11 files changed, 160 insertions(+), 112 deletions(-) diff --git a/docs/command_reference.md b/docs/command_reference.md index aec1943e1..c33307678 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -1063,7 +1063,7 @@ Enable or disable semihosting. Provided for compatibility with OpenOCD. The same ##### `gdbserver` **Usage**: gdbserver {start,stop,status} \ -Control the gdbserver for the selected core. The action argument should be either 'start', 'stop', or 'status'. Use the 'gdbserver_port' and 'telnet_port' session options to control the ports the gdbserver uses. +Control the gdbserver for the selected core. The action argument should be either 'start', 'stop', or 'status'. Use the 'gdbserver_port' and 'stdio_port' session options to control the ports the gdbserver uses. ##### `probeserver` diff --git a/docs/options.md b/docs/options.md index 93bfa1e5c..d913ebde1 100644 --- a/docs/options.md +++ b/docs/options.md @@ -472,7 +472,7 @@ Examples: -telnet_mode +stdio_mode str, list of str 'server' Controls connection to standard I/O. Must be one of 'off', 'console', 'server', or 'file'. For multicore devices @@ -487,10 +487,10 @@ console and the standard I/O for core1 is routed to the Telnet Server.< -telnet_port +stdio_port int, list of int 4444 -The option applies only when telnet_mode is set to 'server'. Base TCP port for the Telnet Server. +The option applies only when stdio_mode is set to 'server'. Base TCP port for the Telnet Server. For multicore targets the zero-based core number will be added to the value unless a list is specified. Examples: @@ -502,10 +502,10 @@ Examples: -telnet_file_in +stdio_file_in str, list of str <target>.in -The option applies only when telnet_mode is set to 'file'. Input filename to use as stdin. +The option applies only when stdio_mode is set to 'file'. Input filename to use as stdin. When a single string is used, the core number (_<core>) will be appended to the filename in case of multicore targets. @@ -519,10 +519,10 @@ assigned to core0 and mytarget_cm0.in is assigned to core1 -telnet_file_out +stdio_file_out str, list of str <target>.out -The option applies only when telnet_mode is set to 'file'. Output filename to use as stdout. +The option applies only when stdio_mode is set to 'file'. Output filename to use as stdout. When a single string is used, the core number (_<core>) will be appended to the filename in case of multicore targets. diff --git a/docs/semihosting.md b/docs/semihosting.md index a5735c034..29056c73c 100644 --- a/docs/semihosting.md +++ b/docs/semihosting.md @@ -38,7 +38,7 @@ To use semihosting with pyOCD, it first must be enabled. This can be done in sev - Set the `enable_semihosting` session option to true. - From gdb, issue a `monitor arm semihosting enable` command. This command is supported for compatibility with OpenOCD. -Assuming the default routing options and telnet port number are acceptable, no further configuration is required. When +Assuming the default routing options and STDIO port number are acceptable, no further configuration is required. When
Currently semihosting only works when pyOCD's gdbserver is running and the target is resumed, with the gdbserver @@ -58,7 +58,7 @@ All other requests, which are always for explicitly opened files, are classified Console requests have two options for routing, controlled with the `semihost_console_type` session option's value. - "telnet": I/O is connected to the "telnet" server. (In fact, it's not a true telnet server because it doesn't implement - the telnet control requests.) This server runs on the port indicated by the `telnet_port` session option or the + the telnet control requests.) This server runs on the port indicated by the `stdio_port` session option or the `-T`/`--telnet-port` command line argument of the gdbserver subcommand. - "console": pyOCD's standard I/O is used. @@ -125,7 +125,7 @@ These are the session options that control semihosting: - `semihost_console_type` - If set to 'telnet' then the semihosting telnet server will be started. If set to 'console' then semihosting will print to pyOCD's console. - `semihost.commandline` - Command line string return to the target for the `SYS_GET_CMDLINE` request. - `semihost_use_syscalls` - Whether to use GDB syscalls for semihosting file access operations, or to have pyOCD perform the operations.) -- `telnet_port` - Base TCP port number for the semihosting telnet server. The core number, which will be 0 for the primary core, is added to this value. +- `stdio_port` - Base TCP port number for the semihosting STDIO server. The core number, which will be 0 for the primary core, is added to this value. ### Supported requests diff --git a/pyocd/core/options.py b/pyocd/core/options.py index e55d42046..3cb7f4d5e 100644 --- a/pyocd/core/options.py +++ b/pyocd/core/options.py @@ -196,8 +196,8 @@ class OptionInfo(NamedTuple): "TCP port number for the raw SWV stream server."), OptionInfo('swv_raw_file', str, None, "File path for the raw SWV stream output."), - OptionInfo('telnet_port', (int, tuple), 4444, - "Base TCP port number for the semihosting telnet server."), + OptionInfo('stdio_port', (int, tuple), 4444, + "Base TCP port number for the STDIO server."), OptionInfo('vector_catch', str, 'h', "Enable vector catch sources."), OptionInfo('register_fields', bool, True, @@ -207,12 +207,12 @@ class OptionInfo(NamedTuple): "Replace software breakpoints with hardware breakpoints."), # Internal cbuild-run session options - OptionInfo('telnet_mode', (str, tuple), None, - "List of telnet modes for each core."), - OptionInfo('telnet_file_in', (str, tuple), None, - "List of telnet input file paths for each core."), - OptionInfo('telnet_file_out', (str, tuple), None, - "List of telnet output file paths for each core."), + OptionInfo('stdio_mode', (str, tuple), None, + "List of STDIO modes for each core."), + OptionInfo('stdio_file_in', (str, tuple), None, + "List of STDIO input file paths for each core."), + OptionInfo('stdio_file_out', (str, tuple), None, + "List of STDIO output file paths for each core."), OptionInfo('rtt', tuple, None, "List of RTT configurations for each core."), OptionInfo('systemview_file', str, None, @@ -223,6 +223,14 @@ class OptionInfo(NamedTuple): "Enable automatic stop of SystemView."), ] +## @brief Aliases for backwards-compatible option names. +OPTIONS_ALIASES: Dict[str, str] = { + 'telnet_port': 'stdio_port', + 'telnet_mode': 'stdio_mode', + 'telnet_file_in': 'stdio_file_in', + 'telnet_file_out': 'stdio_file_out', +} + ## @brief The runtime dictionary of options. OPTIONS_INFO: Dict[str, OptionInfo] = {} diff --git a/pyocd/core/options_manager.py b/pyocd/core/options_manager.py index 862a44595..412df94af 100644 --- a/pyocd/core/options_manager.py +++ b/pyocd/core/options_manager.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2019-2020 Arm Limited +# Copyright (c) 2019-2020,2026 Arm Limited # Copyright (c) 2021 Chris Reed # SPDX-License-Identifier: Apache-2.0 # @@ -19,7 +19,7 @@ from functools import partial from typing import (Any, Callable, Dict, List, Mapping, NamedTuple, Optional) -from .options import OPTIONS_INFO +from .options import (OPTIONS_ALIASES, OPTIONS_INFO) from ..utility.notification import Notifier LOG = logging.getLogger(__name__) @@ -101,14 +101,15 @@ def _convert_options(self, new_options: LayerType) -> LayerType: 1. Strip dictionary entries with a value of None. 2. Replace double-underscores ("__") with a dot ("."). 3. Convert option names to all-lowercase. + 4. Resolve option aliases. """ output = {} for name, value in new_options.items(): if value is None: continue - else: - name = name.replace("__", ".").lower() - output[name] = value + name = name.replace("__", ".").lower() + name = OPTIONS_ALIASES.get(name, name) + output[name] = value return output def is_set(self, key: str) -> bool: diff --git a/pyocd/core/session.py b/pyocd/core/session.py index 197c38342..c0bf0cd5e 100644 --- a/pyocd/core/session.py +++ b/pyocd/core/session.py @@ -295,11 +295,11 @@ def _get_cbuild_run_config(self, command: Optional[str]) -> Dict[str, Any]: debugger_options['connect_mode'] = connect_mode debugger_options['gdbserver_port'] = self.cbuild_run.gdbserver_port - debugger_options['telnet_port'] = self.cbuild_run.telnet_port - debugger_options['telnet_mode'] = self.cbuild_run.telnet_mode - telnet_file = self.cbuild_run.telnet_file - debugger_options['telnet_file_in'] = telnet_file.get('in') - debugger_options['telnet_file_out'] = telnet_file.get('out') + debugger_options['stdio_port'] = self.cbuild_run.stdio_port + debugger_options['stdio_mode'] = self.cbuild_run.stdio_mode + stdio_file = self.cbuild_run.stdio_file + debugger_options['stdio_file_in'] = stdio_file.get('in') + debugger_options['stdio_file_out'] = stdio_file.get('out') debugger_options['rtt'] = self.cbuild_run.rtt debugger_options['systemview_file'] = self.cbuild_run.systemview_file diff --git a/pyocd/target/pack/cbuild_run.py b/pyocd/target/pack/cbuild_run.py index d90a08e86..3ec731845 100644 --- a/pyocd/target/pack/cbuild_run.py +++ b/pyocd/target/pack/cbuild_run.py @@ -518,8 +518,12 @@ def flashinfo(self) -> List[dict]: def debugger(self) -> Dict[str, Any]: """@brief Debugger section of cbuild-run.""" if self._debugger is None: - self._debugger = self._data.get('debugger', {}) - LOG.debug("Read debugger configuration: %s", self._debugger) + _debugger = self._data.get('debugger') or {} + LOG.debug("Read debugger configuration: %s", _debugger) + if 'stdio' in _debugger and 'telnet' in _debugger: + LOG.warning("Both 'stdio' and 'telnet' sections found in debugger configuration. " + "Using 'stdio' section and ignoring 'telnet'.") + self._debugger = _debugger return self._debugger @property @@ -627,52 +631,53 @@ def gdbserver_port(self) -> Optional[Tuple]: return self._get_server_port('gdbserver') @property - def telnet_port(self) -> Optional[Tuple]: - """@brief Telnet server port assignments from debugger section. + def stdio_port(self) -> Optional[Tuple]: + """@brief STDIO server port assignments from debugger section. The method will not be called frequently, so performance is not critical. """ - return self._get_server_port('telnet') + server_type = 'stdio' if 'stdio' in self.debugger else 'telnet' + return self._get_server_port(server_type) @property - def telnet_mode(self) -> Tuple: - """@brief Telnet server mode assignments from debugger section. + def stdio_mode(self) -> Tuple: + """@brief STDIO mode assignments from debugger section. The method will not be called frequently, so performance is not critical. """ SUPPORTED_MODES = { 'off', 'server', 'file', 'console' } MODE_ALIASES = { False: 'off', 'monitor': 'server' } - # Get telnet configuration from debugger section - telnet_config = self.debugger.get('telnet') or [] - valid_config = any('mode' in t for t in telnet_config) + # Get STDIO configuration from debugger section + stdio_config = self._get_stdio_config() + valid_config = any('mode' in s for s in stdio_config) # Determine global mode if specified, default to 'off' otherwise - global_mode = next((t.get('mode') for t in telnet_config if 'pname' not in t), 'off') + global_mode = next((s.get('mode') for s in stdio_config if 'pname' not in s), 'off') global_mode = MODE_ALIASES.get(global_mode, global_mode) - # Build list of telnet modes for each core - telnet_mode = [] + # Build list of STDIO modes for each core + stdio_mode = [] for core in self.sorted_processors: - mode = next((t.get('mode') for t in telnet_config if t.get('pname') == core.name), global_mode) + mode = next((s.get('mode') for s in stdio_config if s.get('pname') == core.name), global_mode) mode = MODE_ALIASES.get(mode, mode) if mode not in SUPPORTED_MODES: if valid_config: - LOG.warning("Invalid telnet mode '%s' for core '%s' in cbuild-run, defaulting to '%s'", + LOG.warning("Invalid STDIO mode '%s' for core '%s' in cbuild-run, defaulting to '%s'", mode, core.name, global_mode) mode = global_mode - telnet_mode.append(mode) + stdio_mode.append(mode) - return tuple(telnet_mode) + return tuple(stdio_mode) @property - def telnet_file(self) -> Dict[str, Optional[Tuple]]: - """@brief Telnet file path assignments from debugger section. + def stdio_file(self) -> Dict[str, Optional[Tuple]]: + """@brief STDIO file path assignments from debugger section. The method will not be called frequently, so performance is not critical. """ - # Get telnet configuration from debugger section - telnet_config = self.debugger.get('telnet') or [] - telnet_mode = self.telnet_mode + # Get STDIO configuration from debugger section + stdio_config = self._get_stdio_config() + stdio_mode = self.stdio_mode - if not any(mode == 'file' for mode in telnet_mode): - # No telnet file paths needed + if not any(mode == 'file' for mode in stdio_mode): + # No STDIO file paths needed return {'in': None, 'out': None} def _resolve_path(file_path: Optional[str], strict: bool = False) -> Optional[str]: @@ -685,7 +690,7 @@ def _resolve_path(file_path: Optional[str], strict: bool = False) -> Optional[st resolved_path = file_path_obj.resolve() # In strict mode check if the file exists if strict and not resolved_path.is_file(): - LOG.warning("Telnet file '%s' not found", resolved_path) + LOG.warning("STDIO file '%s' not found", resolved_path) return str(resolved_path) @@ -693,11 +698,11 @@ def _resolve_path(file_path: Optional[str], strict: bool = False) -> Optional[st out_files = [] # Per pname configuration - config_by_pname = {t['pname']: t for t in telnet_config if 'pname' in t} + config_by_pname = {s['pname']: s for s in stdio_config if 'pname' in s} if config_by_pname: # Build config per pname - for proc_info, mode in zip(self.sorted_processors, telnet_mode): + for proc_info, mode in zip(self.sorted_processors, stdio_mode): if mode != 'file': in_files.append(None) out_files.append(None) @@ -714,11 +719,11 @@ def _resolve_path(file_path: Optional[str], strict: bool = False) -> Optional[st out_file = str((self._base_path / f"{self._cbuild_name}.{proc_info.name}.out").resolve()) out_files.append(out_file) else: - config = next((t for t in telnet_config if t.get('mode') == 'file'), None) + config = next((s for s in stdio_config if s.get('mode') == 'file'), None) if config is not None: if len(self.sorted_processors) > 1: - LOG.warning("Ignoring invalid telnet file configuration for multicore target in cbuild-run") - for proc_info, mode in zip(self.sorted_processors, telnet_mode): + LOG.warning("Ignoring invalid STDIO file configuration for multicore target in cbuild-run") + for proc_info, mode in zip(self.sorted_processors, stdio_mode): if mode != 'file': in_files.append(None) out_files.append(None) @@ -795,6 +800,11 @@ def populate_target(self, target: Optional[str] = None) -> None: }) TARGET[target] = tgt + def _get_stdio_config(self) -> List[dict]: + """@brief Returns STDIO configuration from debugger section, with telnet as an alias.""" + server_type = 'stdio' if 'stdio' in self.debugger else 'telnet' + return self.debugger.get(server_type) or [] + def _get_server_port(self, server_type: str) -> Optional[Tuple]: """@brief Generic method to get server port assignments from debugger section.""" server_config = self.debugger.get(server_type, []) diff --git a/pyocd/utility/cmdline.py b/pyocd/utility/cmdline.py index 7b785179e..54fc77d62 100644 --- a/pyocd/utility/cmdline.py +++ b/pyocd/utility/cmdline.py @@ -21,7 +21,7 @@ import yaml from ..core.target import Target -from ..core.options import OPTIONS_INFO +from ..core.options import (OPTIONS_ALIASES, OPTIONS_INFO) from ..utility.compatibility import to_str_safe LOG = logging.getLogger(__name__) @@ -165,6 +165,7 @@ def convert_one_session_option(name: str, value: Optional[str]) -> Tuple[str, An had_no_prefix = True else: had_no_prefix = False + name = OPTIONS_ALIASES.get(name, name) # Look up this option. try: diff --git a/pyocd/utility/stdio.py b/pyocd/utility/stdio.py index 02ce515e9..cd7663bd0 100644 --- a/pyocd/utility/stdio.py +++ b/pyocd/utility/stdio.py @@ -58,29 +58,29 @@ class StdioTelnet(StdioBase): """STDIO backend that uses a telnet server for reading from and writing to stdin/stdout.""" def __init__(self, session: Session, core: int = 0) -> None: - _telnet_ports = session.options.get('telnet_port') - if isinstance(_telnet_ports, (list, tuple)): - if len(_telnet_ports) <= core or _telnet_ports[core] is None: - raise ValueError(f"STDIO: telnet for core {core} requires a port number in the 'telnet_port' list") - telnet_port = _telnet_ports[core] + _stdio_ports = session.options.get('stdio_port') + if isinstance(_stdio_ports, (list, tuple)): + if len(_stdio_ports) <= core or _stdio_ports[core] is None: + raise ValueError(f"STDIO: server for core {core} requires a port number in the 'stdio_port' list") + stdio_port = _stdio_ports[core] else: - telnet_port = _telnet_ports - if telnet_port != 0: - telnet_port += core + stdio_port = _stdio_ports + if stdio_port != 0: + stdio_port += core serve_local_only = session.options.get('serve_local_only') self._server = StreamServer( - port=telnet_port, + port=stdio_port, serve_local_only=serve_local_only, name="STDIO", is_read_only=False, extra_info=f"core {core}" ) - if telnet_port == 0: - telnet_port = self._server.port + if stdio_port == 0: + stdio_port = self._server.port - self._telnet_port = telnet_port + self._stdio_port = stdio_port # ToDo: consider waiting for client to connect # while self._server._connected_socket is None: @@ -90,7 +90,7 @@ def write(self, data: bytes) -> int: try: return self._server.write(data) except Exception as e: - LOG.debug("Error writing to STDIO telnet server (port %d): %s", self._telnet_port, e) + LOG.debug("Error writing to STDIO telnet server (port %d): %s", self._stdio_port, e) return 0 def read(self, max_bytes: int) -> bytes: @@ -98,7 +98,7 @@ def read(self, max_bytes: int) -> bytes: try: data = self._server.read(max_bytes) except Exception as e: - LOG.debug("Error reading from STDIO telnet server (port %d): %s", self._telnet_port, e) + LOG.debug("Error reading from STDIO telnet server (port %d): %s", self._stdio_port, e) if data is None: return b"" return bytes(data) @@ -107,7 +107,7 @@ def shutdown(self) -> None: try: self._server.stop() except Exception as e: - LOG.debug("Error stopping STDIO telnet server (port %d): %s", self._telnet_port, e) + LOG.debug("Error stopping STDIO telnet server (port %d): %s", self._stdio_port, e) @property def info(self) -> str: @@ -120,60 +120,60 @@ def __init__(self, session: Session, core: int = 0) -> None: is_multi_core = len(session.board.target.cores) > 1 # Get file paths from session options - if session.options.is_set('telnet_file_out'): - _telnet_file_out = session.options.get('telnet_file_out') - if isinstance(_telnet_file_out, (list, tuple)): - if len(_telnet_file_out) <= core or _telnet_file_out[core] is None: + if session.options.is_set('stdio_file_out'): + _stdio_file_out = session.options.get('stdio_file_out') + if isinstance(_stdio_file_out, (list, tuple)): + if len(_stdio_file_out) <= core or _stdio_file_out[core] is None: raise ValueError(f"STDIO file for core {core} requires a valid output file path") - telnet_file_out = _telnet_file_out[core] + stdio_file_out = _stdio_file_out[core] else: if is_multi_core: - root, ext = os.path.splitext(_telnet_file_out) - _telnet_file_out = root + f"_{core}" + ext - telnet_file_out = _telnet_file_out + root, ext = os.path.splitext(_stdio_file_out) + _stdio_file_out = root + f"_{core}" + ext + stdio_file_out = _stdio_file_out else: # Default target_type = session.board.target_type - telnet_file_out = f"{target_type}_{core}.out" if is_multi_core else f"{target_type}.out" + stdio_file_out = f"{target_type}_{core}.out" if is_multi_core else f"{target_type}.out" - telnet_file_in = None - if session.options.is_set('telnet_file_in'): - _telnet_file_in = session.options.get('telnet_file_in') - if isinstance(_telnet_file_in, (list, tuple)): - if len(_telnet_file_in) <= core or _telnet_file_in[core] is None: + stdio_file_in = None + if session.options.is_set('stdio_file_in'): + _stdio_file_in = session.options.get('stdio_file_in') + if isinstance(_stdio_file_in, (list, tuple)): + if len(_stdio_file_in) <= core or _stdio_file_in[core] is None: LOG.debug("No input file configured for core %d", core) - telnet_file_in = _telnet_file_in[core] + stdio_file_in = _stdio_file_in[core] else: if is_multi_core: - root, ext = os.path.splitext(_telnet_file_in) - _telnet_file_in = root + f"_{core}" + ext - telnet_file_in = _telnet_file_in + root, ext = os.path.splitext(_stdio_file_in) + _stdio_file_in = root + f"_{core}" + ext + stdio_file_in = _stdio_file_in else: # Default target_type = session.board.target_type - telnet_file_in = f"{target_type}_{core}.in" if is_multi_core else f"{target_type}.in" + stdio_file_in = f"{target_type}_{core}.in" if is_multi_core else f"{target_type}.in" # Check if the folder exists for input/output files - dir_out = os.path.dirname(telnet_file_out) - self._fname_out = os.path.basename(telnet_file_out) + dir_out = os.path.dirname(stdio_file_out) + self._fname_out = os.path.basename(stdio_file_out) if dir_out and not os.path.exists(dir_out): raise FileNotFoundError(f"Directory {dir_out} for STDIO file {self._fname_out} does not exist") # Open files - if telnet_file_in is not None and os.path.exists(telnet_file_in): - self._input_file = open(telnet_file_in, 'rb') - self._fname_in = os.path.basename(telnet_file_in) + if stdio_file_in is not None and os.path.exists(stdio_file_in): + self._input_file = open(stdio_file_in, 'rb') + self._fname_in = os.path.basename(stdio_file_in) else: - LOG.debug("Input file '%s' does not exist; STDIN will be disabled", telnet_file_in) + LOG.debug("Input file '%s' does not exist; STDIN will be disabled", stdio_file_in) self._input_file = None self._fname_in = None try: - self._output_file = open(telnet_file_out, 'wb') + self._output_file = open(stdio_file_out, 'wb') except OSError as e: if self._input_file: self._input_file.close() - raise IOError(f"Failed to open STDIO file {telnet_file_out}: {e}") + raise IOError(f"Failed to open STDIO file {stdio_file_out}: {e}") def write(self, data: bytes) -> int: # Output file is valid - else exception raised in constructor @@ -308,14 +308,14 @@ class StdioHandler(StdioBase): """ def __init__(self, session: Session, core: int = 0, eot_enabled: bool = False) -> None: - if session.options.is_set('telnet_mode'): - _telnet_mode = session.options.get('telnet_mode') - if isinstance(_telnet_mode, (list, tuple)): - if len(_telnet_mode) <= core or _telnet_mode[core] is None: - raise ValueError(f"STDIO mode for core {core} requires a 'telnet_mode'") - stdio_mode = _telnet_mode[core] + if session.options.is_set('stdio_mode'): + _stdio_mode = session.options.get('stdio_mode') + if isinstance(_stdio_mode, (list, tuple)): + if len(_stdio_mode) <= core or _stdio_mode[core] is None: + raise ValueError(f"STDIO mode for core {core} requires a 'stdio_mode'") + stdio_mode = _stdio_mode[core] else: - stdio_mode = _telnet_mode + stdio_mode = _stdio_mode else: stdio_mode = session.options.get('semihost_console_type') diff --git a/test/unit/test_cmdline.py b/test/unit/test_cmdline.py index 680af4740..15d16a7d0 100644 --- a/test/unit/test_cmdline.py +++ b/test/unit/test_cmdline.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2015,2018-2019 Arm Limited +# Copyright (c) 2015,2018-2019,2026 Arm Limited # Copyright (c) 2022 Chris Reed # SPDX-License-Identifier: Apache-2.0 # @@ -143,6 +143,10 @@ def test_str(self): # Valid assert convert_session_options(['test_binary=abc']) == {'test_binary': 'abc'} + def test_option_alias(self): + assert convert_session_options(['telnet_mode=file']) == {'stdio_mode': 'file'} + assert convert_session_options(['telnet_port=4445']) == {'stdio_port': 4445} + class TestTargetTypeNormalisation: def test_passthrough(self): assert normalise_target_type_name("foobar") == "foobar" diff --git a/test/unit/test_options_manager.py b/test/unit/test_options_manager.py index d8659ffaf..0130ce29c 100644 --- a/test/unit/test_options_manager.py +++ b/test/unit/test_options_manager.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2019 Arm Limited +# Copyright (c) 2019,2026 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ import pytest from pyocd.core.options_manager import OptionsManager -from pyocd.core.options import OPTIONS_INFO +from pyocd.core.options import (OPTIONS_ALIASES, OPTIONS_INFO) @pytest.fixture(scope='function') def mgr(): @@ -73,6 +73,30 @@ def test_convert_double_underscore(self, mgr): assert 'debug.traceback' in mgr assert mgr.get('debug.traceback') == False + def test_option_alias(self, mgr): + mgr.add_back({'telnet_mode': 'file'}) + assert 'stdio_mode' in mgr + assert 'telnet_mode' not in mgr + assert mgr.get('stdio_mode') == 'file' + assert mgr.get('telnet_mode') is None + assert mgr.get_default('telnet_mode') is None + + def test_option_alias_layer_priority(self, mgr): + mgr.add_back({'stdio_mode': 'server'}) + mgr.add_front({'telnet_mode': 'file'}) + assert mgr.get('stdio_mode') == 'file' + + def test_option_alias_same_layer_order(self, mgr): + mgr.add_back({'telnet_mode': 'file', 'stdio_mode': 'console'}) + assert mgr.get('stdio_mode') == 'console' + + mgr = OptionsManager() + mgr.add_back({'stdio_mode': 'console', 'telnet_mode': 'file'}) + assert mgr.get('stdio_mode') == 'file' + + def test_legacy_aliases_not_registered_as_options(self): + assert not any(name in OPTIONS_INFO for name in OPTIONS_ALIASES) + def test_set(self, mgr, layer1): mgr.add_front(layer1) mgr.set('buzz', 1234) From 3b3c9fa78084e5a23d75d7899facd053a23736a0 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 14 Jul 2026 15:13:22 +0200 Subject: [PATCH 2/3] stdio: separate stdio server from semihosting telnet --- docs/command_reference.md | 2 +- docs/options.md | 2 +- docs/semihosting.md | 6 +++--- pyocd/core/options.py | 10 ++++++---- pyocd/utility/stdio.py | 6 +++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/command_reference.md b/docs/command_reference.md index c33307678..aec1943e1 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -1063,7 +1063,7 @@ Enable or disable semihosting. Provided for compatibility with OpenOCD. The same ##### `gdbserver` **Usage**: gdbserver {start,stop,status} \ -Control the gdbserver for the selected core. The action argument should be either 'start', 'stop', or 'status'. Use the 'gdbserver_port' and 'stdio_port' session options to control the ports the gdbserver uses. +Control the gdbserver for the selected core. The action argument should be either 'start', 'stop', or 'status'. Use the 'gdbserver_port' and 'telnet_port' session options to control the ports the gdbserver uses. ##### `probeserver` diff --git a/docs/options.md b/docs/options.md index d913ebde1..e72063dd8 100644 --- a/docs/options.md +++ b/docs/options.md @@ -400,7 +400,7 @@ Controls whether all 256 ADIv5 AP addresses will be probed. bool True -When this option is True, the GDB server, probe server, semihosting telnet, and raw SWV server are only served +When this option is True, the GDB server, probe server, stdio server, rtt server, and raw SWV server are only served on localhost, making them inaccessible across the network. Set to False to enable connecting to these ports from any machine on the network. diff --git a/docs/semihosting.md b/docs/semihosting.md index 29056c73c..a5735c034 100644 --- a/docs/semihosting.md +++ b/docs/semihosting.md @@ -38,7 +38,7 @@ To use semihosting with pyOCD, it first must be enabled. This can be done in sev - Set the `enable_semihosting` session option to true. - From gdb, issue a `monitor arm semihosting enable` command. This command is supported for compatibility with OpenOCD. -Assuming the default routing options and STDIO port number are acceptable, no further configuration is required. When +Assuming the default routing options and telnet port number are acceptable, no further configuration is required. When
Currently semihosting only works when pyOCD's gdbserver is running and the target is resumed, with the gdbserver @@ -58,7 +58,7 @@ All other requests, which are always for explicitly opened files, are classified Console requests have two options for routing, controlled with the `semihost_console_type` session option's value. - "telnet": I/O is connected to the "telnet" server. (In fact, it's not a true telnet server because it doesn't implement - the telnet control requests.) This server runs on the port indicated by the `stdio_port` session option or the + the telnet control requests.) This server runs on the port indicated by the `telnet_port` session option or the `-T`/`--telnet-port` command line argument of the gdbserver subcommand. - "console": pyOCD's standard I/O is used. @@ -125,7 +125,7 @@ These are the session options that control semihosting: - `semihost_console_type` - If set to 'telnet' then the semihosting telnet server will be started. If set to 'console' then semihosting will print to pyOCD's console. - `semihost.commandline` - Command line string return to the target for the `SYS_GET_CMDLINE` request. - `semihost_use_syscalls` - Whether to use GDB syscalls for semihosting file access operations, or to have pyOCD perform the operations.) -- `stdio_port` - Base TCP port number for the semihosting STDIO server. The core number, which will be 0 for the primary core, is added to this value. +- `telnet_port` - Base TCP port number for the semihosting telnet server. The core number, which will be 0 for the primary core, is added to this value. ### Supported requests diff --git a/pyocd/core/options.py b/pyocd/core/options.py index 3cb7f4d5e..5b3b8883b 100644 --- a/pyocd/core/options.py +++ b/pyocd/core/options.py @@ -139,7 +139,7 @@ class OptionInfo(NamedTuple): OptionInfo('scan_all_aps', bool, False, "Controls whether all 256 ADIv5 AP addresses will be probed. Default is False."), OptionInfo('serve_local_only', bool, True, - "When this option is True, the GDB server, probe server, and semihosting telnet, and raw SWV " + "When this option is True, the GDB server, probe server, stdio server, rtt server, and raw SWV " "server are only served on localhost. Set to False to enable remote connections."), OptionInfo('smart_flash', bool, True, "If set to True, the flash loader will attempt to not program pages whose contents are not " @@ -196,8 +196,8 @@ class OptionInfo(NamedTuple): "TCP port number for the raw SWV stream server."), OptionInfo('swv_raw_file', str, None, "File path for the raw SWV stream output."), - OptionInfo('stdio_port', (int, tuple), 4444, - "Base TCP port number for the STDIO server."), + OptionInfo('telnet_port', (int, tuple), 4444, + "Base TCP port number for the semihosting telnet server."), OptionInfo('vector_catch', str, 'h', "Enable vector catch sources."), OptionInfo('register_fields', bool, True, @@ -206,9 +206,11 @@ class OptionInfo(NamedTuple): OptionInfo('soft_bkpt_as_hard', bool, False, "Replace software breakpoints with hardware breakpoints."), - # Internal cbuild-run session options + # Extended options with multicore support OptionInfo('stdio_mode', (str, tuple), None, "List of STDIO modes for each core."), + OptionInfo('stdio_port', (int, tuple), 4444, + "Base TCP port number for the STDIO server."), OptionInfo('stdio_file_in', (str, tuple), None, "List of STDIO input file paths for each core."), OptionInfo('stdio_file_out', (str, tuple), None, diff --git a/pyocd/utility/stdio.py b/pyocd/utility/stdio.py index cd7663bd0..d5eeaacd6 100644 --- a/pyocd/utility/stdio.py +++ b/pyocd/utility/stdio.py @@ -54,7 +54,7 @@ class StdioOff(StdioBase): def info(self) -> str: return "off" -class StdioTelnet(StdioBase): +class StdioServer(StdioBase): """STDIO backend that uses a telnet server for reading from and writing to stdin/stdout.""" def __init__(self, session: Session, core: int = 0) -> None: @@ -296,8 +296,8 @@ def info(self) -> str: _BACKEND_CLASSES: Dict[str or bool, Type[StdioBase]] = { False: StdioOff, "off": StdioOff, - "server": StdioTelnet, - "telnet": StdioTelnet, + "server": StdioServer, + "telnet": StdioServer, "file": StdioFile, "console": StdioConsole } From c4a43d2d3dcf5434d1dbf33cd55075fd62cf6139 Mon Sep 17 00:00:00 2001 From: Teo Mahnic Date: Tue, 14 Jul 2026 15:31:15 +0200 Subject: [PATCH 3/3] options: remove telnet_port entry --- pyocd/core/options.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyocd/core/options.py b/pyocd/core/options.py index 5b3b8883b..cff12df16 100644 --- a/pyocd/core/options.py +++ b/pyocd/core/options.py @@ -196,8 +196,6 @@ class OptionInfo(NamedTuple): "TCP port number for the raw SWV stream server."), OptionInfo('swv_raw_file', str, None, "File path for the raw SWV stream output."), - OptionInfo('telnet_port', (int, tuple), 4444, - "Base TCP port number for the semihosting telnet server."), OptionInfo('vector_catch', str, 'h', "Enable vector catch sources."), OptionInfo('register_fields', bool, True,