| #! /usr/bin/env python3 |
| |
| # Copyright (c) 2017 Linaro Limited. |
| # Copyright (c) 2017 Open Source Foundries Limited. |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| """Zephyr binary runner core interfaces |
| |
| This provides the core ZephyrBinaryRunner class meant for public use, |
| as well as some other helpers for concrete runner classes. |
| """ |
| |
| import abc |
| import argparse |
| import os |
| import platform |
| import shlex |
| import signal |
| import subprocess |
| |
| |
| def quote_sh_list(cmd): |
| '''Transform a command from list into shell string form.''' |
| fmt = ' '.join('{}' for _ in cmd) |
| args = [shlex.quote(s) for s in cmd] |
| return fmt.format(*args) |
| |
| |
| MAX_PORT = 49151 |
| |
| |
| class NetworkPortHelper: |
| '''Helper class for dealing with local IP network ports.''' |
| |
| def get_unused_ports(self, starting_from): |
| '''Find unused network ports, starting at given values. |
| |
| starting_from is an iterable of ports the caller would like to use. |
| |
| The return value is an iterable of ports, in the same order, using |
| the given values if they were unused, or the next sequentially |
| available unused port otherwise. |
| |
| Ports may be bound between this call's check and actual usage, so |
| callers still need to handle errors involving returned ports.''' |
| start = list(starting_from) |
| used = self._used_now() |
| ret = [] |
| |
| for desired in start: |
| port = desired |
| while port in used: |
| port += 1 |
| if port > MAX_PORT: |
| msg = "ports above {} are in use" |
| raise ValueError(msg.format(desired)) |
| used.add(port) |
| ret.append(port) |
| |
| return ret |
| |
| def _used_now(self): |
| handlers = { |
| 'Windows': self._used_now_windows, |
| 'Linux': self._used_now_linux, |
| 'Darwin': self._used_now_darwin, |
| } |
| handler = handlers[platform.system()] |
| return handler() |
| |
| def _used_now_windows(self): |
| cmd = ['netstat', '-a', '-n', '-p', 'tcp'] |
| return self._parser_windows(cmd) |
| |
| def _used_now_linux(self): |
| cmd = ['ss', '-a', '-n', '-t'] |
| return self._parser_linux(cmd) |
| |
| def _used_now_darwin(self): |
| cmd = ['netstat', '-a', '-n', '-p', 'tcp'] |
| return self._parser_darwin(cmd) |
| |
| def _parser_windows(self, cmd): |
| out = subprocess.check_output(cmd).split(b'\r\n') |
| used_bytes = [x.split()[1].rsplit(b':', 1)[1] for x in out |
| if x.startswith(b' TCP')] |
| return {int(b) for b in used_bytes} |
| |
| def _parser_linux(self, cmd): |
| out = subprocess.check_output(cmd).splitlines()[1:] |
| used_bytes = [s.split()[3].rsplit(b':', 1)[1] for s in out] |
| return {int(b) for b in used_bytes} |
| |
| def _parser_darwin(self, cmd): |
| out = subprocess.check_output(cmd).split(b'\n') |
| used_bytes = [x.split()[3].rsplit(b':', 1)[1] for x in out |
| if x.startswith(b'tcp')] |
| return {int(b) for b in used_bytes} |
| |
| |
| class BuildConfiguration: |
| '''This helper class provides access to build-time configuration. |
| |
| Configuration options can be read as if the object were a dict, |
| either object['CONFIG_FOO'] or object.get('CONFIG_FOO'). |
| |
| Configuration values in .config and generated_dts_board.conf are |
| available.''' |
| |
| def __init__(self, build_dir): |
| self.build_dir = build_dir |
| self.options = {} |
| self._init() |
| |
| def __getitem__(self, item): |
| return self.options[item] |
| |
| def get(self, option, *args): |
| return self.options.get(option, *args) |
| |
| def _init(self): |
| build_z = os.path.join(self.build_dir, 'zephyr') |
| generated = os.path.join(build_z, 'include', 'generated') |
| files = [os.path.join(build_z, '.config'), |
| os.path.join(generated, 'generated_dts_board.conf')] |
| for f in files: |
| self._parse(f) |
| |
| def _parse(self, filename): |
| with open(filename, 'r') as f: |
| for line in f: |
| line = line.strip() |
| if not line or line.startswith('#'): |
| continue |
| option, value = line.split('=', 1) |
| self.options[option] = self._parse_value(value) |
| |
| def _parse_value(self, value): |
| if value.startswith('"') or value.startswith("'"): |
| return value.split() |
| try: |
| return int(value, 0) |
| except ValueError: |
| return value |
| |
| |
| class RunnerCaps: |
| '''This class represents a runner class's capabilities. |
| |
| Each capability is represented as an attribute with the same |
| name. Flag attributes are True or False. |
| |
| Available capabilities: |
| |
| - commands: set of supported commands; default is {'flash', |
| 'debug', 'debugserver'}. |
| |
| - flash_addr: whether the runner supports flashing to an |
| arbitrary address. Default is False. If true, the runner |
| must honor the --dt-flash option. |
| ''' |
| |
| def __init__(self, |
| commands={'flash', 'debug', 'debugserver'}, |
| flash_addr=False): |
| self.commands = commands |
| self.flash_addr = bool(flash_addr) |
| |
| |
| _YN_CHOICES = ['Y', 'y', 'N', 'n', 'yes', 'no', 'YES', 'NO'] |
| |
| |
| class _DTFlashAction(argparse.Action): |
| |
| def __call__(self, parser, namespace, values, option_string=None): |
| if values.lower().startswith('y'): |
| namespace.dt_flash = True |
| else: |
| namespace.dt_flash = False |
| |
| |
| class ZephyrBinaryRunner(abc.ABC): |
| '''Abstract superclass for binary runners (flashers, debuggers). |
| |
| **Note**: these APIs are still evolving, and will change! |
| |
| With some exceptions, boards supported by Zephyr must provide |
| generic means to be flashed (have a Zephyr firmware binary |
| permanently installed on the device for running) and debugged |
| (have a breakpoint debugger and program loader on a host |
| workstation attached to a running target). |
| |
| This is supported by three top-level commands managed by the |
| Zephyr build system: |
| |
| - 'flash': flash a previously configured binary to the board, |
| start execution on the target, then return. |
| |
| - 'debug': connect to the board via a debugging protocol, then |
| drop the user into a debugger interface with symbol tables |
| loaded from the current binary, and block until it exits. |
| |
| - 'debugserver': connect via a board-specific debugging protocol, |
| then reset and halt the target. Ensure the user is now able to |
| connect to a debug server with symbol tables loaded from the |
| binary. |
| |
| This class provides an API for these commands. Every runner has a |
| name (like 'pyocd'), and declares commands it can handle (like |
| 'flash'). Zephyr boards (like 'nrf52_pca10040') declare compatible |
| runner(s) by name to the build system, which makes concrete runner |
| instances to execute commands via this class. |
| |
| If your board can use an existing runner, all you have to do is |
| give its name to the build system. How to do that is out of the |
| scope of this documentation, but use the existing boards as a |
| starting point. |
| |
| If you want to define and use your own runner: |
| |
| 1. Define a ZephyrBinaryRunner subclass, and implement its |
| abstract methods. You may need to override capabilities(). |
| |
| 2. Make sure the Python module defining your runner class is |
| imported, e.g. by editing this package's __init__.py (otherwise, |
| get_runners() won't work). |
| |
| 3. Give your runner's name to the Zephyr build system in your |
| board's build files. |
| |
| For command-line invocation from the Zephyr build system, runners |
| define their own argparse-based interface through the common |
| add_parser() (and runner-specific do_add_parser() it delegates |
| to), and provide a way to create instances of themselves from |
| parsed arguments via create_from_args(). |
| |
| Runners use a variety of target-specific tools and configuration |
| values, the user interface to which is abstracted by this |
| class. Each runner subclass should take any values it needs to |
| execute one of these commands in its constructor. The actual |
| command execution is handled in the run() method.''' |
| |
| def __init__(self, debug=False): |
| self.debug = debug |
| |
| @staticmethod |
| def get_runners(): |
| '''Get a list of all currently defined runner classes.''' |
| return ZephyrBinaryRunner.__subclasses__() |
| |
| @classmethod |
| @abc.abstractmethod |
| def name(cls): |
| '''Return this runner's user-visible name. |
| |
| When choosing a name, pick something short and lowercase, |
| based on the name of the tool (like openocd, jlink, etc.) or |
| the target architecture/board (like xtensa, em-starterkit, |
| etc.).''' |
| |
| @classmethod |
| def capabilities(cls): |
| '''Returns a RunnerCaps representing this runner's capabilities. |
| |
| This implementation returns the default capabilities. |
| |
| Subclasses should override appropriately if needed.''' |
| return RunnerCaps() |
| |
| @classmethod |
| def add_parser(cls, parser): |
| '''Adds a sub-command parser for this runner. |
| |
| The given object, parser, is a sub-command parser from the |
| argparse module. For more details, refer to the documentation |
| for argparse.ArgumentParser.add_subparsers(). |
| |
| The standard (required) arguments are: |
| |
| * --board-dir |
| * --kernel-elf, --kernel-hex, --kernel-bin |
| |
| The standard optional arguments are: |
| |
| * --gdb |
| * --openocd, --openocd-search |
| * --dt-flash (if the runner capabilities includes flash_addr) |
| |
| Runner-specific options are added through the do_add_parser() |
| hook. |
| |
| The single positional argument is "command". This is currently |
| restricted to values 'flash', 'debug', and 'debugserver'.''' |
| # Required options. |
| parser.add_argument('--board-dir', required=True, |
| help='Zephyr board directory') |
| parser.add_argument('--kernel-elf', required=True, |
| help='path to kernel binary in .elf format') |
| parser.add_argument('--kernel-hex', required=True, |
| help='path to kernel binary in .hex format') |
| parser.add_argument('--kernel-bin', required=True, |
| help='path to kernel binary in .bin format') |
| |
| # Optional options. |
| if cls.capabilities().flash_addr: |
| parser.add_argument('--dt-flash', default='n', choices=_YN_CHOICES, |
| action=_DTFlashAction, |
| help='''If 'yes', use configuration |
| generated by device tree (DT) to compute flash |
| addresses.''') |
| parser.add_argument('--gdb', default=None, |
| help='GDB compatible with the target') |
| parser.add_argument('--openocd', default='openocd', |
| help='OpenOCD to use') |
| parser.add_argument('--openocd-search', default=None, |
| help='directory to add to OpenOCD search path') |
| |
| # Runner-specific options. |
| cls.do_add_parser(parser) |
| |
| # The lone positional argument. Note that argparse can't cope |
| # with adding options after the first positional argument, so |
| # this must come last. |
| parser.add_argument('command', |
| choices=['flash', 'debug', 'debugserver'], |
| help='command to run (flash, debug, debugserver)') |
| |
| @classmethod |
| @abc.abstractmethod |
| def do_add_parser(cls, parser): |
| '''Hook for adding runner-specific options. |
| |
| Subclasses **must not** add positional arguments. That is, when |
| calling parser.add_argument(), make sure to begin the argument |
| with '-' so it is interpreted as an option, rather than a |
| positional argument. |
| |
| * OK: parser.add_argument('--my-option') |
| * Not OK: parser.add_argument('my-argument').''' |
| |
| @classmethod |
| @abc.abstractmethod |
| def create_from_args(cls, args): |
| '''Create an instance from command-line arguments. |
| |
| These will have been parsed from the command line according to |
| the specification defined by add_parser().''' |
| |
| @classmethod |
| def get_flash_address(cls, args, build_conf, default=0x0): |
| '''Helper method for extracting a flash address. |
| |
| If args.dt_flash is true, get the address from the |
| BoardConfiguration, build_conf. (If |
| CONFIG_HAS_FLASH_LOAD_OFFSET is n in that configuration, it |
| returns CONFIG_FLASH_BASE_ADDRESS. Otherwise, it returns |
| CONFIG_FLASH_BASE_ADDRESS + CONFIG_FLASH_LOAD_OFFSET.) |
| |
| Otherwise (when args.dt_flash is False), the default value is |
| returned.''' |
| if args.dt_flash: |
| if build_conf['CONFIG_HAS_FLASH_LOAD_OFFSET']: |
| return (build_conf['CONFIG_FLASH_BASE_ADDRESS'] + |
| build_conf['CONFIG_FLASH_LOAD_OFFSET']) |
| else: |
| return build_conf['CONFIG_FLASH_BASE_ADDRESS'] |
| else: |
| return default |
| |
| def run(self, command, **kwargs): |
| '''Runs command ('flash', 'debug', 'debugserver'). |
| |
| This is the main entry point to this runner.''' |
| caps = self.capabilities() |
| if command not in caps.commands: |
| raise ValueError('runner {} does not implement command {}'.format( |
| self.name(), command)) |
| self.do_run(command, **kwargs) |
| |
| @abc.abstractmethod |
| def do_run(self, command, **kwargs): |
| '''Concrete runner; run() delegates to this. Implement in subclasses. |
| |
| In case of an unsupported command, raise a ValueError.''' |
| |
| def run_server_and_client(self, server, client): |
| '''Run a server that ignores SIGINT, and a client that handles it. |
| |
| This routine portably: |
| |
| - creates a Popen object for the `server' command which ignores SIGINT |
| - runs `client' in a subprocess while temporarily ignoring SIGINT |
| - cleans up the server after the client exits. |
| |
| It's useful to e.g. open a GDB server and client.''' |
| server_proc = self.popen_ignore_int(server) |
| previous = signal.signal(signal.SIGINT, signal.SIG_IGN) |
| try: |
| self.check_call(client) |
| finally: |
| signal.signal(signal.SIGINT, previous) |
| server_proc.terminate() |
| server_proc.wait() |
| |
| def check_call(self, cmd): |
| '''Subclass subprocess.check_call() wrapper. |
| |
| Subclasses should use this command to run command in a |
| subprocess and check that it executed correctly, rather than |
| using subprocess directly, to keep accurate debug logs. |
| ''' |
| if self.debug: |
| print(quote_sh_list(cmd)) |
| subprocess.check_call(cmd) |
| |
| def check_output(self, cmd): |
| '''Subclass subprocess.check_output() wrapper. |
| |
| Subclasses should use this command to run command in a |
| subprocess and check that it executed correctly, rather than |
| using subprocess directly, to keep accurate debug logs. |
| ''' |
| if self.debug: |
| print(quote_sh_list(cmd)) |
| return subprocess.check_output(cmd) |
| |
| def popen_ignore_int(self, cmd): |
| '''Spawn a child command, ensuring it ignores SIGINT. |
| |
| The returned subprocess.Popen object must be manually terminated.''' |
| cflags = 0 |
| preexec = None |
| system = platform.system() |
| |
| if system == 'Windows': |
| cflags |= subprocess.CREATE_NEW_PROCESS_GROUP |
| elif system in {'Linux', 'Darwin'}: |
| preexec = os.setsid |
| |
| if self.debug: |
| print(quote_sh_list(cmd)) |
| |
| return subprocess.Popen(cmd, creationflags=cflags, preexec_fn=preexec) |