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, 206 subprocess.TimeoutExpired) as e: 207 # Catch the generic runtime error and CalledProcessError which is 208 # raised by the ssh module. 209 self._failures[instance_name] = e 210 finally: 211 client.RecordTime(timed_stage, start_time) 212 213 return instance_name 214 215 def _InitRemoteHost(self): 216 """Remove the existing instance and the instance directory.""" 217 # Disable authentication for emulator console. 218 self._ssh.Run("""'echo -n "" > .emulator_console_auth_token'""") 219 try: 220 with emulator_console.RemoteEmulatorConsole( 221 self._avd_spec.remote_host, 222 self._GetConsolePort(), 223 self._ssh_user, 224 self._ssh_private_key_path, 225 self._ssh_extra_args) as console: 226 console.Kill() 227 logger.info("Killed existing emulator.") 228 except errors.DeviceConnectionError as e: 229 logger.info("Did not kill existing emulator: %s", str(e)) 230 # Delete instance files. 231 self._ssh.Run(f"rm -rf {self._GetInstancePath('')}") 232 233 def _PrepareArtifacts(self): 234 """Prepare artifacts on remote host. 235 236 This method retrieves artifacts from cache or Android Build API and 237 uploads them to the remote host. 238 239 Returns: 240 An object of RemotePaths. 241 """ 242 try: 243 artifact_paths = self._RetrieveArtifacts() 244 return self._UploadArtifacts(artifact_paths) 245 finally: 246 if self._temp_artifact_dir: 247 shutil.rmtree(self._temp_artifact_dir, ignore_errors=True) 248 self._temp_artifact_dir = None 249 250 @staticmethod 251 def _InferEmulatorZipName(build_target, build_id): 252 """Determine the emulator zip name in build artifacts. 253 254 The emulator zip name is composed of build variables that are not 255 revealed in the artifacts. This method infers the emulator zip name 256 from its build target name. 257 258 Args: 259 build_target: The emulator build target name, e.g., 260 "emulator-linux_x64_nolocationui", "aarch64_sdk_tools_mac". 261 build_id: A string, the emulator build ID. 262 263 Returns: 264 The name of the emulator zip. e.g., 265 "sdk-repo-linux-emulator-123456.zip", 266 "sdk-repo-darwin_aarch64-emulator-123456.zip". 267 """ 268 split_target = [x for product_variant in build_target.split("-") 269 for x in product_variant.split("_")] 270 if "darwin" in split_target or "mac" in split_target: 271 os_name = "darwin" 272 else: 273 os_name = "linux" 274 if "aarch64" in split_target: 275 os_name = os_name + "_aarch64" 276 return _EMULATOR_ZIP_NAME_FORMAT % {"os": os_name, 277 "build_id": build_id} 278 279 def _RetrieveArtifact(self, build_target, build_id, 280 resource_id): 281 """Retrieve an artifact from cache or Android Build API. 282 283 Args: 284 build_target: A string, the build target of the artifact. e.g., 285 "sdk_phone_x86_64-userdebug". 286 build_id: A string, the build ID of the artifact. 287 resource_id: A string, the name of the artifact. e.g., 288 "sdk-repo-linux-system-images-123456.zip". 289 290 Returns: 291 The path to the artifact in download_dir. 292 """ 293 local_path = os.path.join(self._download_dir, build_id, build_target, 294 resource_id) 295 if os.path.isfile(local_path): 296 logger.info("Skip downloading existing artifact: %s", local_path) 297 return local_path 298 299 complete = False 300 try: 301 os.makedirs(os.path.dirname(local_path), exist_ok=True) 302 self._build_api.DownloadArtifact( 303 build_target, build_id, resource_id, local_path, 304 self._build_api.LATEST) 305 complete = True 306 finally: 307 if not complete and os.path.isfile(local_path): 308 os.remove(local_path) 309 return local_path 310 311 @utils.TimeExecute(function_description="Download Android Build artifacts") 312 def _RetrieveArtifacts(self): 313 """Retrieve goldfish images and tools from cache or Android Build API. 314 315 Returns: 316 An object of ArtifactPaths. 317 318 Raises: 319 errors.GetRemoteImageError: Fails to download rom images. 320 errors.GetLocalImageError: Fails to validate local image zip. 321 errors.GetSdkRepoPackageError: Fails to retrieve emulator zip. 322 """ 323 # Device images. 324 if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: 325 image_zip_path = self._RetrieveDeviceImageZip() 326 elif self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL: 327 image_zip_path = self._avd_spec.local_image_artifact 328 if not image_zip_path or not zipfile.is_zipfile(image_zip_path): 329 raise errors.GetLocalImageError( 330 f"{image_zip_path or self._avd_spec.local_image_dir} is " 331 "not an SDK repository zip.") 332 else: 333 raise errors.CreateError( 334 f"Unknown image source: {self._avd_spec.image_source}") 335 336 # Emulator tools. 337 emu_zip_path = (self._avd_spec.emulator_zip or 338 self._RetrieveEmulatorZip()) 339 if not emu_zip_path: 340 raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG) 341 342 # System image. 343 if self._avd_spec.local_system_image: 344 # No known use case requires replacing system_ext and product. 345 system_image_path = create_common.FindSystemImages( 346 self._avd_spec.local_system_image).system 347 else: 348 system_image_path = self._RetrieveSystemImage() 349 350 # system_dlkm image. 351 if self._avd_spec.local_system_dlkm_image: 352 system_dlkm_image_path = goldfish_utils.FindSystemDlkmImage( 353 self._avd_spec.local_system_dlkm_image) 354 else: 355 # No known use case requires remote system_dlkm. 356 system_dlkm_image_path = None 357 358 # Boot image. 359 if self._avd_spec.local_kernel_image: 360 boot_image_path = create_common.FindBootImage( 361 self._avd_spec.local_kernel_image) 362 else: 363 boot_image_path = self._RetrieveBootImage() 364 365 # OTA tools. 366 ota_tools_dir = None 367 if system_image_path or system_dlkm_image_path or boot_image_path: 368 if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE: 369 ota_tools_dir = self._RetrieveOtaTools() 370 else: 371 ota_tools_dir = ota_tools.FindOtaToolsDir( 372 self._avd_spec.local_tool_dirs + 373 create_common.GetNonEmptyEnvVars( 374 constants.ENV_ANDROID_SOONG_HOST_OUT, 375 constants.ENV_ANDROID_HOST_OUT)) 376 377 return ArtifactPaths(image_zip_path, emu_zip_path, ota_tools_dir, 378 system_image_path, system_dlkm_image_path, 379 boot_image_path) 380 381 def _RetrieveDeviceImageZip(self): 382 """Retrieve device image zip from cache or Android Build API. 383 384 Returns: 385 The path to the device image zip in download_dir. 386 """ 387 build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) 388 build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) 389 image_zip_name_format = (_EXTRA_IMAGE_ZIP_NAME_FORMAT if 390 self._ShouldMixDiskImage() else 391 _SDK_REPO_IMAGE_ZIP_NAME_FORMAT) 392 return self._RetrieveArtifact( 393 build_target, build_id, 394 image_zip_name_format % {"build_id": build_id}) 395 396 def _RetrieveEmulatorBuildID(self): 397 """Retrieve required emulator build from a goldfish image build. 398 399 Returns: 400 A string, the emulator build ID. 401 None if the build info is empty. 402 """ 403 build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) 404 build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) 405 if build_id and build_target: 406 emu_info_path = self._RetrieveArtifact(build_target, build_id, 407 _EMULATOR_INFO_NAME) 408 with open(emu_info_path, "r", encoding="utf-8") as emu_info: 409 for line in emu_info: 410 match = _EMULATOR_VERSION_PATTERN.fullmatch(line.strip()) 411 if match: 412 logger.info("Found emulator build ID: %s", line) 413 return match.group("build_id") 414 return None 415 416 def _RetrieveEmulatorZip(self): 417 """Retrieve emulator zip from cache or Android Build API. 418 419 Returns: 420 The path to the emulator zip in download_dir. 421 None if this method cannot determine the emulator build ID. 422 """ 423 emu_build_id = (self._avd_spec.emulator_build_id or 424 self._RetrieveEmulatorBuildID()) 425 if not emu_build_id: 426 return None 427 emu_build_target = (self._avd_spec.emulator_build_target or 428 self._avd_spec.cfg.emulator_build_target) 429 emu_zip_name = self._InferEmulatorZipName(emu_build_target, 430 emu_build_id) 431 return self._RetrieveArtifact(emu_build_target, emu_build_id, 432 emu_zip_name) 433 434 def _RetrieveSystemImage(self): 435 """Retrieve and unzip system image if system build info is not empty. 436 437 Returns: 438 The path to the temporary system image. 439 None if the system build info is empty. 440 """ 441 build_id = self._avd_spec.system_build_info.get(constants.BUILD_ID) 442 build_target = self._avd_spec.system_build_info.get( 443 constants.BUILD_TARGET) 444 if not build_id or not build_target: 445 return None 446 image_zip_name = _IMAGE_ZIP_NAME_FORMAT % { 447 "build_target": build_target.split("-", 1)[0], 448 "build_id": build_id} 449 image_zip_path = self._RetrieveArtifact(build_target, build_id, 450 image_zip_name) 451 logger.debug("Unzip %s from %s to %s.", 452 _SYSTEM_IMAGE_NAME, image_zip_path, self._artifact_dir) 453 with zipfile.ZipFile(image_zip_path, "r") as zip_file: 454 zip_file.extract(_SYSTEM_IMAGE_NAME, self._artifact_dir) 455 return os.path.join(self._artifact_dir, _SYSTEM_IMAGE_NAME) 456 457 def _RetrieveBootImage(self): 458 """Retrieve boot image if boot build info is not empty. 459 460 Returns: 461 The path to the boot image in download_dir. 462 None if the boot build info is empty. 463 """ 464 build_id = self._avd_spec.boot_build_info.get(constants.BUILD_ID) 465 build_target = self._avd_spec.boot_build_info.get( 466 constants.BUILD_TARGET) 467 image_name = self._avd_spec.boot_build_info.get( 468 constants.BUILD_ARTIFACT) 469 if build_id and build_target and image_name: 470 return self._RetrieveArtifact(build_target, build_id, image_name) 471 return None 472 473 def _RetrieveOtaTools(self): 474 """Retrieve and unzip OTA tools. 475 476 This method retrieves OTA tools from the goldfish build which contains 477 mk_combined_img. 478 479 Returns: 480 The path to the temporary OTA tools directory. 481 """ 482 build_id = self._avd_spec.remote_image.get(constants.BUILD_ID) 483 build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET) 484 zip_path = self._RetrieveArtifact(build_target, build_id, 485 _OTA_TOOLS_ZIP_NAME) 486 ota_tools_dir = os.path.join(self._artifact_dir, _OTA_TOOLS_DIR_NAME) 487 logger.debug("Unzip %s to %s.", zip_path, ota_tools_dir) 488 os.mkdir(ota_tools_dir) 489 with zipfile.ZipFile(zip_path, "r") as zip_file: 490 zip_file.extractall(ota_tools_dir) 491 return ota_tools_dir 492 493 @staticmethod 494 def _GetSubdirNameInZip(zip_path): 495 """Get the name of the only subdirectory in a zip. 496 497 In an SDK repository zip, the images and the binaries are located in a 498 subdirectory. This class needs to find out the subdirectory name in 499 order to construct the remote commands. 500 501 For example, in a sdk-repo-linux-system-images-*.zip for arm64, all 502 files are in "arm64-v8a/". The zip entries are: 503 504 arm64-v8a/NOTICE.txt 505 arm64-v8a/system.img 506 arm64-v8a/data/local.prop 507 ... 508 509 This method scans the entries and returns the common subdirectory name. 510 """ 511 sep = "/" 512 with zipfile.ZipFile(zip_path, 'r') as zip_obj: 513 entries = zip_obj.namelist() 514 if len(entries) > 0 and sep in entries[0]: 515 subdir = entries[0].split(sep, 1)[0] 516 if all(e.startswith(subdir + sep) for e in entries): 517 return subdir 518 logger.warning("Expect one subdirectory in %s. Actual entries: %s", 519 zip_path, " ".join(entries)) 520 return "" 521 522 def _UploadArtifacts(self, artifact_paths): 523 """Process and upload all images and tools to the remote host. 524 525 Args: 526 artifact_paths: An object of ArtifactPaths. 527 528 Returns: 529 An object of RemotePaths. 530 """ 531 remote_emulator_dir, remote_image_dir = self._UploadDeviceImages( 532 artifact_paths.emulator_zip, artifact_paths.image_zip) 533 534 remote_kernel_path = None 535 remote_ramdisk_path = None 536 537 if (artifact_paths.boot_image or artifact_paths.system_image or 538 artifact_paths.system_dlkm_image): 539 with tempfile.TemporaryDirectory("host_gf") as temp_dir: 540 ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir) 541 542 image_dir = os.path.join(temp_dir, "images") 543 logger.debug("Unzip %s.", artifact_paths.image_zip) 544 with zipfile.ZipFile(artifact_paths.image_zip, 545 "r") as zip_file: 546 zip_file.extractall(image_dir) 547 image_dir = os.path.join( 548 image_dir, 549 self._GetSubdirNameInZip(artifact_paths.image_zip)) 550 551 if (artifact_paths.system_image or 552 artifact_paths.system_dlkm_image): 553 self._MixAndUploadDiskImage( 554 remote_image_dir, image_dir, 555 artifact_paths.system_image, 556 artifact_paths.system_dlkm_image, ota) 557 558 if artifact_paths.boot_image: 559 remote_kernel_path, remote_ramdisk_path = ( 560 self._MixAndUploadKernelImages( 561 image_dir, artifact_paths.boot_image, 562 artifact_paths.system_dlkm_image, ota)) 563 564 return RemotePaths(remote_image_dir, remote_emulator_dir, 565 remote_kernel_path, remote_ramdisk_path) 566 567 def _ShouldMixDiskImage(self): 568 """Determines whether a mixed disk image is required. 569 570 This method checks whether the user requires to replace an image that 571 is part of the disk image. Acloud supports replacing system, 572 system_dlkm, and kernel images. system and system_dlkm are installed 573 on the disk. 574 575 Returns: 576 Boolean, whether a mixed disk image is required. 577 """ 578 return (self._avd_spec.local_system_image or 579 self._avd_spec.local_system_dlkm_image or 580 (self._avd_spec.system_build_info.get(constants.BUILD_ID) and 581 self._avd_spec.system_build_info.get(constants.BUILD_TARGET))) 582 583 @utils.TimeExecute( 584 function_description="Processing and uploading tools and images") 585 def _UploadDeviceImages(self, emulator_zip_path, image_zip_path): 586 """Upload artifacts to remote host and extract them. 587 588 Args: 589 emulator_zip_path: The local path to the emulator zip. 590 image_zip_path: The local path to the image zip. 591 592 Returns: 593 The remote paths to the extracted emulator tools and images. 594 """ 595 remote_emulator_dir = self._GetInstancePath(_REMOTE_EMULATOR_DIR) 596 remote_image_dir = self._GetInstancePath(_REMOTE_IMAGE_DIR) 597 remote_emulator_zip_path = self._GetInstancePath( 598 _REMOTE_EMULATOR_ZIP_PATH) 599 remote_image_zip_path = self._GetInstancePath(_REMOTE_IMAGE_ZIP_PATH) 600 self._ssh.Run(f"mkdir -p {remote_emulator_dir} {remote_image_dir}") 601 self._ssh.ScpPushFile(emulator_zip_path, remote_emulator_zip_path) 602 self._ssh.ScpPushFile(image_zip_path, remote_image_zip_path) 603 604 self._ssh.Run(f"unzip -d {remote_emulator_dir} " 605 f"{remote_emulator_zip_path}") 606 self._ssh.Run(f"unzip -d {remote_image_dir} {remote_image_zip_path}") 607 remote_emulator_subdir = remote_path.join( 608 remote_emulator_dir, _SDK_REPO_EMULATOR_DIR_NAME) 609 remote_image_subdir = remote_path.join( 610 remote_image_dir, self._GetSubdirNameInZip(image_zip_path)) 611 # TODO(b/141898893): In Android build environment, emulator gets build 612 # information from $ANDROID_PRODUCT_OUT/system/build.prop. 613 # If image_dir is an extacted SDK repository, the file is at 614 # image_dir/build.prop. Acloud copies it to 615 # image_dir/system/build.prop. 616 src_path = remote_path.join(remote_image_subdir, "build.prop") 617 dst_path = remote_path.join(remote_image_subdir, "system", 618 "build.prop") 619 self._ssh.Run("'test -f %(dst)s || " 620 "{ mkdir -p %(dst_dir)s && cp %(src)s %(dst)s ; }'" % 621 {"src": src_path, 622 "dst": dst_path, 623 "dst_dir": remote_path.dirname(dst_path)}) 624 return remote_emulator_subdir, remote_image_subdir 625 626 def _MixAndUploadDiskImage(self, remote_image_dir, image_dir, 627 system_image_path, system_dlkm_image_path, ota): 628 """Mix emulator, system, and system_dlkm images and upload them. 629 630 Args: 631 remote_image_dir: The remote directory where the mixed disk image 632 is uploaded. 633 image_dir: The directory containing emulator images. 634 system_image_path: The path to the system image. 635 system_dlkm_image_path: The path to the system_dlkm image. 636 ota: An instance of ota_tools.OtaTools. 637 638 Returns: 639 The remote path to the mixed disk image. 640 """ 641 with tempfile.TemporaryDirectory("host_gf_disk") as temp_dir: 642 mixed_image = goldfish_utils.MixDiskImage( 643 temp_dir, image_dir, system_image_path, system_dlkm_image_path, 644 ota) 645 646 # TODO(b/142228085): Use -system instead of overwriting the file. 647 remote_disk_image_path = os.path.join( 648 remote_image_dir, goldfish_utils.SYSTEM_QEMU_IMAGE_NAME) 649 self._ssh.ScpPushFile(mixed_image, remote_disk_image_path) 650 651 # Adding the parameter to remote VerifiedBootParams.textproto unlocks 652 # the device so that the disabled vbmeta takes effect. An alternative 653 # is to append the parameter to the kernel command line by 654 # `emulator -qemu -append`, but that does not pass the compliance test. 655 remote_params_path = remote_path.join( 656 remote_image_dir, goldfish_utils.VERIFIED_BOOT_PARAMS_FILE_NAME) 657 # \\n is interpreted by shell and echo. \" is interpreted by shell. 658 param = r'\\nparam: \"androidboot.verifiedbootstate=orange\"' 659 self._ssh.Run(f"'test -f {remote_params_path} && " 660 f"echo -e {param} >> {remote_params_path}'") 661 662 return remote_disk_image_path 663 664 def _MixAndUploadKernelImages(self, image_dir, boot_image_path, 665 system_dlkm_image_path, ota): 666 """Mix emulator kernel images with a boot image and upload them. 667 668 Args: 669 image_dir: The directory containing emulator images. 670 boot_image_path: The path to the boot image. 671 system_dlkm_image_path: The path to the system_dlkm image. 672 Can be None. 673 ota: An instance of ota_tools.OtaTools. 674 675 Returns: 676 The remote paths to the kernel image and the ramdisk image. 677 """ 678 remote_kernel_path = self._GetInstancePath(_REMOTE_KERNEL_PATH) 679 remote_ramdisk_path = self._GetInstancePath(_REMOTE_RAMDISK_PATH) 680 with tempfile.TemporaryDirectory("host_gf_kernel") as temp_dir: 681 kernel_path, ramdisk_path = goldfish_utils.MixWithBootImage( 682 temp_dir, image_dir, boot_image_path, 683 (system_dlkm_image_path if 684 self._avd_spec.mix_system_dlkm_into_vendor_ramdisk else None), 685 ota) 686 687 self._ssh.ScpPushFile(kernel_path, remote_kernel_path) 688 self._ssh.ScpPushFile(ramdisk_path, remote_ramdisk_path) 689 690 return remote_kernel_path, remote_ramdisk_path 691 692 def _GetEmulatorLogs(self): 693 """Return the logs created by the remote emulator command.""" 694 return [report.LogFile(self._GetInstancePath(_REMOTE_STDOUT_PATH), 695 constants.LOG_TYPE_KERNEL_LOG), 696 report.LogFile(self._GetInstancePath(_REMOTE_STDERR_PATH), 697 constants.LOG_TYPE_TEXT), 698 report.LogFile(self._GetInstancePath(_REMOTE_LOGCAT_PATH), 699 constants.LOG_TYPE_LOGCAT)] 700 701 @utils.TimeExecute(function_description="Start emulator") 702 def _StartEmulator(self, remote_paths): 703 """Start emulator command as a remote background process. 704 705 Args: 706 remote_emulator_dir: The emulator tool directory on remote host. 707 remote_image_dir: The image directory on remote host. 708 """ 709 remote_emulator_bin_path = remote_path.join( 710 remote_paths.emulator_dir, _EMULATOR_BIN_NAME) 711 remote_bin_paths = [ 712 remote_path.join(remote_paths.emulator_dir, name) for 713 name in _EMULATOR_BIN_DIR_NAMES] 714 remote_bin_paths.append(remote_emulator_bin_path) 715 self._ssh.Run("chmod -R +x %s" % " ".join(remote_bin_paths)) 716 717 remote_runtime_dir = self._GetInstancePath(_REMOTE_RUNTIME_DIR) 718 self._ssh.Run(f"mkdir -p {remote_runtime_dir}") 719 env = {constants.ENV_ANDROID_PRODUCT_OUT: remote_paths.image_dir, 720 constants.ENV_ANDROID_TMP: remote_runtime_dir, 721 constants.ENV_ANDROID_BUILD_TOP: remote_runtime_dir} 722 cmd = ["nohup", remote_emulator_bin_path, "-verbose", "-show-kernel", 723 "-read-only", "-ports", 724 str(self._GetConsolePort()) + "," + str(self.GetAdbPorts()[0]), 725 "-no-window", 726 "-logcat-output", self._GetInstancePath(_REMOTE_LOGCAT_PATH)] 727 728 if remote_paths.kernel: 729 cmd.extend(("-kernel", remote_paths.kernel)) 730 731 if remote_paths.ramdisk: 732 cmd.extend(("-ramdisk", remote_paths.ramdisk)) 733 734 cmd.extend(goldfish_utils.ConvertAvdSpecToArgs(self._avd_spec)) 735 736 # Emulator does not support -stdouterr-file on macOS. 737 self._ssh.Run( 738 "'export {env} ; {cmd} 1> {stdout} 2> {stderr} &'".format( 739 env=" ".join(k + "=~/" + v for k, v in env.items()), 740 cmd=" ".join(cmd), 741 stdout=self._GetInstancePath(_REMOTE_STDOUT_PATH), 742 stderr=self._GetInstancePath(_REMOTE_STDERR_PATH))) 743 744 @utils.TimeExecute(function_description="Wait for emulator") 745 def _WaitForEmulator(self): 746 """Wait for remote emulator console to be active. 747 748 Raises: 749 errors.DeviceBootError if connection fails. 750 errors.DeviceBootTimeoutError if boot times out. 751 """ 752 ip_addr = self._avd_spec.remote_host 753 console_port = self._GetConsolePort() 754 poll_timeout_secs = (self._avd_spec.boot_timeout_secs or 755 _DEFAULT_BOOT_TIMEOUT_SECS) 756 try: 757 with emulator_console.RemoteEmulatorConsole( 758 ip_addr, 759 console_port, 760 self._ssh_user, 761 self._ssh_private_key_path, 762 self._ssh_extra_args) as console: 763 utils.PollAndWait( 764 func=lambda: (True if console.Ping() else 765 console.Reconnect()), 766 expected_return=True, 767 timeout_exception=errors.DeviceBootTimeoutError, 768 timeout_secs=poll_timeout_secs, 769 sleep_interval_secs=5) 770 except errors.DeviceConnectionError as e: 771 raise errors.DeviceBootError("Fail to connect to %s:%d." % 772 (ip_addr, console_port)) from e 773 774 def GetBuildInfoDict(self): 775 """Get build info dictionary. 776 777 Returns: 778 A build info dictionary. 779 """ 780 build_info_dict = {key: val for key, val in 781 self._avd_spec.remote_image.items() if val} 782 return build_info_dict 783 784 def GetAdbPorts(self): 785 """Get ADB ports of the created devices. 786 787 This class does not support --num-avds-per-instance. 788 789 Returns: 790 The port numbers as a list of integers. 791 """ 792 return [self._GetConsolePort() + 1] 793 794 def GetFailures(self): 795 """Get Failures from all devices. 796 797 Returns: 798 A dictionary the contains all the failures. 799 The key is the name of the instance that fails to boot, 800 and the value is an errors.DeviceBootError object. 801 """ 802 return self._failures 803 804 def GetLogs(self): 805 """Get log files of created instances. 806 807 Returns: 808 A dictionary that maps instance names to lists of report.LogFile. 809 """ 810 return self._logs 811