# Copyright 2021 - 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. """RemoteInstanceDeviceFactory provides basic interface to create a goldfish device factory.""" import collections import logging import os import posixpath as remote_path import re import shutil import subprocess import tempfile import time import zipfile from acloud import errors from acloud.create import create_common from acloud.internal import constants from acloud.internal.lib import android_build_client from acloud.internal.lib import auth from acloud.internal.lib import goldfish_utils from acloud.internal.lib import emulator_console from acloud.internal.lib import ota_tools from acloud.internal.lib import remote_host_client from acloud.internal.lib import utils from acloud.internal.lib import ssh from acloud.public import report from acloud.public.actions import base_device_factory logger = logging.getLogger(__name__) # Artifacts _SDK_REPO_IMAGE_ZIP_NAME_FORMAT = ("sdk-repo-linux-system-images-" "%(build_id)s.zip") _EXTRA_IMAGE_ZIP_NAME_FORMAT = "emu-extra-linux-system-images-%(build_id)s.zip" _IMAGE_ZIP_NAME_FORMAT = "%(build_target)s-img-%(build_id)s.zip" _OTA_TOOLS_ZIP_NAME = "otatools.zip" _EMULATOR_INFO_NAME = "emulator-info.txt" _EMULATOR_VERSION_PATTERN = re.compile(r"require\s+version-emulator=" r"(?P\w+)") _EMULATOR_ZIP_NAME_FORMAT = "sdk-repo-%(os)s-emulator-%(build_id)s.zip" _EMULATOR_BIN_DIR_NAMES = ("bin64", "qemu") _EMULATOR_BIN_NAME = "emulator" _SDK_REPO_EMULATOR_DIR_NAME = "emulator" # Files in temporary artifact directory. _DOWNLOAD_DIR_NAME = "download" _OTA_TOOLS_DIR_NAME = "ota_tools" _SYSTEM_IMAGE_NAME = "system.img" # Base directory of an instance. _REMOTE_INSTANCE_DIR_FORMAT = "acloud_gf_%d" # Relative paths in a base directory. _REMOTE_IMAGE_ZIP_PATH = "image.zip" _REMOTE_EMULATOR_ZIP_PATH = "emulator.zip" _REMOTE_IMAGE_DIR = "image" _REMOTE_KERNEL_PATH = "kernel" _REMOTE_RAMDISK_PATH = "mixed_ramdisk" _REMOTE_EMULATOR_DIR = "emulator" _REMOTE_RUNTIME_DIR = "instance" _REMOTE_LOGCAT_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "logcat.txt") _REMOTE_STDOUT_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "kernel.log") _REMOTE_STDERR_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "emu_stderr.txt") # Runtime parameters _EMULATOR_DEFAULT_CONSOLE_PORT = 5554 _DEFAULT_BOOT_TIMEOUT_SECS = 150 # Error messages _MISSING_EMULATOR_MSG = ("No emulator zip. Specify " "--emulator-build-id, or --emulator-zip.") ArtifactPaths = collections.namedtuple( "ArtifactPaths", ["image_zip", "emulator_zip", "ota_tools_dir", "system_image", "system_dlkm_image", "boot_image"]) RemotePaths = collections.namedtuple( "RemotePaths", ["image_dir", "emulator_dir", "kernel", "ramdisk"]) class RemoteHostGoldfishDeviceFactory(base_device_factory.BaseDeviceFactory): """A class that creates a goldfish device on a remote host. Attributes: avd_spec: AVDSpec object that tells us what we're going to create. android_build_client: An AndroidBuildClient that is lazily initialized. temp_artifact_dir: The temporary artifact directory that is lazily initialized during PrepareArtifacts. ssh: Ssh object that executes commands on the remote host. failures: A dictionary the maps instance names to error.DeviceBootError objects. logs: A dictionary that maps instance names to lists of report.LogFile. """ def __init__(self, avd_spec): """Initialize the attributes and the compute client.""" self._avd_spec = avd_spec self._android_build_client = None self._temp_artifact_dir = None self._ssh = ssh.Ssh( ip=ssh.IP(ip=self._avd_spec.remote_host), user=self._ssh_user, ssh_private_key_path=self._ssh_private_key_path, extra_args_ssh_tunnel=self._ssh_extra_args, report_internal_ip=False) self._failures = {} self._logs = {} super().__init__(compute_client=( remote_host_client.RemoteHostClient(avd_spec.remote_host))) @property def _build_api(self): """Initialize android_build_client.""" if not self._android_build_client: credentials = auth.CreateCredentials(self._avd_spec.cfg) self._android_build_client = android_build_client.AndroidBuildClient( credentials) return self._android_build_client @property def _artifact_dir(self): """Initialize temp_artifact_dir.""" if not self._temp_artifact_dir: self._temp_artifact_dir = tempfile.mkdtemp("host_gf") logger.info("Create temporary artifact directory: %s", self._temp_artifact_dir) return self._temp_artifact_dir @property def _download_dir(self): """Get the directory where the artifacts are downloaded.""" if self._avd_spec.image_download_dir: return self._avd_spec.image_download_dir return os.path.join(self._artifact_dir, _DOWNLOAD_DIR_NAME) @property def _ssh_user(self): return self._avd_spec.host_user or constants.GCE_USER @property def _ssh_private_key_path(self): return (self._avd_spec.host_ssh_private_key_path or self._avd_spec.cfg.ssh_private_key_path) @property def _ssh_extra_args(self): return self._avd_spec.cfg.extra_args_ssh_tunnel def _GetConsolePort(self): """Calculate the console port from the instance number. By convention, the console port is an even number, and the adb port is the console port + 1. The first instance uses port 5554 and 5555. The second instance uses 5556 and 5557, and so on. """ return (_EMULATOR_DEFAULT_CONSOLE_PORT + ((self._avd_spec.base_instance_num or 1) - 1) * 2) def _GetInstancePath(self, relative_path): """Append a relative path to the instance directory.""" return remote_path.join( _REMOTE_INSTANCE_DIR_FORMAT % (self._avd_spec.base_instance_num or 1), relative_path) def CreateInstance(self): """Create a goldfish instance on the remote host. Returns: The instance name. """ instance_name = goldfish_utils.FormatRemoteHostInstanceName( self._avd_spec.remote_host, self._GetConsolePort(), self._avd_spec.remote_image) client = self.GetComputeClient() timed_stage = constants.TIME_GCE start_time = time.time() try: client.SetStage(constants.STAGE_SSH_CONNECT) self._InitRemoteHost() start_time = client.RecordTime(timed_stage, start_time) timed_stage = constants.TIME_ARTIFACT client.SetStage(constants.STAGE_ARTIFACT) remote_paths = self._PrepareArtifacts() start_time = client.RecordTime(timed_stage, start_time) timed_stage = constants.TIME_LAUNCH client.SetStage(constants.STAGE_BOOT_UP) self._logs[instance_name] = self._GetEmulatorLogs() self._StartEmulator(remote_paths) self._WaitForEmulator() except (errors.DriverError, subprocess.CalledProcessError) as e: # Catch the generic runtime error and CalledProcessError which is # raised by the ssh module. self._failures[instance_name] = e finally: client.RecordTime(timed_stage, start_time) return instance_name def _InitRemoteHost(self): """Remove the existing instance and the instance directory.""" # Disable authentication for emulator console. self._ssh.Run("""'echo -n "" > .emulator_console_auth_token'""") try: with emulator_console.RemoteEmulatorConsole( self._avd_spec.remote_host, self._GetConsolePort(), self._ssh_user, self._ssh_private_key_path, self._ssh_extra_args) as console: console.Kill() logger.info("Killed existing emulator.") except errors.DeviceConnectionError as e: logger.info("Did not kill existing emulator: %s", str(e)) # Delete instance files. self._ssh.Run(f"rm -rf {self._GetInstancePath('')}") def _PrepareArtifacts(self): """Prepare artifacts on remote host. This method retrieves artifacts from cache or Android Build API and uploads them to the remote host. Returns: An object of RemotePaths. """ try: artifact_paths = self._RetrieveArtifacts() return self._UploadArtifacts(artifact_paths) finally: if self._temp_artifact_dir: shutil.rmtree(self._temp_artifact_dir, ignore_errors=True) self._temp_artifact_dir = None @staticmethod def _InferEmulatorZipName(build_target, build_id): """Determine the emulator zip name in build artifacts. The emulator zip name is composed of build variables that are not revealed in the artifacts. This method infers the emulator zip name from its build target name. Args: build_target: The emulator build target name, e.g., "emulator-linux_x64_nolocationui", "aarch64_sdk_tools_mac". build_id: A string, the emulator build ID. Returns: The name of the emulator zip. e.g., "sdk-repo-linux-emulator-123456.zip", "sdk-repo-darwin_aarch64-emulator-123456.zip". """ split_target = [x for product_variant in build_target.split("-") for x in product_variant.split("_")] if "darwin" in split_target or "mac" in split_target: os_name = "darwin" else: os_name = "linux" if "aarch64" in split_target: os_name = os_name + "_aarch64" return _EMULATOR_ZIP_NAME_FORMAT % {"os": os_name, "build_id": build_id} def _RetrieveArtifact(self, build_target, build_id, resource_id): """Retrieve an artifact from cache or Android Build API. Args: build_target: A string, the build target of the artifact. e.g., "sdk_phone_x86_64-userdebug". build_id: A string, the build ID of the artifact. resource_id: A string, the name of the artifact. e.g., "sdk-repo-linux-system-images-123456.zip". Returns: The path to the artifact in download_dir. """ local_path = os.path.join(self._download_dir, build_id, build_target, resource_id) if os.path.isfile(local_path): logger.info("Skip downloading existing artifact: %s", local_path) return local_path complete = False try: os.makedirs(os.path.dirname(local_path), exist_ok=True) self._build_api.DownloadArtifact( build_target, build_id, resource_id, local_path, self._build_api.LATEST) complete = True finally: if not complete and os.path.isfile(local_path): os.remove(local_path) return local_path @utils.TimeExecute(function_description="Download Android Build artifacts") def _RetrieveArtifacts(self): """Retrieve goldfish images and tools from cache or Android Build API. Returns: An object of ArtifactPaths. Raises: errors.GetRemoteImageError: Fails to download rom images. errors.GetLocalImageError: Fails to validate local image zip. errors.GetSdkRepoPackageError: Fails to retrieve emulator zip. """ # Device images. if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: image_zip_path = self._RetrieveDeviceImageZip() elif self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL: image_zip_path = self._avd_spec.local_image_artifact if not image_zip_path or not zipfile.is_zipfile(image_zip_path): raise errors.GetLocalImageError( f"{image_zip_path or self._avd_spec.local_image_dir} is " "not an SDK repository zip.") else: raise errors.CreateError( f"Unknown image source: {self._avd_spec.image_source}") # Emulator tools. emu_zip_path = (self._avd_spec.emulator_zip or self._RetrieveEmulatorZip()) if not emu_zip_path: raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG) # System image. if self._avd_spec.local_system_image: # No known use case requires replacing system_ext and product. system_image_path = create_common.FindSystemImages( self._avd_spec.local_system_image).system else: system_image_path = self._RetrieveSystemImage() # system_dlkm image. if self._avd_spec.local_system_dlkm_image: system_dlkm_image_path = goldfish_utils.FindSystemDlkmImage( self._avd_spec.local_system_dlkm_image) else: # No known use case requires remote system_dlkm. system_dlkm_image_path = None # Boot image. if self._avd_spec.local_kernel_image: boot_image_path = create_common.FindBootImage( self._avd_spec.local_kernel_image) else: boot_image_path = self._RetrieveBootImage() # OTA tools. ota_tools_dir = None if system_image_path or system_dlkm_image_path or boot_image_path: if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: ota_tools_dir = self._RetrieveOtaTools() else: ota_tools_dir = ota_tools.FindOtaToolsDir( self._avd_spec.local_tool_dirs + create_common.GetNonEmptyEnvVars( constants.ENV_ANDROID_SOONG_HOST_OUT, constants.ENV_ANDROID_HOST_OUT)) return ArtifactPaths(image_zip_path, emu_zip_path, ota_tools_dir, system_image_path, system_dlkm_image_path, boot_image_path) def _RetrieveDeviceImageZip(self): """Retrieve device image zip from cache or Android Build API. Returns: The path to the device image zip in download_dir. """ build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) image_zip_name_format = (_EXTRA_IMAGE_ZIP_NAME_FORMAT if self._ShouldMixDiskImage() else _SDK_REPO_IMAGE_ZIP_NAME_FORMAT) return self._RetrieveArtifact( build_target, build_id, image_zip_name_format % {"build_id": build_id}) def _RetrieveEmulatorBuildID(self): """Retrieve required emulator build from a goldfish image build. Returns: A string, the emulator build ID. None if the build info is empty. """ build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) if build_id and build_target: emu_info_path = self._RetrieveArtifact(build_target, build_id, _EMULATOR_INFO_NAME) with open(emu_info_path, "r", encoding="utf-8") as emu_info: for line in emu_info: match = _EMULATOR_VERSION_PATTERN.fullmatch(line.strip()) if match: logger.info("Found emulator build ID: %s", line) return match.group("build_id") return None def _RetrieveEmulatorZip(self): """Retrieve emulator zip from cache or Android Build API. Returns: The path to the emulator zip in download_dir. None if this method cannot determine the emulator build ID. """ emu_build_id = (self._avd_spec.emulator_build_id or self._RetrieveEmulatorBuildID()) if not emu_build_id: return None emu_build_target = (self._avd_spec.emulator_build_target or self._avd_spec.cfg.emulator_build_target) emu_zip_name = self._InferEmulatorZipName(emu_build_target, emu_build_id) return self._RetrieveArtifact(emu_build_target, emu_build_id, emu_zip_name) def _RetrieveSystemImage(self): """Retrieve and unzip system image if system build info is not empty. Returns: The path to the temporary system image. None if the system build info is empty. """ build_id = self._avd_spec.system_build_info.get(constants.BUILD_ID) build_target = self._avd_spec.system_build_info.get( constants.BUILD_TARGET) if not build_id or not build_target: return None image_zip_name = _IMAGE_ZIP_NAME_FORMAT % { "build_target": build_target.split("-", 1)[0], "build_id": build_id} image_zip_path = self._RetrieveArtifact(build_target, build_id, image_zip_name) logger.debug("Unzip %s from %s to %s.", _SYSTEM_IMAGE_NAME, image_zip_path, self._artifact_dir) with zipfile.ZipFile(image_zip_path, "r") as zip_file: zip_file.extract(_SYSTEM_IMAGE_NAME, self._artifact_dir) return os.path.join(self._artifact_dir, _SYSTEM_IMAGE_NAME) def _RetrieveBootImage(self): """Retrieve boot image if boot build info is not empty. Returns: The path to the boot image in download_dir. None if the boot build info is empty. """ build_id = self._avd_spec.boot_build_info.get(constants.BUILD_ID) build_target = self._avd_spec.boot_build_info.get( constants.BUILD_TARGET) image_name = self._avd_spec.boot_build_info.get( constants.BUILD_ARTIFACT) if build_id and build_target and image_name: return self._RetrieveArtifact(build_target, build_id, image_name) return None def _RetrieveOtaTools(self): """Retrieve and unzip OTA tools. This method retrieves OTA tools from the goldfish build which contains mk_combined_img. Returns: The path to the temporary OTA tools directory. """ build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) zip_path = self._RetrieveArtifact(build_target, build_id, _OTA_TOOLS_ZIP_NAME) ota_tools_dir = os.path.join(self._artifact_dir, _OTA_TOOLS_DIR_NAME) logger.debug("Unzip %s to %s.", zip_path, ota_tools_dir) os.mkdir(ota_tools_dir) with zipfile.ZipFile(zip_path, "r") as zip_file: zip_file.extractall(ota_tools_dir) return ota_tools_dir @staticmethod def _GetSubdirNameInZip(zip_path): """Get the name of the only subdirectory in a zip. In an SDK repository zip, the images and the binaries are located in a subdirectory. This class needs to find out the subdirectory name in order to construct the remote commands. For example, in a sdk-repo-linux-system-images-*.zip for arm64, all files are in "arm64-v8a/". The zip entries are: arm64-v8a/NOTICE.txt arm64-v8a/system.img arm64-v8a/data/local.prop ... This method scans the entries and returns the common subdirectory name. """ sep = "/" with zipfile.ZipFile(zip_path, 'r') as zip_obj: entries = zip_obj.namelist() if len(entries) > 0 and sep in entries[0]: subdir = entries[0].split(sep, 1)[0] if all(e.startswith(subdir + sep) for e in entries): return subdir logger.warning("Expect one subdirectory in %s. Actual entries: %s", zip_path, " ".join(entries)) return "" def _UploadArtifacts(self, artifact_paths): """Process and upload all images and tools to the remote host. Args: artifact_paths: An object of ArtifactPaths. Returns: An object of RemotePaths. """ remote_emulator_dir, remote_image_dir = self._UploadDeviceImages( artifact_paths.emulator_zip, artifact_paths.image_zip) remote_kernel_path = None remote_ramdisk_path = None if (artifact_paths.boot_image or artifact_paths.system_image or artifact_paths.system_dlkm_image): with tempfile.TemporaryDirectory("host_gf") as temp_dir: ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir) image_dir = os.path.join(temp_dir, "images") logger.debug("Unzip %s.", artifact_paths.image_zip) with zipfile.ZipFile(artifact_paths.image_zip, "r") as zip_file: zip_file.extractall(image_dir) image_dir = os.path.join( image_dir, self._GetSubdirNameInZip(artifact_paths.image_zip)) if (artifact_paths.system_image or artifact_paths.system_dlkm_image): self._MixAndUploadDiskImage( remote_image_dir, image_dir, artifact_paths.system_image, artifact_paths.system_dlkm_image, ota) if artifact_paths.boot_image: remote_kernel_path, remote_ramdisk_path = ( self._MixAndUploadKernelImages( image_dir, artifact_paths.boot_image, ota)) return RemotePaths(remote_image_dir, remote_emulator_dir, remote_kernel_path, remote_ramdisk_path) def _ShouldMixDiskImage(self): """Determines whether a mixed disk image is required. This method checks whether the user requires to replace an image that is part of the disk image. Acloud supports replacing system, system_dlkm, and kernel images. system and system_dlkm are installed on the disk. Returns: Boolean, whether a mixed disk image is required. """ return (self._avd_spec.local_system_image or self._avd_spec.local_system_dlkm_image or (self._avd_spec.system_build_info.get(constants.BUILD_ID) and self._avd_spec.system_build_info.get(constants.BUILD_TARGET))) @utils.TimeExecute( function_description="Processing and uploading tools and images") def _UploadDeviceImages(self, emulator_zip_path, image_zip_path): """Upload artifacts to remote host and extract them. Args: emulator_zip_path: The local path to the emulator zip. image_zip_path: The local path to the image zip. Returns: The remote paths to the extracted emulator tools and images. """ remote_emulator_dir = self._GetInstancePath(_REMOTE_EMULATOR_DIR) remote_image_dir = self._GetInstancePath(_REMOTE_IMAGE_DIR) remote_emulator_zip_path = self._GetInstancePath( _REMOTE_EMULATOR_ZIP_PATH) remote_image_zip_path = self._GetInstancePath(_REMOTE_IMAGE_ZIP_PATH) self._ssh.Run(f"mkdir -p {remote_emulator_dir} {remote_image_dir}") self._ssh.ScpPushFile(emulator_zip_path, remote_emulator_zip_path) self._ssh.ScpPushFile(image_zip_path, remote_image_zip_path) self._ssh.Run(f"unzip -d {remote_emulator_dir} " f"{remote_emulator_zip_path}") self._ssh.Run(f"unzip -d {remote_image_dir} {remote_image_zip_path}") remote_emulator_subdir = remote_path.join( remote_emulator_dir, _SDK_REPO_EMULATOR_DIR_NAME) remote_image_subdir = remote_path.join( remote_image_dir, self._GetSubdirNameInZip(image_zip_path)) # 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. src_path = remote_path.join(remote_image_subdir, "build.prop") dst_path = remote_path.join(remote_image_subdir, "system", "build.prop") self._ssh.Run("'test -f %(dst)s || " "{ mkdir -p %(dst_dir)s && cp %(src)s %(dst)s ; }'" % {"src": src_path, "dst": dst_path, "dst_dir": remote_path.dirname(dst_path)}) return remote_emulator_subdir, remote_image_subdir def _MixAndUploadDiskImage(self, remote_image_dir, image_dir, system_image_path, system_dlkm_image_path, ota): """Mix emulator, system, and system_dlkm images and upload them. Args: remote_image_dir: The remote directory where the mixed disk image is uploaded. image_dir: The directory containing emulator images. system_image_path: The path to the system image. system_dlkm_image_path: The path to the system_dlkm image. ota: An instance of ota_tools.OtaTools. Returns: The remote path to the mixed disk image. """ with tempfile.TemporaryDirectory("host_gf_disk") as temp_dir: mixed_image = goldfish_utils.MixDiskImage( temp_dir, image_dir, system_image_path, system_dlkm_image_path, ota) # TODO(b/142228085): Use -system instead of overwriting the file. remote_disk_image_path = os.path.join( remote_image_dir, goldfish_utils.SYSTEM_QEMU_IMAGE_NAME) self._ssh.ScpPushFile(mixed_image, remote_disk_image_path) # Adding the parameter to remote VerifiedBootParams.textproto unlocks # the device so that the disabled vbmeta takes effect. An alternative # is to append the parameter to the kernel command line by # `emulator -qemu -append`, but that does not pass the compliance test. remote_params_path = remote_path.join( remote_image_dir, goldfish_utils.VERIFIED_BOOT_PARAMS_FILE_NAME) # \\n is interpreted by shell and echo. \" is interpreted by shell. param = r'\\nparam: \"androidboot.verifiedbootstate=orange\"' self._ssh.Run(f"'test -f {remote_params_path} && " f"echo -e {param} >> {remote_params_path}'") return remote_disk_image_path def _MixAndUploadKernelImages(self, image_dir, boot_image_path, ota): """Mix emulator kernel images with a boot image and upload them. Args: image_dir: The directory containing emulator images. boot_image_path: The path to the boot image. ota: An instance of ota_tools.OtaTools. Returns: The remote paths to the kernel image and the ramdisk image. """ remote_kernel_path = self._GetInstancePath(_REMOTE_KERNEL_PATH) remote_ramdisk_path = self._GetInstancePath(_REMOTE_RAMDISK_PATH) with tempfile.TemporaryDirectory("host_gf_kernel") as temp_dir: kernel_path, ramdisk_path = goldfish_utils.MixWithBootImage( temp_dir, image_dir, boot_image_path, ota) self._ssh.ScpPushFile(kernel_path, remote_kernel_path) self._ssh.ScpPushFile(ramdisk_path, remote_ramdisk_path) return remote_kernel_path, remote_ramdisk_path def _GetEmulatorLogs(self): """Return the logs created by the remote emulator command.""" return [report.LogFile(self._GetInstancePath(_REMOTE_STDOUT_PATH), constants.LOG_TYPE_KERNEL_LOG), report.LogFile(self._GetInstancePath(_REMOTE_STDERR_PATH), constants.LOG_TYPE_TEXT), report.LogFile(self._GetInstancePath(_REMOTE_LOGCAT_PATH), constants.LOG_TYPE_LOGCAT)] @utils.TimeExecute(function_description="Start emulator") def _StartEmulator(self, remote_paths): """Start emulator command as a remote background process. Args: remote_emulator_dir: The emulator tool directory on remote host. remote_image_dir: The image directory on remote host. """ remote_emulator_bin_path = remote_path.join( remote_paths.emulator_dir, _EMULATOR_BIN_NAME) remote_bin_paths = [ remote_path.join(remote_paths.emulator_dir, name) for name in _EMULATOR_BIN_DIR_NAMES] remote_bin_paths.append(remote_emulator_bin_path) self._ssh.Run("chmod -R +x %s" % " ".join(remote_bin_paths)) remote_runtime_dir = self._GetInstancePath(_REMOTE_RUNTIME_DIR) self._ssh.Run(f"mkdir -p {remote_runtime_dir}") env = {constants.ENV_ANDROID_PRODUCT_OUT: remote_paths.image_dir, constants.ENV_ANDROID_TMP: remote_runtime_dir, constants.ENV_ANDROID_BUILD_TOP: remote_runtime_dir} cmd = ["nohup", remote_emulator_bin_path, "-verbose", "-show-kernel", "-read-only", "-ports", str(self._GetConsolePort()) + "," + str(self.GetAdbPorts()[0]), "-no-window", "-logcat-output", self._GetInstancePath(_REMOTE_LOGCAT_PATH)] if remote_paths.kernel: cmd.extend(("-kernel", remote_paths.kernel)) if remote_paths.ramdisk: cmd.extend(("-ramdisk", remote_paths.ramdisk)) cmd.extend(goldfish_utils.ConvertAvdSpecToArgs(self._avd_spec)) # Emulator does not support -stdouterr-file on macOS. self._ssh.Run( "'export {env} ; {cmd} 1> {stdout} 2> {stderr} &'".format( env=" ".join(k + "=~/" + v for k, v in env.items()), cmd=" ".join(cmd), stdout=self._GetInstancePath(_REMOTE_STDOUT_PATH), stderr=self._GetInstancePath(_REMOTE_STDERR_PATH))) @utils.TimeExecute(function_description="Wait for emulator") def _WaitForEmulator(self): """Wait for remote emulator console to be active. Raises: errors.DeviceBootError if connection fails. errors.DeviceBootTimeoutError if boot times out. """ ip_addr = self._avd_spec.remote_host console_port = self._GetConsolePort() poll_timeout_secs = (self._avd_spec.boot_timeout_secs or _DEFAULT_BOOT_TIMEOUT_SECS) try: with emulator_console.RemoteEmulatorConsole( ip_addr, console_port, self._ssh_user, self._ssh_private_key_path, self._ssh_extra_args) as console: utils.PollAndWait( func=lambda: (True if console.Ping() else console.Reconnect()), expected_return=True, timeout_exception=errors.DeviceBootTimeoutError, timeout_secs=poll_timeout_secs, sleep_interval_secs=5) except errors.DeviceConnectionError as e: raise errors.DeviceBootError("Fail to connect to %s:%d." % (ip_addr, console_port)) from e def GetBuildInfoDict(self): """Get build info dictionary. Returns: A build info dictionary. """ build_info_dict = {key: val for key, val in self._avd_spec.remote_image.items() if val} return build_info_dict def GetAdbPorts(self): """Get ADB ports of the created devices. This class does not support --num-avds-per-instance. Returns: The port numbers as a list of integers. """ return [self._GetConsolePort() + 1] def GetFailures(self): """Get Failures from all devices. Returns: A dictionary the contains all the failures. The key is the name of the instance that fails to boot, and the value is an errors.DeviceBootError object. """ return self._failures def GetLogs(self): """Get log files of created instances. Returns: A dictionary that maps instance names to lists of report.LogFile. """ return self._logs