Source code for tbot.machine.board.uboot

# tbot, Embedded Automation Tool
# Copyright (C) 2019  Harald Seiler
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import contextlib
import re
import time
import typing

import tbot
from .. import shell, machine, channel
from ..linux import special, util


class UBootStartupEvent(tbot.log.EventIO):
    def __init__(self, ub: machine.Machine) -> None:
        self.ub = ub
        super().__init__(
            ["board", "uboot", ub.name],
            tbot.log.c("UBOOT").bold + f" ({ub.name})",
            verbosity=tbot.log.Verbosity.QUIET,
        )

        self.verbosity = tbot.log.Verbosity.STDOUT
        self.prefix = "   <> "

    def close(self) -> None:
        setattr(self.ub, "bootlog", self.getvalue())
        self.data["output"] = self.getvalue()
        super().close()


class UbootStartup(machine.Machine):
    _uboot_init_event: typing.Optional[tbot.log.EventIO] = None
    _timeout_start: typing.Optional[float] = None

    boot_timeout: typing.Optional[float] = None
    """
    Maximum time from power-on to U-Boot shell.

    If tbot can't reach the U-Boot shell during this time, an exception will be thrown.
    """

    def _uboot_startup_event(self) -> tbot.log.EventIO:
        if self._uboot_init_event is None:
            self._uboot_init_event = UBootStartupEvent(self)

            self._timeout_start = time.monotonic()

        return self._uboot_init_event


