1#! /usr/bin/env python3
2
3# Copyright (c) 2017 Linaro Limited.
4# Copyright (c) 2017 Open Source Foundries Limited.
5#
6# SPDX-License-Identifier: Apache-2.0
7
8"""Zephyr binary runner core interfaces
9
10This provides the core ZephyrBinaryRunner class meant for public use,
11as well as some other helpers for concrete runner classes.
12"""
13
14import abc
15import argparse
16import errno
17import logging
18import os
19import platform
20import shlex
21import shutil
22import signal
23import subprocess
24import re
25from dataclasses import dataclass, field
26from functools import partial
27from enum import Enum
28from inspect import isabstract
29from typing import Dict, List, NamedTuple, NoReturn, Optional, Set, Type, \
30    Union
31
32# Turn on to enable just logging the commands that would be run (at
33# info rather than debug level), without actually running them. This
34# can break runners that are expecting output or if one command
35# depends on another, so it's just for debugging.
36_DRY_RUN = False
37
38_logger = logging.getLogger('runners')
39
40
41class _DebugDummyPopen:
42
43    def terminate(self):
44        pass
45
46    def wait(self):
47        pass
48
49
50MAX_PORT = 49151
51
52
53class NetworkPortHelper:
54    '''Helper class for dealing with local IP network ports.'''
55
56    def get_unused_ports(self, starting_from):
57        '''Find unused network ports, starting at given values.
58
59        starting_from is an iterable of ports the caller would like to use.
60
61        The return value is an iterable of ports, in the same order, using
62        the given values if they were unused, or the next sequentially
63        available unused port otherwise.
64
65        Ports may be bound between this call's check and actual usage, so
66        callers still need to handle errors involving returned ports.'''
67        start = list(starting_from)
68        used = self._used_now()
69        ret = []
70
71        for desired in start:
72            port = desired
73            while port in used:
74                port += 1
75                if port > MAX_PORT:
76                    msg = "ports above {} are in use"
77                    raise ValueError(msg.format(desired))
78            used.add(port)
79            ret.append(port)
80
81        return ret
82
83    def _used_now(self):
84        handlers = {
85            'Windows': self._used_now_windows,
86            'Linux': self._used_now_linux,
87            'Darwin': self._used_now_darwin,
88        }
89        handler = handlers[platform.system()]
90        return handler()
91
92    def _used_now_windows(self):
93        cmd = ['netstat', '-a', '-n', '-p', 'tcp']
94        return self._parser_windows(cmd)
95
96    def _used_now_linux(self):
97        cmd = ['ss', '-a', '-n', '-t']
98        return self._parser_linux(cmd)
99
100    def _used_now_darwin(self):
101        cmd = ['netstat', '-a', '-n', '-p', 'tcp']
102        return self._parser_darwin(cmd)
103
104    @staticmethod
105    def _parser_windows(cmd):
106        out = subprocess.check_output(cmd).split(b'\r\n')
107        used_bytes = [x.split()[1].rsplit(b':', 1)[1] for x in out
108                      if x.startswith(b'  TCP')]
109        return {int(b) for b in used_bytes}
110
111    @staticmethod
112    def _parser_linux(cmd):
113        out = subprocess.check_output(cmd).splitlines()[1:]
114        used_bytes = [s.split()[3].rsplit(b':', 1)[1] for s in out]
115        return {int(b) for b in used_bytes}
116
117    @staticmethod
118    def _parser_darwin(cmd):
119        out = subprocess.check_output(cmd).split(b'\n')
120        used_bytes = [x.split()[3].rsplit(b':', 1)[1] for x in out
121                      if x.startswith(b'tcp')]
122        return {int(b) for b in used_bytes}
123
124
125class BuildConfiguration:
126    '''This helper class provides access to build-time configuration.
127
128    Configuration options can be read as if the object were a dict,
129    either object['CONFIG_FOO'] or object.get('CONFIG_FOO').
130
131    Kconfig configuration values are available (parsed from .config).'''
132
133    config_prefix = 'CONFIG'
134
135    def __init__(self, build_dir: str):
136        self.build_dir = build_dir
137        self.options: Dict[str, Union[str, int]] = {}
138        self.path = os.path.join(self.build_dir, 'zephyr', '.config')
139        self._parse()
140
141    def __contains__(self, item):
142        return item in self.options
143
144    def __getitem__(self, item):
145        return self.options[item]
146
147    def get(self, option, *args):
148        return self.options.get(option, *args)
149
150    def getboolean(self, option):
151        '''If a boolean option is explicitly set to y or n,
152        returns its value. Otherwise, falls back to False.
153        '''
154        return self.options.get(option, False)
155
156    def _parse(self):
157        filename = self.path
158
159        opt_value = re.compile(f'^(?P<option>{self.config_prefix}_[A-Za-z0-9_]+)=(?P<value>.*)$')
160        not_set = re.compile(f'^# (?P<option>{self.config_prefix}_[A-Za-z0-9_]+) is not set$')
161
162        with open(filename, 'r') as f:
163            for line in f:
164                match = opt_value.match(line)
165                if match:
166                    value = match.group('value').rstrip()
167                    if value.startswith('"') and value.endswith('"'):
168                        # A string literal should have the quotes stripped,
169                        # but otherwise be left as is.
170                        value = value[1:-1]
171                    elif value == 'y':
172                        # The character 'y' is a boolean option
173                        # that is set to True.
174                        value = True
175                    else:
176                        # Neither a string nor 'y', so try to parse it
177                        # as an integer.
178                        try:
179                            base = 16 if value.startswith('0x') else 10
180                            self.options[match.group('option')] = int(value, base=base)
181                            continue
182                        except ValueError:
183                            pass
184
185                    self.options[match.group('option')] = value
186                    continue
187
188                match = not_set.match(line)
189                if match:
190                    # '# CONFIG_FOO is not set' means a boolean option is false.
191                    self.options[match.group('option')] = False
192
193class SysbuildConfiguration(BuildConfiguration):
194    '''This helper class provides access to sysbuild-time configuration.
195
196    Configuration options can be read as if the object were a dict,
197    either object['SB_CONFIG_FOO'] or object.get('SB_CONFIG_FOO').
198
199    Kconfig configuration values are available (parsed from .config).'''
200
201    config_prefix = 'SB_CONFIG'
202
203    def _parse(self):
204        # If the build does not use sysbuild, skip parsing the file.
205        if not os.path.exists(self.path):
206            return
207        super()._parse()
208
209class MissingProgram(FileNotFoundError):
210    '''FileNotFoundError subclass for missing program dependencies.
211
212    No significant changes from the parent FileNotFoundError; this is
213    useful for explicitly signaling that the file in question is a
214    program that some class requires to proceed.
215
216    The filename attribute contains the missing program.'''
217
218    def __init__(self, program):
219        super().__init__(errno.ENOENT, os.strerror(errno.ENOENT), program)
220
221
222_RUNNERCAPS_COMMANDS = {'flash', 'debug', 'debugserver', 'attach', 'simulate', 'robot'}
223
224@dataclass
225class RunnerCaps:
226    '''This class represents a runner class's capabilities.
227
228    Each capability is represented as an attribute with the same
229    name. Flag attributes are True or False.
230
231    Available capabilities:
232
233    - commands: set of supported commands; default is {'flash',
234      'debug', 'debugserver', 'attach', 'simulate', 'robot'}.
235
236    - dev_id: whether the runner supports device identifiers, in the form of an
237      -i, --dev-id option. This is useful when the user has multiple debuggers
238      connected to a single computer, in order to select which one will be used
239      with the command provided.
240
241    - flash_addr: whether the runner supports flashing to an
242      arbitrary address. Default is False. If true, the runner
243      must honor the --dt-flash option.
244
245    - erase: whether the runner supports an --erase option, which
246      does a mass-erase of the entire addressable flash on the target
247      before flashing. On multi-core SoCs, this may only erase portions of
248      flash specific the actual target core. (This option can be useful for
249      things like clearing out old settings values or other subsystem state
250      that may affect the behavior of the zephyr image. It is also sometimes
251      needed by SoCs which have flash-like areas that can't be sector
252      erased by the underlying tool before flashing; UICR on nRF SoCs
253      is one example.)
254
255    - reset: whether the runner supports a --reset option, which
256      resets the device after a flash operation is complete.
257
258    - extload: whether the runner supports a --extload option, which
259      must be given one time and is passed on to the underlying tool
260      that the runner wraps.
261
262    - tool_opt: whether the runner supports a --tool-opt (-O) option, which
263      can be given multiple times and is passed on to the underlying tool
264      that the runner wraps.
265
266    - file: whether the runner supports a --file option, which specifies
267      exactly the file that should be used to flash, overriding any default
268      discovered in the build directory.
269
270    - hide_load_files: whether the elf/hex/bin file arguments should be hidden.
271    '''
272
273    commands: Set[str] = field(default_factory=lambda: set(_RUNNERCAPS_COMMANDS))
274    dev_id: bool = False
275    flash_addr: bool = False
276    erase: bool = False
277    reset: bool = False
278    extload: bool = False
279    tool_opt: bool = False
280    file: bool = False
281    hide_load_files: bool = False
282
283    def __post_init__(self):
284        if not self.commands.issubset(_RUNNERCAPS_COMMANDS):
285            raise ValueError(f'{self.commands=} contains invalid command')
286
287
288def _missing_cap(cls: Type['ZephyrBinaryRunner'], option: str) -> NoReturn:
289    # Helper function that's called when an option was given on the
290    # command line that corresponds to a missing capability in the
291    # runner class cls.
292
293    raise ValueError(f"{cls.name()} doesn't support {option} option")
294
295
296class FileType(Enum):
297    OTHER = 0
298    HEX = 1
299    BIN = 2
300    ELF = 3
301
302
303class RunnerConfig(NamedTuple):
304    '''Runner execution-time configuration.
305
306    This is a common object shared by all runners. Individual runners
307    can register specific configuration options using their
308    do_add_parser() hooks.
309    '''
310    build_dir: str                  # application build directory
311    board_dir: str                  # board definition directory
312    elf_file: Optional[str]         # zephyr.elf path, or None
313    exe_file: Optional[str]         # zephyr.exe path, or None
314    hex_file: Optional[str]         # zephyr.hex path, or None
315    bin_file: Optional[str]         # zephyr.bin path, or None
316    uf2_file: Optional[str]         # zephyr.uf2 path, or None
317    file: Optional[str]             # binary file path (provided by the user), or None
318    file_type: Optional[FileType] = FileType.OTHER  # binary file type
319    gdb: Optional[str] = None       # path to a usable gdb
320    openocd: Optional[str] = None   # path to a usable openocd
321    openocd_search: List[str] = []  # add these paths to the openocd search path
322
323
324_YN_CHOICES = ['Y', 'y', 'N', 'n', 'yes', 'no', 'YES', 'NO']
325
326
327class _DTFlashAction(argparse.Action):
328
329    def __call__(self, parser, namespace, values, option_string=None):
330        if values.lower().startswith('y'):
331            namespace.dt_flash = True
332        else:
333            namespace.dt_flash = False
334
335
336class _ToggleAction(argparse.Action):
337
338    def __call__(self, parser, args, ignored, option):
339        setattr(args, self.dest, not option.startswith('--no-'))
340
341class DeprecatedAction(argparse.Action):
342
343    def __call__(self, parser, namespace, values, option_string=None):
344        _logger.warning(f'Argument {self.option_strings[0]} is deprecated' +
345                        (f' for your runner {self._cls.name()}'  if self._cls is not None else '') +
346                        f', use {self._replacement} instead.')
347        setattr(namespace, self.dest, values)
348
349def depr_action(*args, cls=None, replacement=None, **kwargs):
350    action = DeprecatedAction(*args, **kwargs)
351    setattr(action, '_cls', cls)
352    setattr(action, '_replacement', replacement)
353    return action
354
355class ZephyrBinaryRunner(abc.ABC):
356    '''Abstract superclass for binary runners (flashers, debuggers).
357
358    **Note**: this class's API has changed relatively rarely since it
359    as added, but it is not considered a stable Zephyr API, and may change
360    without notice.
361
362    With some exceptions, boards supported by Zephyr must provide
363    generic means to be flashed (have a Zephyr firmware binary
364    permanently installed on the device for running) and debugged
365    (have a breakpoint debugger and program loader on a host
366    workstation attached to a running target).
367
368    This is supported by four top-level commands managed by the
369    Zephyr build system:
370
371    - 'flash': flash a previously configured binary to the board,
372      start execution on the target, then return.
373
374    - 'debug': connect to the board via a debugging protocol, program
375      the flash, then drop the user into a debugger interface with
376      symbol tables loaded from the current binary, and block until it
377      exits.
378
379    - 'debugserver': connect via a board-specific debugging protocol,
380      then reset and halt the target. Ensure the user is now able to
381      connect to a debug server with symbol tables loaded from the
382      binary.
383
384    - 'attach': connect to the board via a debugging protocol, then drop
385      the user into a debugger interface with symbol tables loaded from
386      the current binary, and block until it exits. Unlike 'debug', this
387      command does not program the flash.
388
389    This class provides an API for these commands. Every subclass is
390    called a 'runner' for short. Each runner has a name (like
391    'pyocd'), and declares commands it can handle (like
392    'flash'). Boards (like 'nrf52dk/nrf52832') declare which runner(s)
393    are compatible with them to the Zephyr build system, along with
394    information on how to configure the runner to work with the board.
395
396    The build system will then place enough information in the build
397    directory to create and use runners with this class's create()
398    method, which provides a command line argument parsing API. You
399    can also create runners by instantiating subclasses directly.
400
401    In order to define your own runner, you need to:
402
403    1. Define a ZephyrBinaryRunner subclass, and implement its
404       abstract methods. You may need to override capabilities().
405
406    2. Make sure the Python module defining your runner class is
407       imported, e.g. by editing this package's __init__.py (otherwise,
408       get_runners() won't work).
409
410    3. Give your runner's name to the Zephyr build system in your
411       board's board.cmake.
412
413    Additional advice:
414
415    - If you need to import any non-standard-library modules, make sure
416      to catch ImportError and defer complaints about it to a RuntimeError
417      if one is missing. This avoids affecting users that don't require your
418      runner, while still making it clear what went wrong to users that do
419      require it that don't have the necessary modules installed.
420
421    - If you need to ask the user something (e.g. using input()), do it
422      in your create() classmethod, not do_run(). That ensures your
423      __init__() really has everything it needs to call do_run(), and also
424      avoids calling input() when not instantiating within a command line
425      application.
426
427    - Use self.logger to log messages using the standard library's
428      logging API; your logger is named "runner.<your-runner-name()>"
429
430    For command-line invocation from the Zephyr build system, runners
431    define their own argparse-based interface through the common
432    add_parser() (and runner-specific do_add_parser() it delegates
433    to), and provide a way to create instances of themselves from
434    a RunnerConfig and parsed runner-specific arguments via create().
435
436    Runners use a variety of host tools and configuration values, the
437    user interface to which is abstracted by this class. Each runner
438    subclass should take any values it needs to execute one of these
439    commands in its constructor.  The actual command execution is
440    handled in the run() method.'''
441
442    def __init__(self, cfg: RunnerConfig):
443        '''Initialize core runner state.'''
444
445        self.cfg = cfg
446        '''RunnerConfig for this instance.'''
447
448        self.logger = logging.getLogger('runners.{}'.format(self.name()))
449        '''logging.Logger for this instance.'''
450
451    @staticmethod
452    def get_runners() -> List[Type['ZephyrBinaryRunner']]:
453        '''Get a list of all currently defined runner classes.'''
454        def inheritors(klass):
455            subclasses = set()
456            work = [klass]
457            while work:
458                parent = work.pop()
459                for child in parent.__subclasses__():
460                    if child not in subclasses:
461                        if not isabstract(child):
462                            subclasses.add(child)
463                        work.append(child)
464            return subclasses
465
466        return inheritors(ZephyrBinaryRunner)
467
468    @classmethod
469    @abc.abstractmethod
470    def name(cls) -> str:
471        '''Return this runner's user-visible name.
472
473        When choosing a name, pick something short and lowercase,
474        based on the name of the tool (like openocd, jlink, etc.) or
475        the target architecture/board (like xtensa etc.).'''
476
477    @classmethod
478    def capabilities(cls) -> RunnerCaps:
479        '''Returns a RunnerCaps representing this runner's capabilities.
480
481        This implementation returns the default capabilities.
482
483        Subclasses should override appropriately if needed.'''
484        return RunnerCaps()
485
486    @classmethod
487    def add_parser(cls, parser):
488        '''Adds a sub-command parser for this runner.
489
490        The given object, parser, is a sub-command parser from the
491        argparse module. For more details, refer to the documentation
492        for argparse.ArgumentParser.add_subparsers().
493
494        The lone common optional argument is:
495
496        * --dt-flash (if the runner capabilities includes flash_addr)
497
498        Runner-specific options are added through the do_add_parser()
499        hook.'''
500        # Unfortunately, the parser argument's type is not documented
501        # in typeshed, so we can't type annotate much here.
502
503        # Common options that depend on runner capabilities. If a
504        # capability is not supported, the option string or strings
505        # are added anyway, to prevent an individual runner class from
506        # using them to mean something else.
507        caps = cls.capabilities()
508
509        if caps.dev_id:
510            parser.add_argument('-i', '--dev-id',
511                                dest='dev_id',
512                                help=cls.dev_id_help())
513        else:
514            parser.add_argument('-i', '--dev-id', help=argparse.SUPPRESS)
515
516        if caps.flash_addr:
517            parser.add_argument('--dt-flash', default='n', choices=_YN_CHOICES,
518                                action=_DTFlashAction,
519                                help='''If 'yes', try to use flash address
520                                information from devicetree when flash
521                                addresses are unknown (e.g. when flashing a .bin)''')
522        else:
523            parser.add_argument('--dt-flash', help=argparse.SUPPRESS)
524
525        if caps.file:
526            parser.add_argument('-f', '--file',
527                                dest='file',
528                                help="path to binary file")
529            parser.add_argument('-t', '--file-type',
530                                dest='file_type',
531                                help="type of binary file")
532        else:
533            parser.add_argument('-f', '--file', help=argparse.SUPPRESS)
534            parser.add_argument('-t', '--file-type', help=argparse.SUPPRESS)
535
536        if caps.hide_load_files:
537            parser.add_argument('--elf-file', help=argparse.SUPPRESS)
538            parser.add_argument('--hex-file', help=argparse.SUPPRESS)
539            parser.add_argument('--bin-file', help=argparse.SUPPRESS)
540        else:
541            parser.add_argument('--elf-file',
542                                metavar='FILE',
543                                action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
544                                help='path to zephyr.elf' if not caps.file else 'Deprecated, use -f/--file instead.')
545            parser.add_argument('--hex-file',
546                                metavar='FILE',
547                                action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
548                                help='path to zephyr.hex' if not caps.file else 'Deprecated, use -f/--file instead.')
549            parser.add_argument('--bin-file',
550                                metavar='FILE',
551                                action=(partial(depr_action, cls=cls, replacement='-f/--file') if caps.file else None),
552                                help='path to zephyr.bin' if not caps.file else 'Deprecated, use -f/--file instead.')
553
554        parser.add_argument('--erase', '--no-erase', nargs=0,
555                            action=_ToggleAction,
556                            help=("mass erase flash before loading, or don't. "
557                                  "Default action depends on each specific runner."
558                                  if caps.erase else argparse.SUPPRESS))
559
560        parser.add_argument('--reset', '--no-reset', nargs=0,
561                            action=_ToggleAction,
562                            help=("reset device after flashing, or don't. "
563                                  "Default action depends on each specific runner."
564                                  if caps.reset else argparse.SUPPRESS))
565
566        parser.add_argument('--extload', dest='extload',
567                            help=(cls.extload_help() if caps.extload
568                                  else argparse.SUPPRESS))
569
570        parser.add_argument('-O', '--tool-opt', dest='tool_opt',
571                            default=[], action='append',
572                            help=(cls.tool_opt_help() if caps.tool_opt
573                                  else argparse.SUPPRESS))
574
575        # Runner-specific options.
576        cls.do_add_parser(parser)
577
578    @classmethod
579    @abc.abstractmethod
580    def do_add_parser(cls, parser):
581        '''Hook for adding runner-specific options.'''
582
583    @classmethod
584    def create(cls, cfg: RunnerConfig,
585               args: argparse.Namespace) -> 'ZephyrBinaryRunner':
586        '''Create an instance from command-line arguments.
587
588        - ``cfg``: runner configuration (pass to superclass __init__)
589        - ``args``: arguments parsed from execution environment, as
590          specified by ``add_parser()``.'''
591        caps = cls.capabilities()
592        if args.dev_id and not caps.dev_id:
593            _missing_cap(cls, '--dev-id')
594        if args.dt_flash and not caps.flash_addr:
595            _missing_cap(cls, '--dt-flash')
596        if args.erase and not caps.erase:
597            _missing_cap(cls, '--erase')
598        if args.reset and not caps.reset:
599            _missing_cap(cls, '--reset')
600        if args.extload and not caps.extload:
601            _missing_cap(cls, '--extload')
602        if args.tool_opt and not caps.tool_opt:
603            _missing_cap(cls, '--tool-opt')
604        if args.file and not caps.file:
605            _missing_cap(cls, '--file')
606        if args.file_type and not args.file:
607            raise ValueError("--file-type requires --file")
608        if args.file_type and not caps.file:
609            _missing_cap(cls, '--file-type')
610
611        ret = cls.do_create(cfg, args)
612        if args.erase:
613            ret.logger.info('mass erase requested')
614        if args.reset:
615            ret.logger.info('reset after flashing requested')
616        return ret
617
618    @classmethod
619    @abc.abstractmethod
620    def do_create(cls, cfg: RunnerConfig,
621                  args: argparse.Namespace) -> 'ZephyrBinaryRunner':
622        '''Hook for instance creation from command line arguments.'''
623
624    @staticmethod
625    def get_flash_address(args: argparse.Namespace,
626                          build_conf: BuildConfiguration,
627                          default: int = 0x0) -> int:
628        '''Helper method for extracting a flash address.
629
630        If args.dt_flash is true, returns the address obtained from
631        ZephyrBinaryRunner.flash_address_from_build_conf(build_conf).
632
633        Otherwise (when args.dt_flash is False), the default value is
634        returned.'''
635        if args.dt_flash:
636            return ZephyrBinaryRunner.flash_address_from_build_conf(build_conf)
637        else:
638            return default
639
640    @staticmethod
641    def flash_address_from_build_conf(build_conf: BuildConfiguration):
642        '''If CONFIG_HAS_FLASH_LOAD_OFFSET is n in build_conf,
643        return the CONFIG_FLASH_BASE_ADDRESS value. Otherwise, return
644        CONFIG_FLASH_BASE_ADDRESS + CONFIG_FLASH_LOAD_OFFSET.
645        '''
646        if build_conf.getboolean('CONFIG_HAS_FLASH_LOAD_OFFSET'):
647            return (build_conf['CONFIG_FLASH_BASE_ADDRESS'] +
648                    build_conf['CONFIG_FLASH_LOAD_OFFSET'])
649        else:
650            return build_conf['CONFIG_FLASH_BASE_ADDRESS']
651
652    def run(self, command: str, **kwargs):
653        '''Runs command ('flash', 'debug', 'debugserver', 'attach').
654
655        This is the main entry point to this runner.'''
656        caps = self.capabilities()
657        if command not in caps.commands:
658            raise ValueError('runner {} does not implement command {}'.format(
659                self.name(), command))
660        self.do_run(command, **kwargs)
661
662    @abc.abstractmethod
663    def do_run(self, command: str, **kwargs):
664        '''Concrete runner; run() delegates to this. Implement in subclasses.
665
666        In case of an unsupported command, raise a ValueError.'''
667
668    @property
669    def build_conf(self) -> BuildConfiguration:
670        '''Get a BuildConfiguration for the build directory.'''
671        if not hasattr(self, '_build_conf'):
672            self._build_conf = BuildConfiguration(self.cfg.build_dir)
673        return self._build_conf
674
675    @property
676    def sysbuild_conf(self) -> SysbuildConfiguration:
677        '''Get a SysbuildConfiguration for the sysbuild directory.'''
678        if not hasattr(self, '_sysbuild_conf'):
679            self._sysbuild_conf = SysbuildConfiguration(os.path.dirname(self.cfg.build_dir))
680        return self._sysbuild_conf
681
682    @property
683    def thread_info_enabled(self) -> bool:
684        '''Returns True if self.build_conf has
685        CONFIG_DEBUG_THREAD_INFO enabled.
686        '''
687        return self.build_conf.getboolean('CONFIG_DEBUG_THREAD_INFO')
688
689    @classmethod
690    def dev_id_help(cls) -> str:
691        ''' Get the ArgParse help text for the --dev-id option.'''
692        return '''Device identifier. Use it to select
693                  which debugger, device, node or instance to
694                  target when multiple ones are available or
695                  connected.'''
696
697    @classmethod
698    def extload_help(cls) -> str:
699        ''' Get the ArgParse help text for the --extload option.'''
700        return '''External loader to be used by stm32cubeprogrammer
701                  to program the targeted external memory.
702                  The runner requires the external loader (*.stldr) filename.
703                  This external loader (*.stldr) must be located within
704                  STM32CubeProgrammer/bin/ExternalLoader directory.'''
705
706    @classmethod
707    def tool_opt_help(cls) -> str:
708        ''' Get the ArgParse help text for the --tool-opt option.'''
709        return '''Option to pass on to the underlying tool used
710                  by this runner. This can be given multiple times;
711                  the resulting arguments will be given to the tool
712                  in the order they appear on the command line.'''
713
714    @staticmethod
715    def require(program: str, path: Optional[str] = None) -> str:
716        '''Require that a program is installed before proceeding.
717
718        :param program: name of the program that is required,
719                        or path to a program binary.
720        :param path:    PATH where to search for the program binary.
721                        By default check on the system PATH.
722
723        If ``program`` is an absolute path to an existing program
724        binary, this call succeeds. Otherwise, try to find the program
725        by name on the system PATH or in the given PATH, if provided.
726
727        If the program can be found, its path is returned.
728        Otherwise, raises MissingProgram.'''
729        ret = shutil.which(program, path=path)
730        if ret is None:
731            raise MissingProgram(program)
732        return ret
733
734    def run_server_and_client(self, server, client, **kwargs):
735        '''Run a server that ignores SIGINT, and a client that handles it.
736
737        This routine portably:
738
739        - creates a Popen object for the ``server`` command which ignores
740          SIGINT
741        - runs ``client`` in a subprocess while temporarily ignoring SIGINT
742        - cleans up the server after the client exits.
743        - the keyword arguments, if any, will be passed down to both server and
744          client subprocess calls
745
746        It's useful to e.g. open a GDB server and client.'''
747        server_proc = self.popen_ignore_int(server, **kwargs)
748        try:
749            self.run_client(client, **kwargs)
750        finally:
751            server_proc.terminate()
752            server_proc.wait()
753
754    def run_client(self, client, **kwargs):
755        '''Run a client that handles SIGINT.'''
756        previous = signal.signal(signal.SIGINT, signal.SIG_IGN)
757        try:
758            self.check_call(client, **kwargs)
759        finally:
760            signal.signal(signal.SIGINT, previous)
761
762    def _log_cmd(self, cmd: List[str]):
763        escaped = ' '.join(shlex.quote(s) for s in cmd)
764        if not _DRY_RUN:
765            self.logger.debug(escaped)
766        else:
767            self.logger.info(escaped)
768
769    def call(self, cmd: List[str], **kwargs) -> int:
770        '''Subclass subprocess.call() wrapper.
771
772        Subclasses should use this method to run command in a
773        subprocess and get its return code, rather than
774        using subprocess directly, to keep accurate debug logs.
775        '''
776        self._log_cmd(cmd)
777        if _DRY_RUN:
778            return 0
779        return subprocess.call(cmd, **kwargs)
780
781    def check_call(self, cmd: List[str], **kwargs):
782        '''Subclass subprocess.check_call() wrapper.
783
784        Subclasses should use this method to run command in a
785        subprocess and check that it executed correctly, rather than
786        using subprocess directly, to keep accurate debug logs.
787        '''
788        self._log_cmd(cmd)
789        if _DRY_RUN:
790            return
791        subprocess.check_call(cmd, **kwargs)
792
793    def check_output(self, cmd: List[str], **kwargs) -> bytes:
794        '''Subclass subprocess.check_output() wrapper.
795
796        Subclasses should use this method to run command in a
797        subprocess and check that it executed correctly, rather than
798        using subprocess directly, to keep accurate debug logs.
799        '''
800        self._log_cmd(cmd)
801        if _DRY_RUN:
802            return b''
803        return subprocess.check_output(cmd, **kwargs)
804
805    def popen_ignore_int(self, cmd: List[str], **kwargs) -> subprocess.Popen:
806        '''Spawn a child command, ensuring it ignores SIGINT.
807
808        The returned subprocess.Popen object must be manually terminated.'''
809        cflags = 0
810        preexec = None
811        system = platform.system()
812
813        if system == 'Windows':
814            # We can't type check this line on Unix operating systems:
815            # mypy thinks the subprocess module has no such attribute.
816            cflags |= subprocess.CREATE_NEW_PROCESS_GROUP  # type: ignore
817        elif system in {'Linux', 'Darwin'}:
818            # We can't type check this on Windows for the same reason.
819            preexec = os.setsid # type: ignore
820
821        self._log_cmd(cmd)
822        if _DRY_RUN:
823            return _DebugDummyPopen()  # type: ignore
824
825        return subprocess.Popen(cmd, creationflags=cflags, preexec_fn=preexec, **kwargs)
826
827    def ensure_output(self, output_type: str) -> None:
828        '''Ensure self.cfg has a particular output artifact.
829
830        For example, ensure_output('bin') ensures that self.cfg.bin_file
831        refers to an existing file. Errors out if it's missing or undefined.
832
833        :param output_type: string naming the output type
834        '''
835        output_file = getattr(self.cfg, f'{output_type}_file', None)
836
837        if output_file is None:
838            err = f'{output_type} file location is unknown.'
839        elif not os.path.isfile(output_file):
840            err = f'{output_file} does not exist.'
841        else:
842            return
843
844        if output_type in ('elf', 'hex', 'bin', 'uf2'):
845            err += f' Try enabling CONFIG_BUILD_OUTPUT_{output_type.upper()}.'
846
847        # RuntimeError avoids a stack trace saved in run_common.
848        raise RuntimeError(err)
849