blob: 51365eb232f551a298ce6954461a4cc13fefcbbf [file] [log] [blame]
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import absolute_import
import contextlib
import json
import logging
import os
import socket
import stat
import subprocess
import threading
from google.protobuf import text_format # pylint: disable=import-error
from devil.android import device_utils
from devil.android.sdk import adb_wrapper
from devil.utils import cmd_helper
from devil.utils import timeout_retry
from py_utils import tempfile_ext
from pylib import constants
from pylib.local.emulator import ini
from pylib.local.emulator.proto import avd_pb2
_ALL_PACKAGES = object()
_DEFAULT_AVDMANAGER_PATH = os.path.join(
constants.ANDROID_SDK_ROOT, 'cmdline-tools', 'latest', 'bin', 'avdmanager')
# Default to a 480dp mdpi screen (a relatively large phone).
# See https://developer.android.com/training/multiscreen/screensizes
# and https://developer.android.com/training/multiscreen/screendensities
# for more information.
_DEFAULT_SCREEN_DENSITY = 160
_DEFAULT_SCREEN_HEIGHT = 960
_DEFAULT_SCREEN_WIDTH = 480
class AvdException(Exception):
"""Raised when this module has a problem interacting with an AVD."""
def __init__(self, summary, command=None, stdout=None, stderr=None):
message_parts = [summary]
if command:
message_parts.append(' command: %s' % ' '.join(command))
if stdout:
message_parts.append(' stdout:')
message_parts.extend(' %s' % line for line in stdout.splitlines())
if stderr:
message_parts.append(' stderr:')
message_parts.extend(' %s' % line for line in stderr.splitlines())
super(AvdException, self).__init__('\n'.join(message_parts))
def _Load(avd_proto_path):
"""Loads an Avd proto from a textpb file at the given path.
Should not be called outside of this module.
Args:
avd_proto_path: path to a textpb file containing an Avd message.
"""
with open(avd_proto_path) as avd_proto_file:
return text_format.Merge(avd_proto_file.read(), avd_pb2.Avd())
class _AvdManagerAgent(object):
"""Private utility for interacting with avdmanager."""
def __init__(self, avd_home, sdk_root):
"""Create an _AvdManagerAgent.
Args:
avd_home: path to ANDROID_AVD_HOME directory.
Typically something like /path/to/dir/.android/avd
sdk_root: path to SDK root directory.
"""
self._avd_home = avd_home
self._sdk_root = sdk_root
self._env = dict(os.environ)
# The avdmanager from cmdline-tools would look two levels
# up from toolsdir to find the SDK root.
# Pass avdmanager a fake directory under the directory in which
# we install the system images s.t. avdmanager can find the
# system images.
fake_tools_dir = os.path.join(self._sdk_root, 'non-existent-tools',
'non-existent-version')
self._env.update({
'ANDROID_AVD_HOME':
self._avd_home,
'AVDMANAGER_OPTS':
'-Dcom.android.sdkmanager.toolsdir=%s' % fake_tools_dir,
})
def Create(self, avd_name, system_image, force=False):
"""Call `avdmanager create`.
Args:
avd_name: name of the AVD to create.
system_image: system image to use for the AVD.
force: whether to force creation, overwriting any existing
AVD with the same name.
"""
create_cmd = [
_DEFAULT_AVDMANAGER_PATH,
'-v',
'create',
'avd',
'-n',
avd_name,
'-k',
system_image,
]
if force:
create_cmd += ['--force']
create_proc = cmd_helper.Popen(
create_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=self._env)
output, error = create_proc.communicate(input='\n')
if create_proc.returncode != 0:
raise AvdException(
'AVD creation failed',
command=create_cmd,
stdout=output,
stderr=error)
for line in output.splitlines():
logging.info(' %s', line)
def Delete(self, avd_name):
"""Call `avdmanager delete`.
Args:
avd_name: name of the AVD to delete.
"""
delete_cmd = [
_DEFAULT_AVDMANAGER_PATH,
'-v',
'delete',
'avd',
'-n',
avd_name,
]
try:
for line in cmd_helper.IterCmdOutputLines(delete_cmd, env=self._env):
logging.info(' %s', line)
except subprocess.CalledProcessError as e:
raise AvdException('AVD deletion failed: %s' % str(e), command=delete_cmd)
class AvdConfig(object):
"""Represents a particular AVD configuration.
This class supports creation, installation, and execution of an AVD
from a given Avd proto message, as defined in
//build/android/pylib/local/emulator/proto/avd.proto.
"""
def __init__(self, avd_proto_path):
"""Create an AvdConfig object.
Args:
avd_proto_path: path to a textpb file containing an Avd message.
"""
self._config = _Load(avd_proto_path)
self._emulator_home = os.path.join(constants.DIR_SOURCE_ROOT,
self._config.avd_package.dest_path)
self._emulator_sdk_root = os.path.join(
constants.DIR_SOURCE_ROOT, self._config.emulator_package.dest_path)
self._emulator_path = os.path.join(self._emulator_sdk_root, 'emulator',
'emulator')
self._initialized = False
self._initializer_lock = threading.Lock()
@property
def avd_settings(self):
return self._config.avd_settings
def Create(self,
force=False,
snapshot=False,
keep=False,
cipd_json_output=None,
dry_run=False):
"""Create an instance of the AVD CIPD package.
This method:
- installs the requisite system image
- creates the AVD
- modifies the AVD's ini files to support running chromium tests
in chromium infrastructure
- optionally starts & stops the AVD for snapshotting (default no)
- By default creates and uploads an instance of the AVD CIPD package
(can be turned off by dry_run flag).
- optionally deletes the AVD (default yes)
Args:
force: bool indicating whether to force create the AVD.
snapshot: bool indicating whether to snapshot the AVD before creating
the CIPD package.
keep: bool indicating whether to keep the AVD after creating
the CIPD package.
cipd_json_output: string path to pass to `cipd create` via -json-output.
dry_run: When set to True, it will skip the CIPD package creation
after creating the AVD.
"""
logging.info('Installing required packages.')
self._InstallCipdPackages(packages=[
self._config.emulator_package,
self._config.system_image_package,
])
android_avd_home = os.path.join(self._emulator_home, 'avd')
if not os.path.exists(android_avd_home):
os.makedirs(android_avd_home)
avd_manager = _AvdManagerAgent(
avd_home=android_avd_home, sdk_root=self._emulator_sdk_root)
logging.info('Creating AVD.')
avd_manager.Create(
avd_name=self._config.avd_name,
system_image=self._config.system_image_name,
force=force)
try:
logging.info('Modifying AVD configuration.')
# Clear out any previous configuration or state from this AVD.
root_ini = os.path.join(android_avd_home,
'%s.ini' % self._config.avd_name)
features_ini = os.path.join(self._emulator_home, 'advancedFeatures.ini')
avd_dir = os.path.join(android_avd_home, '%s.avd' % self._config.avd_name)
config_ini = os.path.join(avd_dir, 'config.ini')
with ini.update_ini_file(root_ini) as root_ini_contents:
root_ini_contents['path.rel'] = 'avd/%s.avd' % self._config.avd_name
with ini.update_ini_file(features_ini) as features_ini_contents:
# features_ini file will not be refreshed by avdmanager during
# creation. So explicitly clear its content to exclude any leftover
# from previous creation.
features_ini_contents.clear()
features_ini_contents.update(self.avd_settings.advanced_features)
with ini.update_ini_file(config_ini) as config_ini_contents:
height = self.avd_settings.screen.height or _DEFAULT_SCREEN_HEIGHT
width = self.avd_settings.screen.width or _DEFAULT_SCREEN_WIDTH
density = self.avd_settings.screen.density or _DEFAULT_SCREEN_DENSITY
config_ini_contents.update({
'disk.dataPartition.size': '4G',
'hw.keyboard': 'yes',
'hw.lcd.density': density,
'hw.lcd.height': height,
'hw.lcd.width': width,
})
if self.avd_settings.ram_size:
config_ini_contents['hw.ramSize'] = self.avd_settings.ram_size
# Start & stop the AVD.
self._Initialize()
instance = _AvdInstance(self._emulator_path, self._emulator_home,
self._config)
# Enable debug for snapshot when it is set to True
debug_tags = 'init,snapshot' if snapshot else None
instance.Start(
read_only=False, snapshot_save=snapshot, debug_tags=debug_tags)
# Android devices with full-disk encryption are encrypted on first boot,
# and then get decrypted to continue the boot process (See details in
# https://bit.ly/3agmjcM).
# Wait for this step to complete since it can take a while for old OSs
# like M, otherwise the avd may have "Encryption Unsuccessful" error.
device_utils.DeviceUtils(instance.serial).WaitUntilFullyBooted(
decrypt=True, timeout=180, retries=0)
instance.Stop()
# The multiinstance lock file seems to interfere with the emulator's
# operation in some circumstances (beyond the obvious -read-only ones),
# and there seems to be no mechanism by which it gets closed or deleted.
# See https://bit.ly/2pWQTH7 for context.
multiInstanceLockFile = os.path.join(avd_dir, 'multiinstance.lock')
if os.path.exists(multiInstanceLockFile):
os.unlink(multiInstanceLockFile)
package_def_content = {
'package':
self._config.avd_package.package_name,
'root':
self._emulator_home,
'install_mode':
'copy',
'data': [{
'dir': os.path.relpath(avd_dir, self._emulator_home)
}, {
'file': os.path.relpath(root_ini, self._emulator_home)
}, {
'file': os.path.relpath(features_ini, self._emulator_home)
}],
}
logging.info('Creating AVD CIPD package.')
logging.debug('ensure file content: %s',
json.dumps(package_def_content, indent=2))
with tempfile_ext.TemporaryFileName(suffix='.json') as package_def_path:
with open(package_def_path, 'w') as package_def_file:
json.dump(package_def_content, package_def_file)
logging.info(' %s', self._config.avd_package.package_name)
cipd_create_cmd = [
'cipd',
'create',
'-pkg-def',
package_def_path,
'-tag',
'emulator_version:%s' % self._config.emulator_package.version,
'-tag',
'system_image_version:%s' %
self._config.system_image_package.version,
]
if cipd_json_output:
cipd_create_cmd.extend([
'-json-output',
cipd_json_output,
])
logging.info('running %r%s', cipd_create_cmd,
' (dry_run)' if dry_run else '')
if not dry_run:
try:
for line in cmd_helper.IterCmdOutputLines(cipd_create_cmd):
logging.info(' %s', line)
except subprocess.CalledProcessError as e:
raise AvdException(
'CIPD package creation failed: %s' % str(e),
command=cipd_create_cmd)
finally:
if not keep:
logging.info('Deleting AVD.')
avd_manager.Delete(avd_name=self._config.avd_name)
def Install(self, packages=_ALL_PACKAGES):
"""Installs the requested CIPD packages and prepares them for use.
This includes making files writeable and revising some of the
emulator's internal config files.
Returns: None
Raises: AvdException on failure to install.
"""
self._InstallCipdPackages(packages=packages)
self._MakeWriteable()
self._EditConfigs()
def _InstallCipdPackages(self, packages):
pkgs_by_dir = {}
if packages is _ALL_PACKAGES:
packages = [
self._config.avd_package,
self._config.emulator_package,
self._config.system_image_package,
]
for pkg in packages:
if not pkg.dest_path in pkgs_by_dir:
pkgs_by_dir[pkg.dest_path] = []
pkgs_by_dir[pkg.dest_path].append(pkg)
for pkg_dir, pkgs in pkgs_by_dir.items():
logging.info('Installing packages in %s', pkg_dir)
cipd_root = os.path.join(constants.DIR_SOURCE_ROOT, pkg_dir)
if not os.path.exists(cipd_root):
os.makedirs(cipd_root)
ensure_path = os.path.join(cipd_root, '.ensure')
with open(ensure_path, 'w') as ensure_file:
# Make CIPD ensure that all files are present and correct,
# even if it thinks the package is installed.
ensure_file.write('$ParanoidMode CheckIntegrity\n\n')
for pkg in pkgs:
ensure_file.write('%s %s\n' % (pkg.package_name, pkg.version))
logging.info(' %s %s', pkg.package_name, pkg.version)
ensure_cmd = [
'cipd',
'ensure',
'-ensure-file',
ensure_path,
'-root',
cipd_root,
]
try:
for line in cmd_helper.IterCmdOutputLines(ensure_cmd):
logging.info(' %s', line)
except subprocess.CalledProcessError as e:
raise AvdException(
'Failed to install CIPD package %s: %s' % (pkg.package_name,
str(e)),
command=ensure_cmd)
def _MakeWriteable(self):
# The emulator requires that some files are writable.
for dirname, _, filenames in os.walk(self._emulator_home):
for f in filenames:
path = os.path.join(dirname, f)
mode = os.lstat(path).st_mode
if mode & stat.S_IRUSR:
mode = mode | stat.S_IWUSR
os.chmod(path, mode)
def _EditConfigs(self):
android_avd_home = os.path.join(self._emulator_home, 'avd')
avd_dir = os.path.join(android_avd_home, '%s.avd' % self._config.avd_name)
config_path = os.path.join(avd_dir, 'config.ini')
if os.path.exists(config_path):
with open(config_path) as config_file:
config_contents = ini.load(config_file)
else:
config_contents = {}
config_contents['hw.sdCard'] = 'true'
if self.avd_settings.sdcard.size:
sdcard_path = os.path.join(avd_dir, 'cr-sdcard.img')
if not os.path.exists(sdcard_path):
mksdcard_path = os.path.join(
os.path.dirname(self._emulator_path), 'mksdcard')
mksdcard_cmd = [
mksdcard_path,
self.avd_settings.sdcard.size,
sdcard_path,
]
cmd_helper.RunCmd(mksdcard_cmd)
config_contents['hw.sdCard.path'] = sdcard_path
with open(config_path, 'w') as config_file:
ini.dump(config_contents, config_file)
def _Initialize(self):
if self._initialized:
return
with self._initializer_lock:
if self._initialized:
return
# Emulator start-up looks for the adb daemon. Make sure it's running.
adb_wrapper.AdbWrapper.StartServer()
# Emulator start-up tries to check for the SDK root by looking for
# platforms/ and platform-tools/. Ensure they exist.
# See http://bit.ly/2YAkyFE for context.
required_dirs = [
os.path.join(self._emulator_sdk_root, 'platforms'),
os.path.join(self._emulator_sdk_root, 'platform-tools'),
]
for d in required_dirs:
if not os.path.exists(d):
os.makedirs(d)
def CreateInstance(self):
"""Creates an AVD instance without starting it.
Returns:
An _AvdInstance.
"""
self._Initialize()
return _AvdInstance(self._emulator_path, self._emulator_home, self._config)
def StartInstance(self):
"""Starts an AVD instance.
Returns:
An _AvdInstance.
"""
instance = self.CreateInstance()
instance.Start()
return instance
class _AvdInstance(object):
"""Represents a single running instance of an AVD.
This class should only be created directly by AvdConfig.StartInstance,
but its other methods can be freely called.
"""
def __init__(self, emulator_path, emulator_home, avd_config):
"""Create an _AvdInstance object.
Args:
emulator_path: path to the emulator binary.
emulator_home: path to the emulator home directory.
avd_config: AVD config proto.
"""
self._avd_config = avd_config
self._avd_name = avd_config.avd_name
self._emulator_home = emulator_home
self._emulator_path = emulator_path
self._emulator_proc = None
self._emulator_serial = None
self._sink = None
def __str__(self):
return '%s|%s' % (self._avd_name, (self._emulator_serial or id(self)))
def Start(self,
read_only=True,
snapshot_save=False,
window=False,
writable_system=False,
debug_tags=None):
"""Starts the emulator running an instance of the given AVD."""
with tempfile_ext.TemporaryFileName() as socket_path, (contextlib.closing(
socket.socket(socket.AF_UNIX))) as sock:
sock.bind(socket_path)
emulator_cmd = [
self._emulator_path,
'-avd',
self._avd_name,
'-report-console',
'unix:%s' % socket_path,
'-no-boot-anim',
# Set the gpu mode to swiftshader_indirect otherwise the avd may exit
# with the error "change of render" under window mode
'-gpu',
'swiftshader_indirect',
]
if read_only:
emulator_cmd.append('-read-only')
if not snapshot_save:
emulator_cmd.append('-no-snapshot-save')
if writable_system:
emulator_cmd.append('-writable-system')
if debug_tags:
emulator_cmd.extend(['-debug', debug_tags])
emulator_env = {}
if self._emulator_home:
emulator_env['ANDROID_EMULATOR_HOME'] = self._emulator_home
if window:
if 'DISPLAY' in os.environ:
emulator_env['DISPLAY'] = os.environ.get('DISPLAY')
else:
raise AvdException('Emulator failed to start: DISPLAY not defined')
else:
emulator_cmd.append('-no-window')
sock.listen(1)
logging.info('Starting emulator with commands: %s',
' '.join(emulator_cmd))
# TODO(jbudorick): Add support for logging emulator stdout & stderr at
# higher logging levels.
# Enable the emulator log when debug_tags is set.
if not debug_tags:
self._sink = open('/dev/null', 'w')
self._emulator_proc = cmd_helper.Popen(
emulator_cmd, stdout=self._sink, stderr=self._sink, env=emulator_env)
# Waits for the emulator to report its serial as requested via
# -report-console. See http://bit.ly/2lK3L18 for more.
def listen_for_serial(s):
logging.info('Waiting for connection from emulator.')
with contextlib.closing(s.accept()[0]) as conn:
val = conn.recv(1024)
return 'emulator-%d' % int(val)
try:
self._emulator_serial = timeout_retry.Run(
listen_for_serial, timeout=30, retries=0, args=[sock])
logging.info('%s started', self._emulator_serial)
except Exception as e:
self.Stop()
raise AvdException('Emulator failed to start: %s' % str(e))
def Stop(self):
"""Stops the emulator process."""
if self._emulator_proc:
if self._emulator_proc.poll() is None:
if self._emulator_serial:
device_utils.DeviceUtils(self._emulator_serial).adb.Emu('kill')
else:
self._emulator_proc.terminate()
self._emulator_proc.wait()
self._emulator_proc = None
if self._sink:
self._sink.close()
self._sink = None
@property
def serial(self):
return self._emulator_serial