[docs] class UBootAutobootIntercept(machine.Initializer, UbootStartup): """ Machine-initializer to intercept U-Boot autobooting. The default settings for this class should work for most cases, but if a custom autoboot prompt was configured, or a special key sequence is necessary, you will have to adjust this here. **Example**: .. code-block:: python import re class MyUBoot( board.Connector, board.UBootAutobootIntercept, board.UBootShell, ): autoboot_prompt = tbot.Re("Press DEL 4 times.{0,100}", re.DOTALL) autoboot_keys = "\\x7f\\x7f\\x7f\\x7f" """ autoboot_prompt: typing.Optional[channel.channel.ConvenientSearchString] = ( re.compile(b"autoboot:\\s{0,5}\\d{0,3}\\s{0,3}.{0,80}") ) """ Autoboot prompt to wait for. """ autoboot_keys: typing.Union[str, bytes] = "\r" """ Keys to press as soon as autoboot prompt is detected. """ @property def uboot_crash_death_strings(self) -> typing.List[str]: """ Strings in the output that mean U-Boot crashed. They are added to the channel when the machine is initialized, and the channel raises a :py:class:`~tbot.machine.channel.DeathStringException` when one of them appears. None by default. A board class sets them as a class attribute, or as a property when they are only known at run time. .. versionadded:: UNRELEASED """ return [] @contextlib.contextmanager def _init_machine(self) -> typing.Iterator: for death_string in self.uboot_crash_death_strings: self.ch.add_death_string(death_string) if self.autoboot_prompt is not None: with self.ch.with_stream(self._uboot_startup_event()): timeout = None if self.boot_timeout is not None: assert self._timeout_start is not None timeout = self.boot_timeout - ( time.monotonic() - self._timeout_start ) try: self.ch.read_until_prompt( prompt=self.autoboot_prompt, timeout=timeout ) except TimeoutError: raise TimeoutError( "U-Boot autoboot prompt did not show up in time" ) from None self.ch.send(self.autoboot_keys, _ignore_blacklist=True) yield None
_hush_find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.ASCII).search def _hush_quote(s: str) -> str: if not s: return '""' if _hush_find_unsafe(s) is None: return s # - Quote \ (inside quotes) as \\ # - Quote single quotes using a \ (outside the original quotes). # # Example: $'\b is quoted as '$'\''\\b' s = s.replace("\\", "\\\\").replace("'", "'\\''") return "'" + s + "'" ArgTypes = typing.Union[str, special.Special]
[docs] class UBootShell(shell.Shell, UbootStartup): """ U-Boot shell. The interface of this shell was designed to be close to the :ref:`Linux shell <linux-shells>` design. This means that U-Boot shells also provide - :py:meth:`ub.escape() <tbot.machine.board.UBootShell.escape>` - Escape args for the U-Boot shell. - :py:meth:`ub.exec0() <tbot.machine.board.UBootShell.exec0>` - Run command and ensure it succeeded. - :py:meth:`ub.exec() <tbot.machine.board.UBootShell.exec>` - Run command and return output and return code. - :py:meth:`ub.test() <tbot.machine.board.UBootShell.test>` - Run command and return boolean whether it succeeded. - :py:meth:`ub.env() <tbot.machine.board.UBootShell.env>` - Get/Set environment variables. - :py:meth:`ub.interactive() <tbot.machine.board.UBootShell.interactive>` - Start an interactive session for this machine. There is also the special :py:meth:`ub.boot() <tbot.machine.board.UBootShell.boot>` which will boot a payload and return the machine's channel, for use in a machine for the booted payload. """ prompt: typing.Union[str, bytes] = "U-Boot> " """ Prompt which was configured for U-Boot. Commonly ``"U-Boot> "``, ``"=> "``, or ``"U-Boot# "``. .. warning:: **Don't forget the trailing space, if your prompt has one!** """ bootlog: str """Transcript of console output during boot.""" @contextlib.contextmanager def _init_shell(self) -> typing.Iterator: with self._uboot_startup_event() as ev, self.ch.with_stream(ev): self.ch.prompt = ( self.prompt.encode("utf-8") if isinstance(self.prompt, str) else self.prompt ) # Set a blacklist of control characters. These characters are # known to mess up the state of the U-Boot shell. They are: self.ch._write_blacklist = [ 0x00, # NUL | Null 0x01, # SOH | Start of Heading 0x02, # STX | Start of Text 0x03, # ETX | End of Text / Interrupt 0x04, # EOT | End of Transmission 0x05, # ENQ | Enquiry 0x06, # ACK | Acknowledge 0x07, # BEL | Bell, Alert 0x08, # BS | Backspace 0x09, # HT | Character Tabulation, Horizontal Tabulation 0x0B, # VT | Line Tabulation, Vertical Tabulation 0x0C, # FF | Form Feed 0x0E, # SO | Shift Out 0x0F, # SI | Shift In 0x10, # DLE | Data Link Escape 0x11, # DC1 | Device Control One (XON) 0x12, # DC2 | Device Control Two 0x13, # DC3 | Device Control Three (XOFF) 0x14, # DC4 | Device Control Four 0x15, # NAK | Negative Acknowledge 0x16, # SYN | Synchronous Idle 0x17, # ETB | End of Transmission Block 0x18, # CAN | Cancel 0x1A, # SUB | Substitute / Suspend Process 0x1B, # ESC | Escape 0x1C, # FS | File Separator 0x7F, # DEL | Delete ] while True: if self.boot_timeout is not None: assert self._timeout_start is not None if (time.monotonic() - self._timeout_start) > self.boot_timeout: raise TimeoutError("U-Boot did not reach shell in time") try: self.ch.read_until_prompt(timeout=0.5) break except TimeoutError: self.ch.sendintr() time.sleep(0.5) yield None def _sendline_read_echo(self, ch: channel.Channel, cmd: str) -> None: """ Send a command line to U-Boot and read back its echo. U-Boot echoes the Enter key as ``"\\r\\n"`` when it is built with ``CONFIG_CMDLINE_EDITING``. Without it, the simple readline of ``common/cli_readline.c`` answers with ``puts("\\r\\n")`` and the serial uclass puts a ``\\r`` in front of every ``\\n``, so the echo is ``"\\r\\r\\n"``. Read back the command by its length and the echo of the Enter key up to the newline, so that no part of the echo is left in front of the output of the command. """ ch.send(cmd, read_back=True) ch.sendline() ch.readline(lineending="\n")
[docs] def escape(self, *args: ArgTypes) -> str: """Escape a string so it can be used safely on the U-Boot command-line.""" string_args = [] for arg in args: if isinstance(arg, str): # We can't use shlex.quote() here because U-Boot's shell of # course has its own rules for quoting ... string_args.append(_hush_quote(arg)) elif isinstance(arg, special.Special): string_args.append(arg._to_string(self)) else: raise TypeError(f"{type(arg)!r} is not a supported argument type!") return " ".join(string_args)
[docs] def exec(self, *args: ArgTypes) -> typing.Tuple[int, str]: """ Run a command in U-Boot. **Example**: .. code-block:: python retcode, output = ub.exec("version") assert retcode == 0 :rtype: tuple(int, str) :returns: A tuple with the return code of the command and its console output. The output will also contain a trailing newline in most cases. """ cmd = self.escape(*args) # There is an ugly ugly problem with no great solution: The `crc32` # command in U-Boot prints the string `=> ` as part of its output. # This is a commonly used prompt string which means that tbot gets # completely thrown off by this situation. As a stopgap solution, # let's special case this here to make people's lives a bit easier... if args[0] == "crc32" and self.ch.prompt in ("=> ", b"=> "): tbot.log.warning( "Applying workaround for `crc32` command in U-Boot!\n" + " See https://github.com/rahix/tbot/issues/111 for details." ) override_prompt = "\n=> " else: override_prompt = None with tbot.log_event.command(self.name, cmd) as ev: self._sendline_read_echo(self.ch, cmd) with self.ch.with_prompt(override_prompt): with self.ch.with_stream(ev, show_prompt=False): out = self.ch.read_until_prompt(prompt=override_prompt) if override_prompt == "\n=> ": # The overridden prompt ate the trailing '\n' ev.write("\n") out += "\n" ev.data["stdout"] = out self._sendline_read_echo(self.ch, "echo $?") retcode_str = self.ch.read_until_prompt() try: retcode = int(retcode_str) except ValueError: raise tbot.error.InvalidRetcodeError(self, retcode_str) from None return (retcode, out)
[docs] def exec0(self, *args: ArgTypes) -> str: """ Run a command and assert its return code to be 0. **Example**: .. code-block:: python output = ub.exec0("version") # This will raise an exception! ub.exec0("false") :rtype: str :returns: The command's console output. It will also contain a trailing newline in most cases. """ retcode, out = self.exec(*args) if retcode != 0: raise tbot.error.CommandFailure(self, args, repr=self.escape(*args)) return out
[docs] def test(self, *args: ArgTypes) -> bool: """ Run a command and return a boolean value whether it succeeded. **Example**: .. code-block:: python if ub.test("true"): tbot.log.message("Is correct") :rtype: bool :returns: Boolean representation of commands success. ``True`` if return code was ``0``, ``False`` otherwise. """ retcode, _ = self.exec(*args) return retcode == 0
[docs] def env(self, var: str, value: typing.Optional[ArgTypes] = None) -> str: """ Get or set an environment variable. **Example**: .. code-block:: python # Get the value of a var value = ub.env("bootcmd") # Set the value of a var lnx.env("bootargs", "loglevel=7") :param str var: Environment variable name. :param str value: Optional value to set the variable to. :rtype: str :returns: Current (new) value of the environment variable. """ if value is not None: self.exec0("setenv", var, value) # Use `printenv var` instead of `echo "$var"` because some values would # otherwise result in broken expansion. output = self.exec0("printenv", var) # `output` contains "<varname>=<value>\n" so slice off the variable # name and trailing newline. return output[len(var) + 1 : -1]
[docs] def boot(self, *args: ArgTypes) -> channel.Channel: """ Boot a payload from U-Boot. This method will run the given command and expects it to start booting a payload. ``ub.boot()`` will then return the channel so a new machine can be built on top of it for the booted payload. **Example**: .. code-block:: python ub.env("bootargs", "loglevel=7") ch = ub.boot("bootm", "0x10000000") :rtype: tbot.machine.channel.Channel """ cmd = self.escape(*args) with tbot.log_event.command(self.name, cmd): self._sendline_read_echo(self.ch, cmd) return self.ch.take()
[docs] def interactive(self) -> None: """ Start an interactive session on this machine. This method will connect tbot's stdio to the machine's channel so you can interactively run commands. This method is used by the ``interactive_uboot`` testcase. """ tbot.log.message(f"Entering interactive shell...") # It is important to send a space before the newline. Otherwise U-Boot # will reexecute the last command which we definitely do not want here. self.ch.sendline(" ") self.ch.attach_interactive() print("") self.ch.sendline(" ") try: self.ch.read_until_prompt(timeout=0.5) except TimeoutError: raise tbot.error.MachineError( "Failed to reacquire U-Boot after interactive session!" ) tbot.log.message("Exiting interactive shell ...")
[docs] @contextlib.contextmanager def run(self, *args: ArgTypes) -> typing.Iterator[util.RunCommandProxy]: """ Start an interactive command. .. versionadded:: UNRELEASED """ def cmd_context( proxy_ch: util.RunCommandProxy, ) -> typing.Generator[str, None, typing.Tuple[int, str]]: cmd = self.escape(*args) with contextlib.ExitStack() as cx: ev = cx.enter_context(tbot.log_event.command(self.name, cmd)) self._sendline_read_echo(proxy_ch, cmd) cx.enter_context(proxy_ch.with_stream(ev, show_prompt=False)) assert proxy_ch.prompt is not None, "prompt is missing!" # During the context (before calling terminate), the prompt # string may never appear in the command output. If it does # anyway, raise an Exception. # # The exception type is dynamically created here to capture # some variables from the context. This way, the context knows # of an early exit happening and can behave differently because # of it. early_exit = False class CommandEndedException(util.CommandEndedException): def __init__(self, string: bytes): nonlocal early_exit early_exit = True proxy_ch._pre_terminate() super().__init__(string) def __str__(self) -> str: return f"The interactive command {cmd!r} exited prematurely." with proxy_ch.with_death_string(proxy_ch.prompt, CommandEndedException): yield cmd output = "" if not early_exit: output = proxy_ch.read_until_prompt() ev.data["stdout"] = ev.getvalue() self._sendline_read_echo(proxy_ch, "echo $?") retcode = int(proxy_ch.read_until_prompt()) return (retcode, output) yield from util.RunCommandProxy._ctx( channel=self.ch, context=cmd_context, host=self, args=args )
# Utilities ----- {{{ _ram_base: int @property def ram_base(self) -> int: """ Return the base address of RAM for this U-Boot instance. This address can be used as a safe bet when your testcase needs to store something in RAM. **Example**: .. code-block:: python serverip = # ... filepath = # ... ub.exec0("tftp", hex(ub.ram_base), f"{serverip}:{filepath}") ub.exec0("iminfo", hex(ub.ram_base)) """ try: return self._ram_base except AttributeError: out = self.exec0("bdinfo") match = re.search(r"^-> start\s+= (0x[\dA-Fa-f]+)$", out, re.MULTILINE) if match is None: raise tbot.error.MachineError("RAM base not found in bdinfo output!") self._ram_base = int(match.group(1), 16) return self._ram_base
# }}}