blob: 72aeac2134f7c89625ff0549567c46ae1c28b88f [file]
############################################################################
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
"""Fastboot transport controller."""
import subprocess
from typing import TYPE_CHECKING, List, Optional
from ntfc.log.logger import logger
if TYPE_CHECKING:
from pathlib import Path
class FastbootController:
"""Controls a device via the fastboot protocol.
Provides device detection, generic command execution, and memory
dump retrieval. Commands that target a specific device are issued
with the ``-s <dev_sn>`` flag automatically.
:param dev_sn: Fastboot device serial number.
"""
def __init__(self, dev_sn: str) -> None:
"""Initialize :class:`FastbootController`.
:param dev_sn: Fastboot device serial number.
"""
self._dev_sn = dev_sn
def is_connected(self) -> bool:
"""Return ``True`` if the device is visible in fastboot mode.
:return: ``True`` when the device serial number appears in the
output of ``fastboot devices``. Always ``False`` for an
empty serial number, since an empty string is trivially a
substring of any output.
"""
if not self._dev_sn:
return False
try:
result = subprocess.run(
["fastboot", "devices"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=10.0,
)
return self._dev_sn in result.stdout.decode(errors="replace")
except subprocess.TimeoutExpired:
logger.debug("fastboot: devices check timed out")
return False
except OSError as exc:
logger.debug(f"fastboot: devices check failed: {exc}")
return False
def memdump(
self, addr: str, size: str, output_path: "Path"
) -> "Optional[Path]":
"""Dump a memory region from the device to a local file.
Executes ``fastboot oem memdump <addr> <size>`` then
``fastboot get_staged <output_path>``.
:param addr: Memory start address (hex string, e.g.
``"0x40000000"``).
:param size: Memory region size (hex string, e.g.
``"0x08000000"``).
:param output_path: Destination file path for the dump.
:return: *output_path* on success, ``None`` on failure.
"""
output_path.parent.mkdir(parents=True, exist_ok=True)
if not self.run(["oem", "memdump", addr, size], timeout=30.0):
logger.warning("fastboot: oem memdump failed")
return None
if not self.run(["get_staged", str(output_path)], timeout=30.0):
logger.warning("fastboot: get_staged failed")
return None
return output_path
def run(self, args: List[str], timeout: float) -> bool:
r"""Run a fastboot subcommand against :attr:`_dev_sn`.
:param args: Arguments appended after ``fastboot -s <dev_sn>``.
:param timeout: Maximum seconds to wait for the command.
:return: ``True`` on exit code 0, ``False`` otherwise.
"""
cmd = ["fastboot", "-s", self._dev_sn] + args
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=timeout,
)
output = result.stdout.decode(errors="replace")
logger.debug(f"fastboot {' '.join(args)}: {output.strip()}")
return result.returncode == 0
except subprocess.TimeoutExpired:
logger.warning(f"fastboot: {' '.join(args)} timed out")
return False
except OSError as exc:
logger.debug(f"fastboot: {' '.join(args)} failed: {exc}")
return False