1# Copyright 2021 - The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""RemoteInstanceDeviceFactory provides basic interface to create a goldfish 16device factory.""" 17 18import collections 19import logging 20import os 21import posixpath as remote_path 22import re 23import shutil 24import subprocess 25import tempfile 26import time 27import zipfile 28 29from acloud import errors 30from acloud.create import create_common 31from acloud.internal import constants 32from acloud.internal.lib import android_build_client 33from acloud.internal.lib import auth 34from acloud.internal.lib import goldfish_utils 35from acloud.internal.lib import emulator_console 36from acloud.internal.lib import ota_tools 37from acloud.internal.lib import remote_host_client 38from acloud.internal.lib import utils 39from acloud.internal.lib import ssh 40from acloud.public import report 41from acloud.public.actions import base_device_factory 42 43 44logger = logging.getLogger(__name__) 45# Artifacts 46_SDK_REPO_IMAGE_ZIP_NAME_FORMAT = ("sdk-repo-linux-system-images-" 47 "%(build_id)s.zip") 48_EXTRA_IMAGE_ZIP_NAME_FORMAT = "emu-extra-linux-system-images-%(build_id)s.zip" 49_IMAGE_ZIP_NAME_FORMAT = "%(build_target)s-img-%(build_id)s.zip" 50_OTA_TOOLS_ZIP_NAME = "otatools.zip" 51_EMULATOR_INFO_NAME = "emulator-info.txt" 52_EMULATOR_VERSION_PATTERN = re.compile(r"require\s+version-emulator=" 53 r"(?P<build_id>\w+)") 54_EMULATOR_ZIP_NAME_FORMAT = "sdk-repo-%(os)s-emulator-%(build_id)s.zip" 55_EMULATOR_BIN_DIR_NAMES = ("bin64", "qemu") 56_EMULATOR_BIN_NAME = "emulator" 57_SDK_REPO_EMULATOR_DIR_NAME = "emulator" 58# Files in temporary artifact directory. 59_DOWNLOAD_DIR_NAME = "download" 60_OTA_TOOLS_DIR_NAME = "ota_tools" 61_SYSTEM_IMAGE_NAME = "system.img" 62# Base directory of an instance. 63_REMOTE_INSTANCE_DIR_FORMAT = "acloud_gf_%d" 64# Relative paths in a base directory. 65_REMOTE_IMAGE_ZIP_PATH = "image.zip" 66_REMOTE_EMULATOR_ZIP_PATH = "emulator.zip" 67_REMOTE_IMAGE_DIR = "image" 68_REMOTE_KERNEL_PATH = "kernel" 69_REMOTE_RAMDISK_PATH = "mixed_ramdisk" 70_REMOTE_EMULATOR_DIR = "emulator" 71_REMOTE_RUNTIME_DIR = "instance" 72_REMOTE_LOGCAT_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "logcat.txt") 73_REMOTE_STDOUT_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "kernel.log") 74_REMOTE_STDERR_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "emu_stderr.txt") 75# Runtime parameters 76_EMULATOR_DEFAULT_CONSOLE_PORT = 5554 77_DEFAULT_BOOT_TIMEOUT_SECS = 150 78# Error messages 79_MISSING_EMULATOR_MSG = ("No emulator zip. Specify " 80 "--emulator-build-id, or --emulator-zip.") 81 82ArtifactPaths = collections.namedtuple( 83 "ArtifactPaths", 84 ["image_zip", "emulator_zip", "ota_tools_dir", 85 "system_image", "system_dlkm_image", "boot_image"]) 86 87RemotePaths = collections.namedtuple( 88 "RemotePaths", 89 ["image_dir", "emulator_dir", "kernel", "ramdisk"]) 90 91 92class RemoteHostGoldfishDeviceFactory(base_device_factory.BaseDeviceFactory): 93 """A class that creates a goldfish device on a remote host. 94 95 Attributes: 96 avd_spec: AVDSpec object that tells us what we're going to create. 97 android_build_client: An AndroidBuildClient that is lazily initialized. 98 temp_artifact_dir: The temporary artifact directory that is lazily 99 initialized during PrepareArtifacts. 100 ssh: Ssh object that executes commands on the remote host. 101 failures: A dictionary the maps instance names to 102 error.DeviceBootError objects. 103 logs: A dictionary that maps instance names to lists of report.LogFile. 104 """ 105 def __init__(self, avd_spec): 106 """Initialize the attributes and the compute client.""" 107 self._avd_spec = avd_spec 108 self._android_build_client = None 109 self._temp_artifact_dir = None 110 self._ssh = ssh.Ssh( 111 ip=ssh.IP(ip=self._avd_spec.remote_host), 112 user=self._ssh_user, 113 ssh_private_key_path=self._ssh_private_key_path, 114 extra_args_ssh_tunnel=self._ssh_extra_args, 115 report_internal_ip=False) 116 self._failures = {} 117 self._logs = {} 118 super().__init__(compute_client=( 119 remote_host_client.RemoteHostClient(avd_spec.remote_host))) 120 121 @property 122 def _build_api(self): 123 """Initialize android_build_client.""" 124 if not self._android_build_client: 125 credentials = auth.CreateCredentials(self._avd_spec.cfg) 126 self._android_build_client = android_build_client.AndroidBuildClient( 127 credentials) 128 return self._android_build_client 129 130 @property 131 def _artifact_dir(self): 132 """Initialize temp_artifact_dir.""" 133 if not self._temp_artifact_dir: 134 self._temp_artifact_dir = tempfile.mkdtemp("host_gf") 135 logger.info("Create temporary artifact directory: %s", 136 self._temp_artifact_dir) 137 return self._temp_artifact_dir 138 139 @property 140 def _download_dir(self): 141 """Get the directory where the artifacts are downloaded.""" 142 if self._avd_spec.image_download_dir: 143 return self._avd_spec.image_download_dir 144 return os.path.join(self._artifact_dir, _DOWNLOAD_DIR_NAME) 145 146 @property 147 def _ssh_user(self): 148 return self._avd_spec.host_user or constants.GCE_USER 149 150 @property 151 def _ssh_private_key_path(self): 152 return (self._avd_spec.host_ssh_private_key_path or 153 self._avd_spec.cfg.ssh_private_key_path) 154 155 @property 156 def _ssh_extra_args(self): 157 return self._avd_spec.cfg.extra_args_ssh_tunnel 158 159 def _GetConsolePort(self): 160 """Calculate the console port from the instance number. 161 162 By convention, the console port is an even number, and the adb port is 163 the console port + 1. The first instance uses port 5554 and 5555. The 164 second instance uses 5556 and 5557, and so on. 165 """ 166 return (_EMULATOR_DEFAULT_CONSOLE_PORT + 167 ((self._avd_spec.base_instance_num or 1) - 1) * 2) 168 169 def _GetInstancePath(self, relative_path): 170 """Append a relative path to the instance directory.""" 171 return remote_path.join( 172 _REMOTE_INSTANCE_DIR_FORMAT % 173 (self._avd_spec.base_instance_num or 1), 174 relative_path) 175 176 def CreateInstance(self): 177 """Create a goldfish instance on the remote host. 178 179 Returns: 180 The instance name. 181 """ 182 instance_name = goldfish_utils.FormatRemoteHostInstanceName( 183 self._avd_spec.remote_host, 184 self._GetConsolePort(), 185 self._avd_spec.remote_image) 186 187 client = self.GetComputeClient() 188 timed_stage = constants.TIME_GCE 189 start_time = time.time() 190 try: 191 client.SetStage(constants.STAGE_SSH_CONNECT) 192 self._InitRemoteHost() 193 194 start_time = client.RecordTime(timed_stage, start_time) 195 timed_stage = constants.TIME_ARTIFACT 196 client.SetStage(constants.STAGE_ARTIFACT) 197 remote_paths = self._PrepareArtifacts() 198 199 start_time = client.RecordTime(timed_stage, start_time) 200 timed_stage = constants.TIME_LAUNCH 201 client.SetStage(constants.STAGE_BOOT_UP) 202 self._logs[instance_name] = self._GetEmulatorLogs() 203 self._StartEmulator(remote_paths) 204 self._WaitForEmulator() 205 except (errors.DriverError, subprocess.CalledProcessError) as e: 206 # Catch the generic runtime error and CalledProcessError which is 207 # raised by the ssh module. 208 self._failures[instance_name] = e 209 finally: 210 client.RecordTime(timed_stage, start_time) 211 212 return instance_name 213 214 def _InitRemoteHost(self): 215 """Remove the existing instance and the instance directory.""" 216 # Disable authentication for emulator console. 217 self._ssh.Run("""'echo -n "" > .emulator_console_auth_token'""") 218 try: 219 with emulator_console.RemoteEmulatorConsole( 220 self._avd_spec.remote_host, 221 self._GetConsolePort(), 222 self._ssh_user, 223 self._ssh_private_key_path, 224 self._ssh_extra_args) as console: 225 console.Kill() 226 logger.info("Killed existing emulator.") 227 except errors.DeviceConnectionError as e: 228 logger.info("Did not kill existing emulator: %s", str(e)) 229 # Delete instance files. 230 self._ssh.Run(f"rm -rf {self._GetInstancePath('')}") 231 232 def _PrepareArtifacts(self): 233 """Prepare artifacts on remote host. 234 235 This method retrieves artifacts from cache or Android Build API and 236 uploads them to the remote host. 237 238 Returns: 239 An object of RemotePaths. 240 """ 241 try: 242 artifact_paths = self._RetrieveArtifacts() 243 return self._UploadArtifacts(artifact_paths) 244 finally: 245 if self._temp_artifact_dir: 246 shutil.rmtree(self._temp_artifact_dir, ignore_errors=True) 247 self._temp_artifact_dir = None 248 249 @staticmethod 250 def _InferEmulatorZipName(build_target, build_id): 251 """Determine the emulator zip name in build artifacts. 252 253 The emulator zip name is composed of build variables that are not 254 revealed in the artifacts. This method infers the emulator zip name 255 from its build target name. 256 257 Args: 258 build_target: The emulator build target name, e.g., 259 "emulator-linux_x64_nolocationui", "aarch64_sdk_tools_mac". 260 build_id: A string, the emulator build ID. 261 262 Returns: 263 The name of the emulator zip. e.g., 264 "sdk-repo-linux-emulator-123456.zip", 265 "sdk-repo-darwin_aarch64-emulator-123456.zip". 266 """ 267 split_target = [x for product_variant in build_target.split("-") 268 for x in product_variant.split("_")] 269 if "darwin" in split_target or "mac" in split_target: 270 os_name = "darwin" 271 else: 272 os_name = "linux" 273 if "aarch64" in split_target: 274 os_name = os_name + "_aarch64" 275 return _EMULATOR_ZIP_NAME_FORMAT % {"os": os_name, 276 "build_id": build_id} 277 278 def _RetrieveArtifact(self, build_target, build_id, 279 resource_id): 280 """Retrieve an artifact from cache or Android Build API. 281 282 Args: 283 build_target: A string, the build target of the artifact. e.g., 284 "sdk_phone_x86_64-userdebug". 285 build_id: A string, the build ID of the artifact. 286 resource_id: A string, the name of the artifact. e.g., 287 "sdk-repo-linux-system-images-123456.zip". 288 289 Returns: 290 The path to the artifact in download_dir. 291 """ 292 local_path = os.path.join(self._download_dir, build_id, build_target, 293 resource_id) 294 if os.path.isfile(local_path): 295 logger.info("Skip downloading existing artifact: %s", local_path) 296 return local_path 297 298 complete = False 299 try: 300 os.makedirs(os.path.dirname(local_path), exist_ok=True) 301 self._build_api.DownloadArtifact( 302 build_target, build_id, resource_id, local_path, 303 self._build_api.LATEST) 304 complete = True 305 finally: 306 if not complete and os.path.isfile(local_path): 307 os.remove(local_path) 308 return local_path 309 310 @utils.TimeExecute(function_description="Download Android Build artifacts") 311 def _RetrieveArtifacts(self): 312 """Retrieve goldfish images and tools from cache or Android Build API. 313 314 Returns: 315 An object of ArtifactPaths. 316 317 Raises: 318 errors.GetRemoteImageError: Fails to download rom images. 319 errors.GetLocalImageError: Fails to validate local image zip. 320 errors.GetSdkRepoPackageError: Fails to retrieve emulator zip. 321 """ 322 # Device images. 323 if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: 324 image_zip_path = self._RetrieveDeviceImageZip() 325 elif self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL: 326 image_zip_path = self._avd_spec.local_image_artifact 327 if not image_zip_path or not zipfile.is_zipfile(image_zip_path): 328 raise errors.GetLocalImageError( 329 f"{image_zip_path or self._avd_spec.local_image_dir} is " 330 "not an SDK repository zip.") 331 else: 332 raise errors.CreateError( 333 f"Unknown image source: {self._avd_spec.image_source}") 334 335 # Emulator tools. 336 emu_zip_path = (self._avd_spec.emulator_zip or 337 self._RetrieveEmulatorZip()) 338 if not emu_zip_path: 339 raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG) 340 341 # System image. 342 if self._avd_spec.local_system_image: 343 # No known use case requires replacing system_ext and product. 344 system_image_path = create_common.FindSystemImages( 345 self._avd_spec.local_system_image).system 346 else: 347 system_image_path = self._RetrieveSystemImage() 348 349 # system_dlkm image. 350 if self._avd_spec.local_system_dlkm_image: 351 system_dlkm_image_path = goldfish_utils.FindSystemDlkmImage( 352 self._avd_spec.local_system_dlkm_image) 353 else: 354 # No known use case requires remote system_dlkm. 355 system_dlkm_image_path = None 356 357 # Boot image. 358 if self._avd_spec.local_kernel_image: 359 boot_image_path = create_common.FindBootImage( 360 self._avd_spec.local_kernel_image) 361 else: 362 boot_image_path = self._RetrieveBootImage() 363 364 # OTA tools. 365 ota_tools_dir = None 366 if system_image_path or system_dlkm_image_path or boot_image_path: 367 if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: 368 ota_tools_dir = self._RetrieveOtaTools() 369 else: 370 ota_tools_dir = ota_tools.FindOtaToolsDir( 371 self._avd_spec.local_tool_dirs + 372 create_common.GetNonEmptyEnvVars( 373 constants.ENV_ANDROID_SOONG_HOST_OUT, 374 constants.ENV_ANDROID_HOST_OUT)) 375 376 return ArtifactPaths(image_zip_path, emu_zip_path, ota_tools_dir, 377 system_image_path, system_dlkm_image_path, 378 boot_image_path) 379 380 def _RetrieveDeviceImageZip(self): 381 """Retrieve device image zip from cache or Android Build API. 382 383 Returns: 384 The path to the device image zip in download_dir. 385 """ 386 build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) 387 build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) 388 image_zip_name_format = (_EXTRA_IMAGE_ZIP_NAME_FORMAT if 389 self._ShouldMixDiskImage() else 390 _SDK_REPO_IMAGE_ZIP_NAME_FORMAT) 391 return self._RetrieveArtifact( 392 build_target, build_id, 393 image_zip_name_format % {"build_id": build_id}) 394 395 def _RetrieveEmulatorBuildID(self): 396 """Retrieve required emulator build from a goldfish image build. 397 398 Returns: 399 A string, the emulator build ID. 400 None if the build info is empty. 401 """ 402 build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) 403 build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) 404 if build_id and build_target: 405 emu_info_path = self._RetrieveArtifact(build_target, build_id, 406 _EMULATOR_INFO_NAME) 407 with open(emu_info_path, "r", encoding="utf-8") as emu_info: 408 for line in emu_info: 409 match = _EMULATOR_VERSION_PATTERN.fullmatch(line.strip()) 410 if match: 411 logger.info("Found emulator build ID: %s", line) 412 return match.group("build_id") 413 return None 414 415 def _RetrieveEmulatorZip(self): 416 """Retrieve emulator zip from cache or Android Build API. 417 418 Returns: 419 The path to the emulator zip in download_dir. 420 None if this method cannot determine the emulator build ID. 421 """ 422 emu_build_id = (self._avd_spec.emulator_build_id or 423 self._RetrieveEmulatorBuildID()) 424 if not emu_build_id: 425 return None 426 emu_build_target = (self._avd_spec.emulator_build_target or 427 self._avd_spec.cfg.emulator_build_target) 428 emu_zip_name = self._InferEmulatorZipName(emu_build_target, 429 emu_build_id) 430 return self._RetrieveArtifact(emu_build_target, emu_build_id, 431 emu_zip_name) 432 433 def _RetrieveSystemImage(self): 434 """Retrieve and unzip system image if system build info is not empty. 435 436 Returns: 437 The path to the temporary system image. 438 None if the system build info is empty. 439 """ 440 build_id = self._avd_spec.system_build_info.get(constants.BUILD_ID) 441 build_target = self._avd_spec.system_build_info.get( 442 constants.BUILD_TARGET) 443 if not build_id or not build_target: 444 return None 445 image_zip_name = _IMAGE_ZIP_NAME_FORMAT % { 446 "build_target": build_target.split("-", 1)[0], 447 "build_id": build_id} 448 image_zip_path = self._RetrieveArtifact(build_target, build_id, 449 image_zip_name) 450 logger.debug("Unzip %s from %s to %s.", 451 _SYSTEM_IMAGE_NAME, image_zip_path, self._artifact_dir) 452 with zipfile.ZipFile(image_zip_path, "r") as zip_file: 453 zip_file.extract(_SYSTEM_IMAGE_NAME, self._artifact_dir) 454 return os.path.join(self._artifact_dir, _SYSTEM_IMAGE_NAME) 455 456 def _RetrieveBootImage(self): 457 """Retrieve boot image if boot build info is not empty. 458 459 Returns: 460 The path to the boot image in download_dir. 461 None if the boot build info is empty. 462 """ 463 build_id = self._avd_spec.boot_build_info.get(constants.BUILD_ID) 464 build_target = self._avd_spec.boot_build_info.get( 465 constants.BUILD_TARGET) 466 image_name = self._avd_spec.boot_build_info.get( 467 constants.BUILD_ARTIFACT) 468 if build_id and build_target and image_name: 469 return self._RetrieveArtifact(build_target, build_id, image_name) 470 return None 471 472 def _RetrieveOtaTools(self): 473 """Retrieve and unzip OTA tools. 474 475 This method retrieves OTA tools from the goldfish build which contains 476 mk_combined_img. 477 478 Returns: 479 The path to the temporary OTA tools directory. 480 """ 481 build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) 482 build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) 483 zip_path = self._RetrieveArtifact(build_target, build_id, 484 _OTA_TOOLS_ZIP_NAME) 485 ota_tools_dir = os.path.join(self._artifact_dir, _OTA_TOOLS_DIR_NAME) 486 logger.debug("Unzip %s to %s.", zip_path, ota_tools_dir) 487 os.mkdir(ota_tools_dir) 488 with zipfile.ZipFile(zip_path, "r") as zip_file: 489 zip_file.extractall(ota_tools_dir) 490 return ota_tools_dir 491 492 @staticmethod 493 def _GetSubdirNameInZip(zip_path): 494 """Get the name of the only subdirectory in a zip. 495 496 In an SDK repository zip, the images and the binaries are located in a 497 subdirectory. This class needs to find out the subdirectory name in 498 order to construct the remote commands. 499 500 For example, in a sdk-repo-linux-system-images-*.zip for arm64, all 501 files are in "arm64-v8a/". The zip entries are: 502 503 arm64-v8a/NOTICE.txt 504 arm64-v8a/system.img 505 arm64-v8a/data/local.prop 506 ... 507 508 This method scans the entries and returns the common subdirectory name. 509 """ 510 sep = "/" 511 with zipfile.ZipFile(zip_path, 'r') as zip_obj: 512 entries = zip_obj.namelist() 513 if len(entries) > 0 and sep in entries[0]: 514 subdir = entries[0].split(sep, 1)[0] 515 if all(e.startswith(subdir + sep) for e in entries): 516 return subdir 517 logger.warning("Expect one subdirectory in %s. Actual entries: %s", 518 zip_path, " ".join(entries)) 519 return "" 520 521 def _UploadArtifacts(self, artifact_paths): 522 """Process and upload all images and tools to the remote host. 523 524 Args: 525 artifact_paths: An object of ArtifactPaths. 526 527 Returns: 528 An object of RemotePaths. 529 """ 530 remote_emulator_dir, remote_image_dir = self._UploadDeviceImages( 531 artifact_paths.emulator_zip, artifact_paths.image_zip) 532 533 remote_kernel_path = None 534 remote_ramdisk_path = None 535 536 if (artifact_paths.boot_image or artifact_paths.system_image or 537 artifact_paths.system_dlkm_image): 538 with tempfile.TemporaryDirectory("host_gf") as temp_dir: 539 ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir) 540 541 image_dir = os.path.join(temp_dir, "images") 542 logger.debug("Unzip %s.", artifact_paths.image_zip) 543 with zipfile.ZipFile(artifact_paths.image_zip, 544 "r") as zip_file: 545 zip_file.extractall(image_dir) 546 image_dir = os.path.join( 547 image_dir, 548 self._GetSubdirNameInZip(artifact_paths.image_zip)) 549 550 if (artifact_paths.system_image or 551 artifact_paths.system_dlkm_image): 552 self._MixAndUploadDiskImage( 553 remote_image_dir, image_dir, 554 artifact_paths.system_image, 555 artifact_paths.system_dlkm_image, ota) 556 557 if artifact_paths.boot_image: 558 remote_kernel_path, remote_ramdisk_path = ( 559 self._MixAndUploadKernelImages( 560 image_dir, artifact_paths.boot_image, ota)) 561 562 return RemotePaths(remote_image_dir, remote_emulator_dir, 563 remote_kernel_path, remote_ramdisk_path) 564 565 def _ShouldMixDiskImage(self): 566 """Determines whether a mixed disk image is required. 567 568 This method checks whether the user requires to replace an image that 569 is part of the disk image. Acloud supports replacing system, 570 system_dlkm, and kernel images. system and system_dlkm are installed 571 on the disk. 572 573 Returns: 574 Boolean, whether a mixed disk image is required. 575 """ 576 return (self._avd_spec.local_system_image or 577 self._avd_spec.local_system_dlkm_image or 578 (self._avd_spec.system_build_info.get(constants.BUILD_ID) and 579 self._avd_spec.system_build_info.get(constants.BUILD_TARGET))) 580 581 @utils.TimeExecute( 582 function_description="Processing and uploading tools and images") 583 def _UploadDeviceImages(self, emulator_zip_path, image_zip_path): 584 """Upload artifacts to remote host and extract them. 585 586 Args: 587 emulator_zip_path: The local path to the emulator zip. 588 image_zip_path: The local path to the image zip. 589 590 Returns: 591 The remote paths to the extracted emulator tools and images. 592 """ 593 remote_emulator_dir = self._GetInstancePath(_REMOTE_EMULATOR_DIR) 594 remote_image_dir = self._GetInstancePath(_REMOTE_IMAGE_DIR) 595 remote_emulator_zip_path = self._GetInstancePath( 596 _REMOTE_EMULATOR_ZIP_PATH) 597 remote_image_zip_path = self._GetInstancePath(_REMOTE_IMAGE_ZIP_PATH) 598 self._ssh.Run(f"mkdir -p {remote_emulator_dir} {remote_image_dir}") 599 self._ssh.ScpPushFile(emulator_zip_path, remote_emulator_zip_path) 600 self._ssh.ScpPushFile(image_zip_path, remote_image_zip_path) 601 602 self._ssh.Run(f"unzip -d {remote_emulator_dir} " 603 f"{remote_emulator_zip_path}") 604 self._ssh.Run(f"unzip -d {remote_image_dir} {remote_image_zip_path}") 605 remote_emulator_subdir = remote_path.join( 606 remote_emulator_dir, _SDK_REPO_EMULATOR_DIR_NAME) 607 remote_image_subdir = remote_path.join( 608 remote_image_dir, self._GetSubdirNameInZip(image_zip_path)) 609 # TODO(b/141898893): In Android build environment, emulator gets build 610 # information from $ANDROID_PRODUCT_OUT/system/build.prop. 611 # If image_dir is an extacted SDK repository, the file is at 612 # image_dir/build.prop. Acloud copies it to 613 # image_dir/system/build.prop. 614 src_path = remote_path.join(remote_image_subdir, "build.prop") 615 dst_path = remote_path.join(remote_image_subdir, "system", 616 "build.prop") 617 self._ssh.Run("'test -f %(dst)s || " 618 "{ mkdir -p %(dst_dir)s && cp %(src)s %(dst)s ; }'" % 619 {"src": src_path, 620 "dst": dst_path, 621 "dst_dir": remote_path.dirname(dst_path)}) 622 return remote_emulator_subdir, remote_image_subdir 623 624 def _MixAndUploadDiskImage(self, remote_image_dir, image_dir, 625 system_image_path, system_dlkm_image_path, ota): 626 """Mix emulator, system, and system_dlkm images and upload them. 627 628 Args: 629 remote_image_dir: The remote directory where the mixed disk image 630 is uploaded. 631 image_dir: The directory containing emulator images. 632 system_image_path: The path to the system image. 633 system_dlkm_image_path: The path to the system_dlkm image. 634 ota: An instance of ota_tools.OtaTools. 635 636 Returns: 637 The remote path to the mixed disk image. 638 """ 639 with tempfile.TemporaryDirectory("host_gf_disk") as temp_dir: 640 mixed_image = goldfish_utils.MixDiskImage( 641 temp_dir, image_dir, system_image_path, system_dlkm_image_path, 642 ota) 643 644 # TODO(b/142228085): Use -system instead of overwriting the file. 645 remote_disk_image_path = os.path.join( 646 remote_image_dir, goldfish_utils.SYSTEM_QEMU_IMAGE_NAME) 647 self._ssh.ScpPushFile(mixed_image, remote_disk_image_path) 648 649 # Adding the parameter to remote VerifiedBootParams.textproto unlocks 650 # the device so that the disabled vbmeta takes effect. An alternative 651 # is to append the parameter to the kernel command line by 652 # `emulator -qemu -append`, but that does not pass the compliance test. 653 remote_params_path = remote_path.join( 654 remote_image_dir, goldfish_utils.VERIFIED_BOOT_PARAMS_FILE_NAME) 655 # \\n is interpreted by shell and echo. \" is interpreted by shell. 656 param = r'\\nparam: \"androidboot.verifiedbootstate=orange\"' 657 self._ssh.Run(f"'test -f {remote_params_path} && " 658 f"echo -e {param} >> {remote_params_path}'") 659 660 return remote_disk_image_path 661 662 def _MixAndUploadKernelImages(self, image_dir, boot_image_path, ota): 663 """Mix emulator kernel images with a boot image and upload them. 664 665 Args: 666 image_dir: The directory containing emulator images. 667 boot_image_path: The path to the boot image. 668 ota: An instance of ota_tools.OtaTools. 669 670 Returns: 671 The remote paths to the kernel image and the ramdisk image. 672 """ 673 remote_kernel_path = self._GetInstancePath(_REMOTE_KERNEL_PATH) 674 remote_ramdisk_path = self._GetInstancePath(_REMOTE_RAMDISK_PATH) 675 with tempfile.TemporaryDirectory("host_gf_kernel") as temp_dir: 676 kernel_path, ramdisk_path = goldfish_utils.MixWithBootImage( 677 temp_dir, image_dir, boot_image_path, ota) 678 679 self._ssh.ScpPushFile(kernel_path, remote_kernel_path) 680 self._ssh.ScpPushFile(ramdisk_path, remote_ramdisk_path) 681 682 return remote_kernel_path, remote_ramdisk_path 683 684 def _GetEmulatorLogs(self): 685 """Return the logs created by the remote emulator command.""" 686 return [report.LogFile(self._GetInstancePath(_REMOTE_STDOUT_PATH), 687 constants.LOG_TYPE_KERNEL_LOG), 688 report.LogFile(self._GetInstancePath(_REMOTE_STDERR_PATH), 689 constants.LOG_TYPE_TEXT), 690 report.LogFile(self._GetInstancePath(_REMOTE_LOGCAT_PATH), 691 constants.LOG_TYPE_LOGCAT)] 692 693 @utils.TimeExecute(function_description="Start emulator") 694 def _StartEmulator(self, remote_paths): 695 """Start emulator command as a remote background process. 696 697 Args: 698 remote_emulator_dir: The emulator tool directory on remote host. 699 remote_image_dir: The image directory on remote host. 700 """ 701 remote_emulator_bin_path = remote_path.join( 702 remote_paths.emulator_dir, _EMULATOR_BIN_NAME) 703 remote_bin_paths = [ 704 remote_path.join(remote_paths.emulator_dir, name) for 705 name in _EMULATOR_BIN_DIR_NAMES] 706 remote_bin_paths.append(remote_emulator_bin_path) 707 self._ssh.Run("chmod -R +x %s" % " ".join(remote_bin_paths)) 708 709 remote_runtime_dir = self._GetInstancePath(_REMOTE_RUNTIME_DIR) 710 self._ssh.Run(f"mkdir -p {remote_runtime_dir}") 711 env = {constants.ENV_ANDROID_PRODUCT_OUT: remote_paths.image_dir, 712 constants.ENV_ANDROID_TMP: remote_runtime_dir, 713 constants.ENV_ANDROID_BUILD_TOP: remote_runtime_dir} 714 cmd = ["nohup", remote_emulator_bin_path, "-verbose", "-show-kernel", 715 "-read-only", "-ports", 716 str(self._GetConsolePort()) + "," + str(self.GetAdbPorts()[0]), 717 "-no-window", 718 "-logcat-output", self._GetInstancePath(_REMOTE_LOGCAT_PATH)] 719 720 if remote_paths.kernel: 721 cmd.extend(("-kernel", remote_paths.kernel)) 722 723 if remote_paths.ramdisk: 724 cmd.extend(("-ramdisk", remote_paths.ramdisk)) 725 726 cmd.extend(goldfish_utils.ConvertAvdSpecToArgs(self._avd_spec)) 727 728 # Emulator does not support -stdouterr-file on macOS. 729 self._ssh.Run( 730 "'export {env} ; {cmd} 1> {stdout} 2> {stderr} &'".format( 731 env=" ".join(k + "=~/" + v for k, v in env.items()), 732 cmd=" ".join(cmd), 733 stdout=self._GetInstancePath(_REMOTE_STDOUT_PATH), 734 stderr=self._GetInstancePath(_REMOTE_STDERR_PATH))) 735 736 @utils.TimeExecute(function_description="Wait for emulator") 737 def _WaitForEmulator(self): 738 """Wait for remote emulator console to be active. 739 740 Raises: 741 errors.DeviceBootError if connection fails. 742 errors.DeviceBootTimeoutError if boot times out. 743 """ 744 ip_addr = self._avd_spec.remote_host 745 console_port = self._GetConsolePort() 746 poll_timeout_secs = (self._avd_spec.boot_timeout_secs or 747 _DEFAULT_BOOT_TIMEOUT_SECS) 748 try: 749 with emulator_console.RemoteEmulatorConsole( 750 ip_addr, 751 console_port, 752 self._ssh_user, 753 self._ssh_private_key_path, 754 self._ssh_extra_args) as console: 755 utils.PollAndWait( 756 func=lambda: (True if console.Ping() else 757 console.Reconnect()), 758 expected_return=True, 759 timeout_exception=errors.DeviceBootTimeoutError, 760 timeout_secs=poll_timeout_secs, 761 sleep_interval_secs=5) 762 except errors.DeviceConnectionError as e: 763 raise errors.DeviceBootError("Fail to connect to %s:%d." % 764 (ip_addr, console_port)) from e 765 766 def GetBuildInfoDict(self): 767 """Get build info dictionary. 768 769 Returns: 770 A build info dictionary. 771 """ 772 build_info_dict = {key: val for key, val in 773 self._avd_spec.remote_image.items() if val} 774 return build_info_dict 775 776 def GetAdbPorts(self): 777 """Get ADB ports of the created devices. 778 779 This class does not support --num-avds-per-instance. 780 781 Returns: 782 The port numbers as a list of integers. 783 """ 784 return [self._GetConsolePort() + 1] 785 786 def GetFailures(self): 787 """Get Failures from all devices. 788 789 Returns: 790 A dictionary the contains all the failures. 791 The key is the name of the instance that fails to boot, 792 and the value is an errors.DeviceBootError object. 793 """ 794 return self._failures 795 796 def GetLogs(self): 797 """Get log files of created instances. 798 799 Returns: 800 A dictionary that maps instance names to lists of report.LogFile. 801 """ 802 return self._logs 803