blob: b98242a3e954ffdaf08d64519750866de8f86192 [file] [log] [blame]
# Copyright 2019 - The Android Open Source Project
#
# Licensed 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.
r"""GoldfishLocalImageLocalInstance class.
Create class that is responsible for creating a local goldfish instance with
local images.
The emulator binary supports two types of environments, Android build system
and SDK. This class runs the emulator in build environment.
- This class uses the prebuilt emulator in ANDROID_EMULATOR_PREBUILTS.
- If the instance requires mixed images, this class uses the OTA tools in
ANDROID_HOST_OUT.
To run this program outside of a build environment, the following setup is
required.
- One of the local tool directories is an unzipped SDK emulator repository,
i.e., sdk-repo-<os>-emulator-<build>.zip.
- If the instance doesn't require mixed images, the local image directory
should be an unzipped SDK image repository, i.e.,
sdk-repo-<os>-system-images-<build>.zip.
- If the instance requires mixed images, the local image directory should
contain both the unzipped update package and the unzipped extra image
package, i.e., <target>-img-<build>.zip and
emu-extra-<os>-system-images-<build>.zip.
- If the instance requires mixed images, one of the local tool directories
should be an unzipped OTA tools package, i.e., otatools.zip.
"""
import logging
import os
import shutil
import subprocess
import sys
from acloud import errors
from acloud.create import base_avd_create
from acloud.internal import constants
from acloud.internal.lib import adb_tools
from acloud.internal.lib import ota_tools
from acloud.internal.lib import utils
from acloud.list import instance
from acloud.public import report
logger = logging.getLogger(__name__)
# Input and output file names
_EMULATOR_BIN_NAME = "emulator"
_SDK_REPO_EMULATOR_DIR_NAME = "emulator"
_SYSTEM_IMAGE_NAME = "system.img"
_SYSTEM_QEMU_IMAGE_NAME = "system-qemu.img"
_NON_MIXED_BACKUP_IMAGE_EXT = ".bak-non-mixed"
_BUILD_PROP_FILE_NAME = "build.prop"
_MISC_INFO_FILE_NAME = "misc_info.txt"
_SYSTEM_QEMU_CONFIG_FILE_NAME = "system-qemu-config.txt"
# Partition names
_SYSTEM_PARTITION_NAME = "system"
_SUPER_PARTITION_NAME = "super"
_VBMETA_PARTITION_NAME = "vbmeta"
# Timeout
_DEFAULT_EMULATOR_TIMEOUT_SECS = 150
_EMULATOR_TIMEOUT_ERROR = "Emulator did not boot within %(timeout)d secs."
_EMU_KILL_TIMEOUT_SECS = 20
_EMU_KILL_TIMEOUT_ERROR = "Emulator did not stop within %(timeout)d secs."
_CONFIRM_RELAUNCH = ("\nGoldfish AVD is already running. \n"
"Enter 'y' to terminate current instance and launch a "
"new instance, enter anything else to exit out[y/N]: ")
_MISSING_EMULATOR_MSG = ("Emulator binary is not found. Check "
"ANDROID_EMULATOR_PREBUILTS in build environment, "
"or set --local-tool to an unzipped SDK emulator "
"repository.")
def _GetImageForLogicalPartition(partition_name, system_image_path, image_dir):
"""Map a logical partition name to an image path.
Args:
partition_name: String. On emulator, the logical partitions include
"system", "vendor", and "product".
system_image_path: String. The path to system image.
image_dir: String. The directory containing the other images.
Returns:
system_image_path if the partition is "system".
Otherwise, this method returns the path under image_dir.
Raises
errors.GetLocalImageError if the image does not exist.
"""
if partition_name == _SYSTEM_PARTITION_NAME:
image_path = system_image_path
else:
image_path = os.path.join(image_dir, partition_name + ".img")
if not os.path.isfile(image_path):
raise errors.GetLocalImageError(
"Cannot find image for logical partition %s" % partition_name)
return image_path
def _GetImageForPhysicalPartition(partition_name, super_image_path,
vbmeta_image_path, image_dir):
"""Map a physical partition name to an image path.
Args:
partition_name: String. On emulator, the physical partitions include
"super" and "vbmeta".
super_image_path: String. The path to super image.
vbmeta_image_path: String. The path to vbmeta image.
image_dir: String. The directory containing the other images.
Returns:
super_image_path if the partition is "super".
vbmeta_image_path if the partition is "vbmeta".
Otherwise, this method returns the path under image_dir.
Raises:
errors.GetLocalImageError if the image does not exist.
"""
if partition_name == _SUPER_PARTITION_NAME:
image_path = super_image_path
elif partition_name == _VBMETA_PARTITION_NAME:
image_path = vbmeta_image_path
else:
image_path = os.path.join(image_dir, partition_name + ".img")
if not os.path.isfile(image_path):
raise errors.GetLocalImageError(
"Unexpected physical partition: %s" % partition_name)
return image_path
class GoldfishLocalImageLocalInstance(base_avd_create.BaseAVDCreate):
"""Create class for a local image local instance emulator."""
def _CreateAVD(self, avd_spec, no_prompts):
"""Create the AVD.
Args:
avd_spec: AVDSpec object that provides the local image directory.
no_prompts: Boolean, True to skip all prompts.
Returns:
A Report instance.
Raises:
errors.GetSdkRepoPackageError if emulator binary is not found.
errors.GetLocalImageError if the local image directory does not
contain required files.
errors.CreateError if an instance exists and cannot be deleted.
errors.CheckPathError if OTA tools are not found.
"""
if not utils.IsSupportedPlatform(print_warning=True):
result_report = report.Report(command="create")
result_report.SetStatus(report.Status.FAIL)
return result_report
emulator_path = self._FindEmulatorBinary(avd_spec.local_tool_dirs)
emulator_path = os.path.abspath(emulator_path)
image_dir = os.path.abspath(avd_spec.local_image_dir)
if not (os.path.isfile(os.path.join(image_dir, _SYSTEM_IMAGE_NAME)) or
os.path.isfile(os.path.join(image_dir,
_SYSTEM_QEMU_IMAGE_NAME))):
raise errors.GetLocalImageError("No system image in %s." %
image_dir)
# TODO(b/141898893): In Android build environment, emulator gets build
# information from $ANDROID_PRODUCT_OUT/system/build.prop.
# If image_dir is an extacted SDK repository, the file is at
# image_dir/build.prop. Acloud copies it to
# image_dir/system/build.prop.
self._CopyBuildProp(image_dir)
instance_id = avd_spec.local_instance_id
inst = instance.LocalGoldfishInstance(instance_id,
avd_flavor=avd_spec.flavor)
adb = adb_tools.AdbTools(adb_port=inst.adb_port,
device_serial=inst.device_serial)
self._CheckRunningEmulator(adb, no_prompts)
instance_dir = inst.instance_dir
shutil.rmtree(instance_dir, ignore_errors=True)
os.makedirs(instance_dir)
extra_args = self._ConvertAvdSpecToArgs(avd_spec, instance_dir)
logger.info("Instance directory: %s", instance_dir)
proc = self._StartEmulatorProcess(emulator_path, instance_dir,
image_dir, inst.console_port,
inst.adb_port, extra_args)
boot_timeout_secs = (avd_spec.boot_timeout_secs or
_DEFAULT_EMULATOR_TIMEOUT_SECS)
result_report = report.Report(command="create")
try:
self._WaitForEmulatorToStart(adb, proc, boot_timeout_secs)
except (errors.DeviceBootTimeoutError, errors.SubprocessFail) as e:
result_report.SetStatus(report.Status.BOOT_FAIL)
result_report.AddDeviceBootFailure(inst.name, inst.ip,
inst.adb_port, vnc_port=None,
error=str(e))
else:
result_report.SetStatus(report.Status.SUCCESS)
result_report.AddDevice(inst.name, inst.ip, inst.adb_port,
vnc_port=None)
if proc.poll() is None:
inst.WriteCreationTimestamp()
return result_report
@staticmethod
def _MixImages(output_dir, image_dir, system_image_dir, ota):
"""Mix emulator images and a system image into a disk image.
Args:
output_dir: The path to the output directory.
image_dir: The input directory that provides images except
system.img.
system_image_dir: The input directory that provides system.img.
ota: An instance of ota_tools.OtaTools.
Returns:
The path to the mixed disk image in output_dir.
"""
# Create the super image.
mixed_super_image_path = os.path.join(output_dir, "mixed_super.img")
system_image_path = os.path.join(system_image_dir, _SYSTEM_IMAGE_NAME)
ota.BuildSuperImage(mixed_super_image_path,
os.path.join(image_dir, _MISC_INFO_FILE_NAME),
lambda partition: _GetImageForLogicalPartition(
partition, system_image_path, image_dir))
# Create the vbmeta image.
disabled_vbmeta_image_path = os.path.join(output_dir,
"disabled_vbmeta.img")
ota.MakeDisabledVbmetaImage(disabled_vbmeta_image_path)
# Create the disk image.
combined_image = os.path.join(output_dir, "combined.img")
ota.MkCombinedImg(combined_image,
os.path.join(image_dir,
_SYSTEM_QEMU_CONFIG_FILE_NAME),
lambda partition: _GetImageForPhysicalPartition(
partition, mixed_super_image_path,
disabled_vbmeta_image_path, image_dir))
return combined_image
@staticmethod
def _FindEmulatorBinary(search_paths):
"""Return the path to the emulator binary."""
# Find in unzipped sdk-repo-*.zip.
for search_path in search_paths:
path = os.path.join(search_path, _EMULATOR_BIN_NAME)
if os.path.isfile(path):
return path
path = os.path.join(search_path, _SDK_REPO_EMULATOR_DIR_NAME,
_EMULATOR_BIN_NAME)
if os.path.isfile(path):
return path
# Find in build environment.
prebuilt_emulator_dir = os.environ.get(
constants.ENV_ANDROID_EMULATOR_PREBUILTS)
if prebuilt_emulator_dir:
path = os.path.join(prebuilt_emulator_dir, _EMULATOR_BIN_NAME)
if os.path.isfile(path):
return path
raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG)
@staticmethod
def _IsEmulatorRunning(adb):
"""Check existence of an emulator by sending an empty command.
Args:
adb: adb_tools.AdbTools initialized with the emulator's serial.
Returns:
Boolean, whether the emulator is running.
"""
return adb.EmuCommand() == 0
def _CheckRunningEmulator(self, adb, no_prompts):
"""Attempt to delete a running emulator.
Args:
adb: adb_tools.AdbTools initialized with the emulator's serial.
no_prompts: Boolean, True to skip all prompts.
Raises:
errors.CreateError if the emulator isn't deleted.
"""
if not self._IsEmulatorRunning(adb):
return
logger.info("Goldfish AVD is already running.")
if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH):
if adb.EmuCommand("kill") != 0:
raise errors.CreateError("Cannot kill emulator.")
self._WaitForEmulatorToStop(adb)
else:
sys.exit(constants.EXIT_BY_USER)
@staticmethod
def _CopyBuildProp(image_dir):
"""Copy build.prop to system/build.prop if it doesn't exist.
Args:
image_dir: The directory to find build.prop in.
Raises:
errors.GetLocalImageError if build.prop does not exist.
"""
build_prop_path = os.path.join(image_dir, "system",
_BUILD_PROP_FILE_NAME)
if os.path.exists(build_prop_path):
return
build_prop_src_path = os.path.join(image_dir, _BUILD_PROP_FILE_NAME)
if not os.path.isfile(build_prop_src_path):
raise errors.GetLocalImageError("No %s in %s." %
_BUILD_PROP_FILE_NAME, image_dir)
build_prop_dir = os.path.dirname(build_prop_path)
logger.info("Copy %s to %s", _BUILD_PROP_FILE_NAME, build_prop_path)
if not os.path.exists(build_prop_dir):
os.makedirs(build_prop_dir)
shutil.copyfile(build_prop_src_path, build_prop_path)
@staticmethod
def _ReplaceSystemQemuImg(new_image, image_dir):
"""Replace system-qemu.img in the directory.
Args:
new_image: The path to the new image.
image_dir: The directory containing system-qemu.img.
"""
system_qemu_img = os.path.join(image_dir, _SYSTEM_QEMU_IMAGE_NAME)
if os.path.exists(system_qemu_img):
system_qemu_img_bak = system_qemu_img + _NON_MIXED_BACKUP_IMAGE_EXT
if not os.path.exists(system_qemu_img_bak):
# If system-qemu.img.bak-non-mixed does not exist, the
# system-qemu.img was not created by acloud and should be
# preserved. The user can restore it by renaming the backup to
# system-qemu.img.
logger.info("Rename %s to %s%s.",
system_qemu_img, _SYSTEM_QEMU_IMAGE_NAME,
_NON_MIXED_BACKUP_IMAGE_EXT)
os.rename(system_qemu_img, system_qemu_img_bak)
else:
# The existing system-qemu.img.bak-non-mixed was renamed by
# the previous invocation on acloud. The existing
# system-qemu.img is a mixed image. Acloud removes the mixed
# image because it is large and not reused.
os.remove(system_qemu_img)
try:
logger.info("Link %s to %s.", system_qemu_img, new_image)
os.link(new_image, system_qemu_img)
except OSError:
logger.info("Fail to link. Copy %s to %s",
system_qemu_img, new_image)
shutil.copyfile(new_image, system_qemu_img)
def _ConvertAvdSpecToArgs(self, avd_spec, instance_dir):
"""Convert AVD spec to emulator arguments.
Args:
avd_spec: AVDSpec object.
instance_dir: The instance directory for mixed images.
Returns:
List of strings, the arguments for emulator command.
"""
args = []
if avd_spec.gpu:
args.extend(("-gpu", avd_spec.gpu))
if not avd_spec.autoconnect:
args.append("-no-window")
if avd_spec.local_system_image_dir:
mixed_image_dir = os.path.join(instance_dir, "mixed_images")
os.mkdir(mixed_image_dir)
image_dir = os.path.abspath(avd_spec.local_image_dir)
ota_tools_dir = ota_tools.FindOtaTools(avd_spec.local_tool_dirs)
ota_tools_dir = os.path.abspath(ota_tools_dir)
mixed_image = self._MixImages(
mixed_image_dir, image_dir,
os.path.abspath(avd_spec.local_system_image_dir),
ota_tools.OtaTools(ota_tools_dir))
# TODO(b/142228085): Use -system instead of modifying image_dir.
self._ReplaceSystemQemuImg(mixed_image, image_dir)
# Unlock the device so that the disabled vbmeta takes effect.
args.extend(("-qemu", "-append",
"androidboot.verifiedbootstate=orange"))
return args
@staticmethod
def _StartEmulatorProcess(emulator_path, working_dir, image_dir,
console_port, adb_port, extra_args):
"""Start an emulator process.
Args:
emulator_path: The path to emulator binary.
working_dir: The working directory for the emulator process.
The emulator command creates files in the directory.
image_dir: The directory containing the required images.
e.g., composite system.img or system-qemu.img.
console_port: The console port of the emulator.
adb_port: The ADB port of the emulator.
extra_args: List of strings, the extra arguments.
Returns:
A Popen object, the emulator process.
"""
emulator_env = os.environ.copy()
emulator_env[constants.ENV_ANDROID_PRODUCT_OUT] = image_dir
# Set ANDROID_TMP for emulator to create AVD info files in.
emulator_env[constants.ENV_ANDROID_TMP] = working_dir
# Set ANDROID_BUILD_TOP so that the emulator considers itself to be in
# build environment.
if constants.ENV_ANDROID_BUILD_TOP not in emulator_env:
emulator_env[constants.ENV_ANDROID_BUILD_TOP] = image_dir
logcat_path = os.path.join(working_dir, "logcat.txt")
stdouterr_path = os.path.join(working_dir, "stdouterr.txt")
# The command doesn't create -stdouterr-file automatically.
with open(stdouterr_path, "w") as _:
pass
emulator_cmd = [
os.path.abspath(emulator_path),
"-verbose", "-show-kernel", "-read-only",
"-ports", str(console_port) + "," + str(adb_port),
"-logcat-output", logcat_path,
"-stdouterr-file", stdouterr_path
]
emulator_cmd.extend(extra_args)
logger.debug("Execute %s", emulator_cmd)
with open(os.devnull, "rb+") as devnull:
return subprocess.Popen(
emulator_cmd, shell=False, cwd=working_dir, env=emulator_env,
stdin=devnull, stdout=devnull, stderr=devnull)
def _WaitForEmulatorToStop(self, adb):
"""Wait for an emulator to be unavailable on the console port.
Args:
adb: adb_tools.AdbTools initialized with the emulator's serial.
Raises:
errors.CreateError if the emulator does not stop within timeout.
"""
create_error = errors.CreateError(_EMU_KILL_TIMEOUT_ERROR %
{"timeout": _EMU_KILL_TIMEOUT_SECS})
utils.PollAndWait(func=lambda: self._IsEmulatorRunning(adb),
expected_return=False,
timeout_exception=create_error,
timeout_secs=_EMU_KILL_TIMEOUT_SECS,
sleep_interval_secs=1)
def _WaitForEmulatorToStart(self, adb, proc, timeout):
"""Wait for an emulator to be available on the console port.
Args:
adb: adb_tools.AdbTools initialized with the emulator's serial.
proc: Popen object, the running emulator process.
timeout: Integer, timeout in seconds.
Raises:
errors.DeviceBootTimeoutError if the emulator does not boot within
timeout.
errors.SubprocessFail if the process terminates.
"""
timeout_error = errors.DeviceBootTimeoutError(_EMULATOR_TIMEOUT_ERROR %
{"timeout": timeout})
utils.PollAndWait(func=lambda: (proc.poll() is None and
self._IsEmulatorRunning(adb)),
expected_return=True,
timeout_exception=timeout_error,
timeout_secs=timeout,
sleep_interval_secs=5)
if proc.poll() is not None:
raise errors.SubprocessFail("Emulator process returned %d." %
proc.returncode)