You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

565 lines
23 KiB

# 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
be an unzipped extra image package, i.e.,
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.create import create_common
from acloud.internal import constants
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"
_EMULATOR_BIN_DIR_NAMES = ("bin64", "qemu")
_SDK_REPO_EMULATOR_DIR_NAME = "emulator"
_SYSTEM_IMAGE_NAME = "system.img"
_SYSTEM_IMAGE_NAME_PATTERN = r"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"
# 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.")
_INSTANCES_IN_USE_MSG = ("All instances are in use. Try resetting an instance "
"by specifying --local-instance and an id between 1 "
"and %(max_id)d.")
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.
"""
if not utils.IsSupportedPlatform(print_warning=True):
result_report = report.Report(command="create")
result_report.SetStatus(report.Status.FAIL)
return result_report
try:
ins_id, ins_lock = self._LockInstance(avd_spec)
except errors.CreateError as e:
result_report = report.Report(command="create")
result_report.AddError(str(e))
result_report.SetStatus(report.Status.FAIL)
return result_report
try:
ins = instance.LocalGoldfishInstance(ins_id,
avd_flavor=avd_spec.flavor)
if not self._CheckRunningEmulator(ins.adb, no_prompts):
# Mark as in-use so that it won't be auto-selected again.
ins_lock.SetInUse(True)
sys.exit(constants.EXIT_BY_USER)
result_report = self._CreateAVDForInstance(ins, avd_spec)
# The infrastructure is able to delete the instance only if the
# instance name is reported. This method changes the state to
# in-use after creating the report.
ins_lock.SetInUse(True)
return result_report
finally:
ins_lock.Unlock()
@staticmethod
def _LockInstance(avd_spec):
"""Select an id and lock the instance.
Args:
avd_spec: AVDSpec for the device.
Returns:
The instance id and the LocalInstanceLock that is locked by this
process.
Raises:
errors.CreateError if fails to select or lock the instance.
"""
if avd_spec.local_instance_id:
ins_id = avd_spec.local_instance_id
ins_lock = instance.LocalGoldfishInstance.GetLockById(ins_id)
if ins_lock.Lock():
return ins_id, ins_lock
raise errors.CreateError("Instance %d is locked by another "
"process." % ins_id)
max_id = instance.LocalGoldfishInstance.GetMaxNumberOfInstances()
for ins_id in range(1, max_id + 1):
ins_lock = instance.LocalGoldfishInstance.GetLockById(ins_id)
if ins_lock.LockIfNotInUse(timeout_secs=0):
logger.info("Selected instance id: %d", ins_id)
return ins_id, ins_lock
raise errors.CreateError(_INSTANCES_IN_USE_MSG % {"max_id": max_id})
def _CreateAVDForInstance(self, ins, avd_spec):
"""Create an emulator process for the goldfish instance.
Args:
ins: LocalGoldfishInstance to be initialized.
avd_spec: AVDSpec for the device.
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.CheckPathError if OTA tools are not found.
"""
emulator_path = self._FindEmulatorBinary(avd_spec.local_tool_dirs)
image_dir = self._FindImageDir(avd_spec.local_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_dir = ins.instance_dir
create_common.PrepareLocalInstanceDir(instance_dir, avd_spec)
extra_args = self._ConvertAvdSpecToArgs(avd_spec, instance_dir)
logger.info("Instance directory: %s", instance_dir)
proc = self._StartEmulatorProcess(emulator_path, instance_dir,
image_dir, ins.console_port,
ins.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(ins.adb, proc, boot_timeout_secs)
except (errors.DeviceBootTimeoutError, errors.SubprocessFail) as e:
result_report.SetStatus(report.Status.BOOT_FAIL)
result_report.AddDeviceBootFailure(ins.name, ins.ip,
ins.adb_port, vnc_port=None,
error=str(e),
device_serial=ins.device_serial)
else:
result_report.SetStatus(report.Status.SUCCESS)
result_report.AddDevice(ins.name, ins.ip, ins.adb_port,
vnc_port=None,
device_serial=ins.device_serial)
return result_report
@staticmethod
def _MixImages(output_dir, image_dir, system_image_path, 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_path: The path to the system image.
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")
ota.BuildSuperImage(
mixed_super_image_path,
os.path.join(image_dir, _MISC_INFO_FILE_NAME),
lambda partition: ota_tools.GetImageForPartition(
partition, image_dir, system=system_image_path))
# 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: ota_tools.GetImageForPartition(
partition, image_dir, super=mixed_super_image_path,
vbmeta=disabled_vbmeta_image_path))
return combined_image
@staticmethod
def _FindEmulatorBinary(search_paths):
"""Find emulator binary in the directories.
The directories may be extracted from zip archives without preserving
file permissions. When this method finds the emulator binary and its
dependencies, it sets the files to be executable.
Args:
search_paths: Collection of strings, the directories to search for
emulator binary.
Returns:
The path to the emulator binary.
Raises:
errors.GetSdkRepoPackageError if emulator binary is not found.
"""
emulator_dir = None
# Find in unzipped sdk-repo-*.zip.
for search_path in search_paths:
if os.path.isfile(os.path.join(search_path, _EMULATOR_BIN_NAME)):
emulator_dir = search_path
break
sdk_repo_dir = os.path.join(search_path,
_SDK_REPO_EMULATOR_DIR_NAME)
if os.path.isfile(os.path.join(sdk_repo_dir, _EMULATOR_BIN_NAME)):
emulator_dir = sdk_repo_dir
break
# Find in build environment.
if not emulator_dir:
prebuilt_emulator_dir = os.environ.get(
constants.ENV_ANDROID_EMULATOR_PREBUILTS)
if (prebuilt_emulator_dir and os.path.isfile(
os.path.join(prebuilt_emulator_dir, _EMULATOR_BIN_NAME))):
emulator_dir = prebuilt_emulator_dir
if not emulator_dir:
raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG)
emulator_dir = os.path.abspath(emulator_dir)
# Set the binaries to be executable.
for subdir_name in _EMULATOR_BIN_DIR_NAMES:
subdir_path = os.path.join(emulator_dir, subdir_name)
if os.path.isdir(subdir_path):
utils.SetDirectoryTreeExecutable(subdir_path)
emulator_path = os.path.join(emulator_dir, _EMULATOR_BIN_NAME)
utils.SetExecutable(emulator_path)
return emulator_path
@staticmethod
def _FindImageDir(image_dir):
"""Find emulator images in the directory.
In build environment, the images are in $ANDROID_PRODUCT_OUT.
In an extracted SDK repository, the images are in the subdirectory
named after the CPU architecture.
Args:
image_dir: The path given by the environment variable or the user.
Returns:
The directory containing the emulator images.
Raises:
errors.GetLocalImageError if the images are not found.
"""
image_dir = os.path.abspath(image_dir)
entries = os.listdir(image_dir)
if len(entries) == 1:
first_entry = os.path.join(image_dir, entries[0])
if os.path.isdir(first_entry):
image_dir = first_entry
if (os.path.isfile(os.path.join(image_dir, _SYSTEM_QEMU_IMAGE_NAME)) or
os.path.isfile(os.path.join(image_dir, _SYSTEM_IMAGE_NAME))):
return image_dir
raise errors.GetLocalImageError("No device image in %s." % image_dir)
@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.
Returns:
Whether the user wants to continue.
Raises:
errors.CreateError if the emulator isn't deleted.
"""
if not self._IsEmulatorRunning(adb):
return True
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)
return True
return False
@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:
mixed_image_dir = os.path.join(instance_dir, "mixed_images")
if os.path.exists(mixed_image_dir):
shutil.rmtree(mixed_image_dir)
os.mkdir(mixed_image_dir)
image_dir = self._FindImageDir(avd_spec.local_image_dir)
system_image_path = create_common.FindLocalImage(
avd_spec.local_system_image, _SYSTEM_IMAGE_NAME_PATTERN)
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, system_image_path,
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)