1# Copyright 2022 - 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"""Utility functions that process cuttlefish images.""" 16 17import collections 18import fnmatch 19import glob 20import json 21import logging 22import os 23import posixpath as remote_path 24import random 25import re 26import shlex 27import subprocess 28import tempfile 29import time 30import zipfile 31 32from acloud import errors 33from acloud.create import create_common 34from acloud.internal import constants 35from acloud.internal.lib import ota_tools 36from acloud.internal.lib import ssh 37from acloud.internal.lib import utils 38from acloud.public import report 39 40 41logger = logging.getLogger(__name__) 42 43# Local build artifacts to be uploaded. 44_ARTIFACT_FILES = ["*.img", "bootloader", "kernel"] 45_SYSTEM_DLKM_IMAGE_NAMES = ( 46 "system_dlkm.flatten.ext4.img", # GKI artifact 47 "system_dlkm.img", # cuttlefish artifact 48) 49_VENDOR_BOOT_IMAGE_NAME = "vendor_boot.img" 50_KERNEL_IMAGE_NAMES = ("kernel", "bzImage", "Image") 51_INITRAMFS_IMAGE_NAME = "initramfs.img" 52_SUPER_IMAGE_NAME = "super.img" 53_VENDOR_IMAGE_NAMES = ("vendor.img", "vendor_dlkm.img", "odm.img", 54 "odm_dlkm.img") 55VendorImagePaths = collections.namedtuple( 56 "VendorImagePaths", 57 ["vendor", "vendor_dlkm", "odm", "odm_dlkm"]) 58 59# The relative path to the base directory containing cuttelfish runtime files. 60# On a GCE instance, the directory is the SSH user's HOME. 61GCE_BASE_DIR = "." 62_REMOTE_HOST_BASE_DIR_FORMAT = "acloud_cf_%(num)d" 63# By default, fetch_cvd or UploadArtifacts creates remote cuttlefish images and 64# tools in the base directory. The user can set the image directory path by 65# --remote-image-dir. 66# The user may specify extra images such as --local-system-image and 67# --local-kernel-image. UploadExtraImages uploads them to "acloud_image" 68# subdirectory in the image directory. The following are the relative paths 69# under the image directory. 70_REMOTE_EXTRA_IMAGE_DIR = "acloud_image" 71_REMOTE_BOOT_IMAGE_PATH = remote_path.join(_REMOTE_EXTRA_IMAGE_DIR, "boot.img") 72_REMOTE_VENDOR_BOOT_IMAGE_PATH = remote_path.join( 73 _REMOTE_EXTRA_IMAGE_DIR, _VENDOR_BOOT_IMAGE_NAME) 74_REMOTE_VBMETA_IMAGE_PATH = remote_path.join( 75 _REMOTE_EXTRA_IMAGE_DIR, "vbmeta.img") 76_REMOTE_KERNEL_IMAGE_PATH = remote_path.join( 77 _REMOTE_EXTRA_IMAGE_DIR, _KERNEL_IMAGE_NAMES[0]) 78_REMOTE_INITRAMFS_IMAGE_PATH = remote_path.join( 79 _REMOTE_EXTRA_IMAGE_DIR, _INITRAMFS_IMAGE_NAME) 80_REMOTE_SUPER_IMAGE_PATH = remote_path.join( 81 _REMOTE_EXTRA_IMAGE_DIR, _SUPER_IMAGE_NAME) 82# The symbolic link to --remote-image-dir. It's in the base directory. 83_IMAGE_DIR_LINK_NAME = "image_dir_link" 84# The text file contains the number of references to --remote-image-dir. 85# Th path is --remote-image-dir + EXT. 86_REF_CNT_FILE_EXT = ".lock" 87 88# Remote host instance name 89_REMOTE_HOST_INSTANCE_NAME_FORMAT = ( 90 constants.INSTANCE_TYPE_HOST + 91 "-%(ip_addr)s-%(num)d-%(build_id)s-%(build_target)s") 92_REMOTE_HOST_INSTANCE_NAME_PATTERN = re.compile( 93 constants.INSTANCE_TYPE_HOST + r"-(?P<ip_addr>[\d.]+)-(?P<num>\d+)-.+") 94# android-info.txt contents. 95_CONFIG_PATTERN = re.compile(r"^config=(?P<config>.+)$", re.MULTILINE) 96# launch_cvd arguments. 97_DATA_POLICY_CREATE_IF_MISSING = "create_if_missing" 98_DATA_POLICY_ALWAYS_CREATE = "always_create" 99_NUM_AVDS_ARG = "-num_instances=%(num_AVD)s" 100AGREEMENT_PROMPT_ARG = "-report_anonymous_usage_stats=y" 101UNDEFOK_ARG = "-undefok=report_anonymous_usage_stats,config" 102# Connect the OpenWrt device via console file. 103_ENABLE_CONSOLE_ARG = "-console=true" 104# WebRTC args 105_WEBRTC_ID = "--webrtc_device_id=%(instance)s" 106_WEBRTC_ARGS = ["--start_webrtc", "--vm_manager=crosvm"] 107_VNC_ARGS = ["--start_vnc_server=true"] 108 109# Cuttlefish runtime directory is specified by `-instance_dir <runtime_dir>`. 110# Cuttlefish tools may create a symbolic link at the specified path. 111# The actual location of the runtime directory depends on the version: 112# 113# In Android 10, the directory is `<runtime_dir>`. 114# 115# In Android 11 and 12, the directory is `<runtime_dir>.<num>`. 116# `<runtime_dir>` is a symbolic link to the first device's directory. 117# 118# In the latest version, if `--instance-dir <runtime_dir>` is specified, the 119# directory is `<runtime_dir>/instances/cvd-<num>`. 120# `<runtime_dir>_runtime` and `<runtime_dir>.<num>` are symbolic links. 121# 122# If `--instance-dir <runtime_dir>` is not specified, the directory is 123# `~/cuttlefish/instances/cvd-<num>`. 124# `~/cuttlefish_runtime` and `~/cuttelfish_runtime.<num>` are symbolic links. 125_LOCAL_LOG_DIR_FORMAT = os.path.join( 126 "%(runtime_dir)s", "instances", "cvd-%(num)d", "logs") 127# Relative paths in a base directory. 128_REMOTE_RUNTIME_DIR_FORMAT = remote_path.join( 129 "cuttlefish", "instances", "cvd-%(num)d") 130_REMOTE_LEGACY_RUNTIME_DIR_FORMAT = "cuttlefish_runtime.%(num)d" 131HOST_KERNEL_LOG = report.LogFile( 132 "/var/log/kern.log", constants.LOG_TYPE_KERNEL_LOG, "host_kernel.log") 133 134# Contents of the target_files archive. 135_DOWNLOAD_MIX_IMAGE_NAME = "{build_target}-target_files-{build_id}.zip" 136_TARGET_FILES_META_DIR_NAME = "META" 137_TARGET_FILES_IMAGES_DIR_NAME = "IMAGES" 138_MISC_INFO_FILE_NAME = "misc_info.txt" 139# glob patterns of target_files entries used by acloud. 140_TARGET_FILES_ENTRIES = [ 141 "IMAGES/" + pattern for pattern in _ARTIFACT_FILES 142] + ["META/misc_info.txt"] 143 144# Represents a 64-bit ARM architecture. 145_ARM_MACHINE_TYPE = "aarch64" 146 147 148def GetAdbPorts(base_instance_num, num_avds_per_instance): 149 """Get ADB ports of cuttlefish. 150 151 Args: 152 base_instance_num: An integer or None, the instance number of the first 153 device. 154 num_avds_per_instance: An integer or None, the number of devices. 155 156 Returns: 157 The port numbers as a list of integers. 158 """ 159 return [constants.CF_ADB_PORT + (base_instance_num or 1) - 1 + index 160 for index in range(num_avds_per_instance or 1)] 161 162 163def GetVncPorts(base_instance_num, num_avds_per_instance): 164 """Get VNC ports of cuttlefish. 165 166 Args: 167 base_instance_num: An integer or None, the instance number of the first 168 device. 169 num_avds_per_instance: An integer or None, the number of devices. 170 171 Returns: 172 The port numbers as a list of integers. 173 """ 174 return [constants.CF_VNC_PORT + (base_instance_num or 1) - 1 + index 175 for index in range(num_avds_per_instance or 1)] 176 177 178@utils.TimeExecute(function_description="Extracting target_files zip.") 179def ExtractTargetFilesZip(zip_path, output_dir): 180 """Extract images and misc_info.txt from a target_files zip.""" 181 with zipfile.ZipFile(zip_path, "r") as zip_file: 182 for entry in zip_file.namelist(): 183 if any(fnmatch.fnmatch(entry, pattern) for pattern in 184 _TARGET_FILES_ENTRIES): 185 zip_file.extract(entry, output_dir) 186 187 188def _UploadImageZip(ssh_obj, remote_image_dir, image_zip): 189 """Upload an image zip to a remote host and a GCE instance. 190 191 Args: 192 ssh_obj: An Ssh object. 193 remote_image_dir: The remote image directory. 194 image_zip: The path to the image zip. 195 """ 196 remote_cmd = f"/usr/bin/install_zip.sh {remote_image_dir} < {image_zip}" 197 logger.debug("remote_cmd:\n %s", remote_cmd) 198 ssh_obj.Run(remote_cmd) 199 200 201def _UploadImageDir(ssh_obj, remote_image_dir, image_dir): 202 """Upload an image directory to a remote host or a GCE instance. 203 204 The images are compressed for faster upload. 205 206 Args: 207 ssh_obj: An Ssh object. 208 remote_image_dir: The remote image directory. 209 image_dir: The directory containing the files to be uploaded. 210 """ 211 try: 212 images_path = os.path.join(image_dir, "required_images") 213 with open(images_path, "r", encoding="utf-8") as images: 214 artifact_files = images.read().splitlines() 215 except IOError: 216 # Older builds may not have a required_images file. In this case 217 # we fall back to *.img. 218 artifact_files = [] 219 for file_name in _ARTIFACT_FILES: 220 artifact_files.extend( 221 os.path.basename(image) for image in glob.glob( 222 os.path.join(image_dir, file_name))) 223 # Upload android-info.txt to parse config value. 224 artifact_files.append(constants.ANDROID_INFO_FILE) 225 cmd = (f"tar -cf - --lzop -S -C {image_dir} {' '.join(artifact_files)} | " 226 f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- " 227 f"tar -xf - --lzop -S -C {remote_image_dir}") 228 logger.debug("cmd:\n %s", cmd) 229 ssh.ShellCmdWithRetry(cmd) 230 231 232def _UploadCvdHostPackage(ssh_obj, remote_image_dir, cvd_host_package): 233 """Upload a CVD host package to a remote host or a GCE instance. 234 235 Args: 236 ssh_obj: An Ssh object. 237 remote_image_dir: The remote base directory. 238 cvd_host_package: The path to the CVD host package. 239 """ 240 if os.path.isdir(cvd_host_package): 241 cmd = (f"tar -cf - --lzop -S -C {cvd_host_package} . | " 242 f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- " 243 f"tar -xf - --lzop -S -C {remote_image_dir}") 244 logger.debug("cmd:\n %s", cmd) 245 ssh.ShellCmdWithRetry(cmd) 246 else: 247 remote_cmd = f"tar -xzf - -C {remote_image_dir} < {cvd_host_package}" 248 logger.debug("remote_cmd:\n %s", remote_cmd) 249 ssh_obj.Run(remote_cmd) 250 251 252@utils.TimeExecute(function_description="Processing and uploading local images") 253def UploadArtifacts(ssh_obj, remote_image_dir, image_path, cvd_host_package): 254 """Upload images and a CVD host package to a remote host or a GCE instance. 255 256 Args: 257 ssh_obj: An Ssh object. 258 remote_image_dir: The remote image directory. 259 image_path: A string, the path to the image zip built by `m dist`, 260 the directory containing the images built by `m`, or 261 the directory containing extracted target files. 262 cvd_host_package: A string, the path to the CVD host package in gzip. 263 """ 264 if os.path.isdir(image_path): 265 _UploadImageDir(ssh_obj, remote_image_dir, FindImageDir(image_path)) 266 else: 267 _UploadImageZip(ssh_obj, remote_image_dir, image_path) 268 _UploadCvdHostPackage(ssh_obj, remote_image_dir, cvd_host_package) 269 270 271def FindBootImages(search_path): 272 """Find boot and vendor_boot images in a path. 273 274 Args: 275 search_path: A path to an image file or an image directory. 276 277 Returns: 278 The boot image path and the vendor_boot image path. Each value can be 279 None if the path doesn't exist. 280 281 Raises: 282 errors.GetLocalImageError if search_path contains more than one boot 283 image or the file format is not correct. 284 """ 285 boot_image_path = create_common.FindBootImage(search_path, 286 raise_error=False) 287 vendor_boot_image_path = os.path.join(search_path, _VENDOR_BOOT_IMAGE_NAME) 288 if not os.path.isfile(vendor_boot_image_path): 289 vendor_boot_image_path = None 290 291 return boot_image_path, vendor_boot_image_path 292 293 294def FindKernelImages(search_path): 295 """Find kernel and initramfs images in a path. 296 297 Args: 298 search_path: A path to an image directory. 299 300 Returns: 301 The kernel image path and the initramfs image path. Each value can be 302 None if the path doesn't exist. 303 """ 304 paths = [os.path.join(search_path, name) for name in _KERNEL_IMAGE_NAMES] 305 kernel_image_path = next((path for path in paths if os.path.isfile(path)), 306 None) 307 308 initramfs_image_path = os.path.join(search_path, _INITRAMFS_IMAGE_NAME) 309 if not os.path.isfile(initramfs_image_path): 310 initramfs_image_path = None 311 312 return kernel_image_path, initramfs_image_path 313 314 315@utils.TimeExecute(function_description="Uploading local kernel images.") 316def _UploadKernelImages(ssh_obj, remote_image_dir, kernel_search_path, 317 vendor_boot_search_path): 318 """Find and upload kernel or boot images to a remote host or a GCE 319 instance. 320 321 Args: 322 ssh_obj: An Ssh object. 323 remote_image_dir: The remote image directory. 324 kernel_search_path: A path to an image file or an image directory. 325 vendor_boot_search_path: A path to a vendor boot image file or an image 326 directory. 327 328 Returns: 329 A list of string pairs. Each pair consists of a launch_cvd option and a 330 remote path. 331 332 Raises: 333 errors.GetLocalImageError if search_path does not contain kernel 334 images. 335 """ 336 # Assume that the caller cleaned up the remote home directory. 337 ssh_obj.Run("mkdir -p " + 338 remote_path.join(remote_image_dir, _REMOTE_EXTRA_IMAGE_DIR)) 339 340 # Find images 341 kernel_image_path = None 342 initramfs_image_path = None 343 boot_image_path = None 344 vendor_boot_image_path = None 345 346 if kernel_search_path: 347 kernel_image_path, initramfs_image_path = FindKernelImages( 348 kernel_search_path) 349 if not (kernel_image_path and initramfs_image_path): 350 boot_image_path, vendor_boot_image_path = FindBootImages( 351 kernel_search_path) 352 353 if vendor_boot_search_path: 354 vendor_boot_image_path = create_common.FindVendorBootImage( 355 vendor_boot_search_path) 356 357 # Upload 358 launch_cvd_args = [] 359 360 if kernel_image_path and initramfs_image_path: 361 remote_kernel_image_path = remote_path.join( 362 remote_image_dir, _REMOTE_KERNEL_IMAGE_PATH) 363 remote_initramfs_image_path = remote_path.join( 364 remote_image_dir, _REMOTE_INITRAMFS_IMAGE_PATH) 365 ssh_obj.ScpPushFile(kernel_image_path, remote_kernel_image_path) 366 ssh_obj.ScpPushFile(initramfs_image_path, remote_initramfs_image_path) 367 launch_cvd_args.append(("-kernel_path", remote_kernel_image_path)) 368 launch_cvd_args.append(("-initramfs_path", remote_initramfs_image_path)) 369 370 if boot_image_path: 371 remote_boot_image_path = remote_path.join( 372 remote_image_dir, _REMOTE_BOOT_IMAGE_PATH) 373 ssh_obj.ScpPushFile(boot_image_path, remote_boot_image_path) 374 launch_cvd_args.append(("-boot_image", remote_boot_image_path)) 375 376 if vendor_boot_image_path: 377 remote_vendor_boot_image_path = remote_path.join( 378 remote_image_dir, _REMOTE_VENDOR_BOOT_IMAGE_PATH) 379 ssh_obj.ScpPushFile(vendor_boot_image_path, 380 remote_vendor_boot_image_path) 381 launch_cvd_args.append( 382 ("-vendor_boot_image", remote_vendor_boot_image_path)) 383 384 if not launch_cvd_args: 385 raise errors.GetLocalImageError( 386 f"{kernel_search_path}, {vendor_boot_search_path} is not a boot " 387 "image or a directory containing images.") 388 389 return launch_cvd_args 390 391 392def _FindSystemDlkmImage(search_path): 393 """Find system_dlkm image in a path. 394 395 Args: 396 search_path: A path to an image file or an image directory. 397 398 Returns: 399 The system_dlkm image path. 400 401 Raises: 402 errors.GetLocalImageError if search_path does not contain a 403 system_dlkm image. 404 """ 405 if os.path.isfile(search_path): 406 return search_path 407 408 for name in _SYSTEM_DLKM_IMAGE_NAMES: 409 path = os.path.join(search_path, name) 410 if os.path.isfile(path): 411 return path 412 413 raise errors.GetLocalImageError( 414 f"{search_path} is not a system_dlkm image or a directory containing " 415 "images.") 416 417 418def _MixSuperImage(super_image_path, avd_spec, target_files_dir, ota): 419 """Mix super image from device images and extra images. 420 421 Args: 422 super_image_path: The path to the output mixed super image. 423 avd_spec: An AvdSpec object. 424 target_files_dir: The path to the extracted target_files zip containing 425 device images and misc_info.txt. 426 ota: An OtaTools object. 427 """ 428 misc_info_path = FindMiscInfo(target_files_dir) 429 image_dir = FindImageDir(target_files_dir) 430 431 system_image_path = None 432 system_ext_image_path = None 433 product_image_path = None 434 system_dlkm_image_path = None 435 vendor_image_path = None 436 vendor_dlkm_image_path = None 437 odm_image_path = None 438 odm_dlkm_image_path = None 439 440 if avd_spec.local_system_image: 441 ( 442 system_image_path, 443 system_ext_image_path, 444 product_image_path, 445 ) = create_common.FindSystemImages(avd_spec.local_system_image) 446 447 if avd_spec.local_system_dlkm_image: 448 system_dlkm_image_path = _FindSystemDlkmImage( 449 avd_spec.local_system_dlkm_image) 450 451 if avd_spec.local_vendor_image: 452 ( 453 vendor_image_path, 454 vendor_dlkm_image_path, 455 odm_image_path, 456 odm_dlkm_image_path, 457 ) = FindVendorImages(avd_spec.local_vendor_image) 458 459 ota.MixSuperImage(super_image_path, misc_info_path, image_dir, 460 system_image=system_image_path, 461 system_ext_image=system_ext_image_path, 462 product_image=product_image_path, 463 system_dlkm_image=system_dlkm_image_path, 464 vendor_image=vendor_image_path, 465 vendor_dlkm_image=vendor_dlkm_image_path, 466 odm_image=odm_image_path, 467 odm_dlkm_image=odm_dlkm_image_path) 468 469 470@utils.TimeExecute(function_description="Uploading disabled vbmeta image.") 471def _UploadVbmetaImage(ssh_obj, remote_image_dir, vbmeta_image_path): 472 """Upload disabled vbmeta image to a remote host or a GCE instance. 473 474 Args: 475 ssh_obj: An Ssh object. 476 remote_image_dir: The remote image directory. 477 vbmeta_image_path: The path to the vbmeta image. 478 479 Returns: 480 A pair of strings, the launch_cvd option and the remote path. 481 """ 482 remote_vbmeta_image_path = remote_path.join(remote_image_dir, 483 _REMOTE_VBMETA_IMAGE_PATH) 484 ssh_obj.ScpPushFile(vbmeta_image_path, remote_vbmeta_image_path) 485 return "-vbmeta_image", remote_vbmeta_image_path 486 487 488def AreTargetFilesRequired(avd_spec): 489 """Return whether UploadExtraImages requires target_files_dir.""" 490 return bool(avd_spec.local_system_image or avd_spec.local_vendor_image or 491 avd_spec.local_system_dlkm_image) 492 493 494def UploadExtraImages(ssh_obj, remote_image_dir, avd_spec, target_files_dir): 495 """Find and upload the images specified in avd_spec. 496 497 This function finds the kernel, system, and vendor images specified in 498 avd_spec. It processes them and uploads kernel, super, and vbmeta images. 499 500 Args: 501 ssh_obj: An Ssh object. 502 remote_image_dir: The remote image directory. 503 avd_spec: An AvdSpec object containing extra image paths. 504 target_files_dir: The path to an extracted target_files zip if the 505 avd_spec requires building a super image. 506 507 Returns: 508 A list of string pairs. Each pair consists of a launch_cvd option and a 509 remote path. 510 511 Raises: 512 errors.GetLocalImageError if any specified image path does not exist. 513 errors.CheckPathError if avd_spec.local_tool_dirs do not contain OTA 514 tools, or target_files_dir does not contain misc_info.txt. 515 ValueError if target_files_dir is required but not specified. 516 """ 517 extra_img_args = [] 518 if avd_spec.local_kernel_image or avd_spec.local_vendor_boot_image: 519 extra_img_args += _UploadKernelImages(ssh_obj, remote_image_dir, 520 avd_spec.local_kernel_image, 521 avd_spec.local_vendor_boot_image) 522 523 524 if AreTargetFilesRequired(avd_spec): 525 if not target_files_dir: 526 raise ValueError("target_files_dir is required when avd_spec has " 527 "local system image, local system_dlkm image, or " 528 "local vendor image.") 529 ota = ota_tools.FindOtaTools( 530 avd_spec.local_tool_dirs + create_common.GetNonEmptyEnvVars( 531 constants.ENV_ANDROID_SOONG_HOST_OUT, 532 constants.ENV_ANDROID_HOST_OUT)) 533 ssh_obj.Run( 534 "mkdir -p " + 535 remote_path.join(remote_image_dir, _REMOTE_EXTRA_IMAGE_DIR)) 536 with tempfile.TemporaryDirectory() as super_image_dir: 537 _MixSuperImage(os.path.join(super_image_dir, _SUPER_IMAGE_NAME), 538 avd_spec, target_files_dir, ota) 539 extra_img_args.append(_UploadSuperImage(ssh_obj, remote_image_dir, 540 super_image_dir)) 541 542 vbmeta_image_path = os.path.join(super_image_dir, "vbmeta.img") 543 ota.MakeDisabledVbmetaImage(vbmeta_image_path) 544 extra_img_args.append(_UploadVbmetaImage(ssh_obj, remote_image_dir, 545 vbmeta_image_path)) 546 547 return extra_img_args 548 549 550@utils.TimeExecute(function_description="Uploading super image.") 551def _UploadSuperImage(ssh_obj, remote_image_dir, super_image_dir): 552 """Upload a super image to a remote host or a GCE instance. 553 554 Args: 555 ssh_obj: An Ssh object. 556 remote_image_dir: The remote image directory. 557 super_image_dir: The path to the directory containing the super image. 558 559 Returns: 560 A pair of strings, the launch_cvd option and the remote path. 561 """ 562 remote_super_image_path = remote_path.join(remote_image_dir, 563 _REMOTE_SUPER_IMAGE_PATH) 564 remote_super_image_dir = remote_path.dirname(remote_super_image_path) 565 cmd = (f"tar -cf - --lzop -S -C {super_image_dir} {_SUPER_IMAGE_NAME} | " 566 f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- " 567 f"tar -xf - --lzop -S -C {remote_super_image_dir}") 568 ssh.ShellCmdWithRetry(cmd) 569 return "-super_image", remote_super_image_path 570 571 572def CleanUpRemoteCvd(ssh_obj, remote_dir, raise_error): 573 """Call stop_cvd and delete the files on a remote host. 574 575 Args: 576 ssh_obj: An Ssh object. 577 remote_dir: The remote base directory. 578 raise_error: Whether to raise an error if the remote instance is not 579 running. 580 581 Raises: 582 subprocess.CalledProcessError if any command fails. 583 """ 584 # FIXME: Use the images and launch_cvd in --remote-image-dir when 585 # cuttlefish can reliably share images. 586 _DeleteRemoteImageDirLink(ssh_obj, remote_dir) 587 home = remote_path.join("$HOME", remote_dir) 588 stop_cvd_path = remote_path.join(remote_dir, "bin", "stop_cvd") 589 stop_cvd_cmd = f"'HOME={home} {stop_cvd_path}'" 590 if raise_error: 591 ssh_obj.Run(stop_cvd_cmd) 592 else: 593 try: 594 ssh_obj.Run(stop_cvd_cmd, retry=0) 595 except Exception as e: 596 logger.debug( 597 "Failed to stop_cvd (possibly no running device): %s", e) 598 599 # This command deletes all files except hidden files under remote_dir. 600 # It does not raise an error if no files can be deleted. 601 ssh_obj.Run(f"'rm -rf {remote_path.join(remote_dir, '*')}'") 602 603 604def GetRemoteHostBaseDir(base_instance_num): 605 """Get remote base directory by instance number. 606 607 Args: 608 base_instance_num: Integer or None, the instance number of the device. 609 610 Returns: 611 The remote base directory. 612 """ 613 return _REMOTE_HOST_BASE_DIR_FORMAT % {"num": base_instance_num or 1} 614 615 616def FormatRemoteHostInstanceName(ip_addr, base_instance_num, build_id, 617 build_target): 618 """Convert an IP address and build info to an instance name. 619 620 Args: 621 ip_addr: String, the IP address of the remote host. 622 base_instance_num: Integer or None, the instance number of the device. 623 build_id: String, the build id. 624 build_target: String, the build target, e.g., aosp_cf_x86_64_phone. 625 626 Return: 627 String, the instance name. 628 """ 629 return _REMOTE_HOST_INSTANCE_NAME_FORMAT % { 630 "ip_addr": ip_addr, 631 "num": base_instance_num or 1, 632 "build_id": build_id, 633 "build_target": build_target} 634 635 636def ParseRemoteHostAddress(instance_name): 637 """Parse IP address from a remote host instance name. 638 639 Args: 640 instance_name: String, the instance name. 641 642 Returns: 643 The IP address and the base directory as strings. 644 None if the name does not represent a remote host instance. 645 """ 646 match = _REMOTE_HOST_INSTANCE_NAME_PATTERN.fullmatch(instance_name) 647 if match: 648 return (match.group("ip_addr"), 649 GetRemoteHostBaseDir(int(match.group("num")))) 650 return None 651 652 653def PrepareRemoteImageDirLink(ssh_obj, remote_dir, remote_image_dir): 654 """Create a link to a directory containing images and tools. 655 656 Args: 657 ssh_obj: An Ssh object. 658 remote_dir: The directory in which the link is created. 659 remote_image_dir: The directory that is linked to. 660 """ 661 remote_link = remote_path.join(remote_dir, _IMAGE_DIR_LINK_NAME) 662 663 # If remote_image_dir is relative to HOME, compute the relative path based 664 # on remote_dir. 665 ln_cmd = ("ln -s " + 666 ("" if remote_path.isabs(remote_image_dir) else "-r ") + 667 f"{remote_image_dir} {remote_link}") 668 669 remote_ref_cnt = remote_path.normpath(remote_image_dir) + _REF_CNT_FILE_EXT 670 ref_cnt_cmd = (f"expr $(test -s {remote_ref_cnt} && " 671 f"cat {remote_ref_cnt} || echo 0) + 1 > {remote_ref_cnt}") 672 673 # `flock` creates the file automatically. 674 # This command should create its parent directory before `flock`. 675 ssh_obj.Run(shlex.quote( 676 f"mkdir -p {remote_image_dir} && flock {remote_ref_cnt} -c " + 677 shlex.quote( 678 f"mkdir -p {remote_dir} {remote_image_dir} && " 679 f"{ln_cmd} && {ref_cnt_cmd}"))) 680 681 682def _DeleteRemoteImageDirLink(ssh_obj, remote_dir): 683 """Delete the directories containing images and tools. 684 685 Args: 686 ssh_obj: An Ssh object. 687 remote_dir: The directory containing the link to the image directory. 688 """ 689 remote_link = remote_path.join(remote_dir, _IMAGE_DIR_LINK_NAME) 690 # This command returns an absolute path if the link exists; otherwise 691 # an empty string. It raises an exception only if connection error. 692 remote_image_dir = ssh_obj.Run( 693 shlex.quote(f"readlink -n -e {remote_link} || true")) 694 if not remote_image_dir: 695 return 696 697 remote_ref_cnt = (remote_path.normpath(remote_image_dir) + 698 _REF_CNT_FILE_EXT) 699 # `expr` returns 1 if the result is 0. 700 ref_cnt_cmd = (f"expr $(test -s {remote_ref_cnt} && " 701 f"cat {remote_ref_cnt} || echo 1) - 1 > " 702 f"{remote_ref_cnt}") 703 704 # `flock` creates the file automatically. 705 # This command should create its parent directory before `flock`. 706 ssh_obj.Run(shlex.quote( 707 f"mkdir -p {remote_image_dir} && flock {remote_ref_cnt} -c " + 708 shlex.quote( 709 f"rm -f {remote_link} && " 710 f"{ref_cnt_cmd} || " 711 f"rm -rf {remote_image_dir} {remote_ref_cnt}"))) 712 713 714def LoadRemoteImageArgs(ssh_obj, remote_timestamp_path, remote_args_path, 715 deadline): 716 """Load launch_cvd arguments from a remote path. 717 718 Acloud processes using the same --remote-image-dir synchronizes on 719 remote_timestamp_path and remote_args_path in the directory. This function 720 implements the synchronization in 3 steps: 721 722 1. This function checks whether remote_timestamp_path is empty. If it is, 723 this acloud process becomes the uploader. This function writes the upload 724 deadline to the file and returns None. The caller should upload files to 725 the --remote-image-dir and then call SaveRemoteImageArgs. The upload 726 deadline written to the file represents when this acloud process should 727 complete uploading. 728 729 2. If remote_timestamp_path is not empty, this function reads the upload 730 deadline from it. It then waits until remote_args_path contains the 731 arguments in a valid format, or the upload deadline passes. 732 733 3. If this function loads arguments from remote_args_path successfully, 734 it returns the arguments. Otherwise, the uploader misses the deadline. The 735 --remote-image-dir is not usable. This function raises an error. It does 736 not attempt to reset the --remote-image-dir. 737 738 Args: 739 ssh_obj: An Ssh object. 740 remote_timestamp_path: The remote path containing the time when the 741 uploader will complete. 742 remote_args_path: The remote path where the arguments are loaded. 743 deadline: The deadline written to remote_timestamp_path if this process 744 becomes the uploader. 745 746 Returns: 747 A list of string pairs, the arguments generated by UploadExtraImages. 748 None if the directory has not been initialized. 749 750 Raises: 751 errors.CreateError if timeout. 752 """ 753 timeout = int(deadline - time.time()) 754 if timeout <= 0: 755 raise errors.CreateError("Timed out before loading remote image args.") 756 757 timestamp_cmd = (f"test -s {remote_timestamp_path} && " 758 f"cat {remote_timestamp_path} || " 759 f"expr $(date +%s) + {timeout} > {remote_timestamp_path}") 760 upload_deadline = ssh_obj.Run(shlex.quote( 761 f"flock {remote_timestamp_path} -c " + 762 shlex.quote(timestamp_cmd))).strip() 763 if not upload_deadline: 764 return None 765 766 # Wait until remote_args_path is not empty or upload_deadline <= now. 767 wait_cmd = (f"test -s {remote_args_path} -o " 768 f"{upload_deadline} -le $(date +%s) || echo wait...") 769 timeout = deadline - time.time() 770 utils.PollAndWait( 771 lambda : ssh_obj.Run(shlex.quote( 772 f"flock {remote_args_path} -c " + shlex.quote(wait_cmd))), 773 expected_return="", 774 timeout_exception=errors.CreateError( 775 f"{remote_args_path} is not ready within {timeout} secs"), 776 timeout_secs=timeout, 777 sleep_interval_secs=10 + random.uniform(0, 5)) 778 779 args_str = ssh_obj.Run(shlex.quote( 780 f"flock {remote_args_path} -c " + 781 shlex.quote(f"cat {remote_args_path}"))) 782 if not args_str: 783 raise errors.CreateError( 784 f"The uploader did not meet the deadline {upload_deadline}. " 785 f"{remote_args_path} is unusable.") 786 try: 787 return json.loads(args_str) 788 except json.JSONDecodeError as e: 789 raise errors.CreateError(f"Cannot load {remote_args_path}: {e}") 790 791 792def SaveRemoteImageArgs(ssh_obj, remote_args_path, launch_cvd_args): 793 """Save launch_cvd arguments to a remote path. 794 795 Args: 796 ssh_obj: An Ssh object. 797 remote_args_path: The remote path where the arguments are saved. 798 launch_cvd_args: A list of string pairs, the arguments generated by 799 UploadExtraImages. 800 """ 801 # args_str is interpreted three times by SSH, remote shell, and flock. 802 args_str = shlex.quote(json.dumps(launch_cvd_args)) 803 ssh_obj.Run(shlex.quote( 804 f"flock {remote_args_path} -c " + 805 shlex.quote(f"echo {args_str} > {remote_args_path}"))) 806 807 808def GetConfigFromRemoteAndroidInfo(ssh_obj, remote_image_dir): 809 """Get config from android-info.txt on a remote host or a GCE instance. 810 811 Args: 812 ssh_obj: An Ssh object. 813 remote_image_dir: The remote image directory. 814 815 Returns: 816 A string, the config value. For example, "phone". 817 """ 818 android_info = ssh_obj.GetCmdOutput( 819 "cat " + 820 remote_path.join(remote_image_dir, constants.ANDROID_INFO_FILE)) 821 logger.debug("Android info: %s", android_info) 822 config_match = _CONFIG_PATTERN.search(android_info) 823 if config_match: 824 return config_match.group("config") 825 return None 826 827 828# pylint:disable=too-many-branches 829def _GetLaunchCvdArgs(avd_spec, config): 830 """Get launch_cvd arguments for remote instances. 831 832 Args: 833 avd_spec: An AVDSpec instance. 834 config: A string or None, the name of the predefined hardware config. 835 e.g., "auto", "phone", and "tv". 836 837 Returns: 838 A list of strings, arguments of launch_cvd. 839 """ 840 launch_cvd_args = [] 841 842 blank_data_disk_size_gb = avd_spec.cfg.extra_data_disk_size_gb 843 if blank_data_disk_size_gb and blank_data_disk_size_gb > 0: 844 launch_cvd_args.append( 845 "-data_policy=" + _DATA_POLICY_CREATE_IF_MISSING) 846 launch_cvd_args.append( 847 "-blank_data_image_mb=" + str(blank_data_disk_size_gb * 1024)) 848 849 if config: 850 launch_cvd_args.append("-config=" + config) 851 if avd_spec.hw_customize or not config: 852 launch_cvd_args.append( 853 "-x_res=" + avd_spec.hw_property[constants.HW_X_RES]) 854 launch_cvd_args.append( 855 "-y_res=" + avd_spec.hw_property[constants.HW_Y_RES]) 856 launch_cvd_args.append( 857 "-dpi=" + avd_spec.hw_property[constants.HW_ALIAS_DPI]) 858 if constants.HW_ALIAS_DISK in avd_spec.hw_property: 859 launch_cvd_args.append( 860 "-data_policy=" + _DATA_POLICY_ALWAYS_CREATE) 861 launch_cvd_args.append( 862 "-blank_data_image_mb=" 863 + avd_spec.hw_property[constants.HW_ALIAS_DISK]) 864 if constants.HW_ALIAS_CPUS in avd_spec.hw_property: 865 launch_cvd_args.append( 866 "-cpus=" + str(avd_spec.hw_property[constants.HW_ALIAS_CPUS])) 867 if constants.HW_ALIAS_MEMORY in avd_spec.hw_property: 868 launch_cvd_args.append( 869 "-memory_mb=" + 870 str(avd_spec.hw_property[constants.HW_ALIAS_MEMORY])) 871 872 if avd_spec.connect_webrtc: 873 launch_cvd_args.extend(_WEBRTC_ARGS) 874 if avd_spec.webrtc_device_id: 875 launch_cvd_args.append( 876 _WEBRTC_ID % {"instance": avd_spec.webrtc_device_id}) 877 if avd_spec.connect_vnc: 878 launch_cvd_args.extend(_VNC_ARGS) 879 if avd_spec.openwrt: 880 launch_cvd_args.append(_ENABLE_CONSOLE_ARG) 881 if avd_spec.num_avds_per_instance > 1: 882 launch_cvd_args.append( 883 _NUM_AVDS_ARG % {"num_AVD": avd_spec.num_avds_per_instance}) 884 if avd_spec.base_instance_num: 885 launch_cvd_args.append( 886 "--base_instance_num=" + str(avd_spec.base_instance_num)) 887 if avd_spec.launch_args: 888 # b/286321583: Need to process \" as ". 889 launch_cvd_args.append(avd_spec.launch_args.replace("\\\"", "\"")) 890 891 launch_cvd_args.append(UNDEFOK_ARG) 892 launch_cvd_args.append(AGREEMENT_PROMPT_ARG) 893 return launch_cvd_args 894 895 896def GetRemoteLaunchCvdCmd(remote_dir, avd_spec, config, extra_args): 897 """Get launch_cvd command for remote instances. 898 899 Args: 900 remote_dir: The remote base directory. 901 avd_spec: An AVDSpec instance. 902 config: A string or None, the name of the predefined hardware config. 903 e.g., "auto", "phone", and "tv". 904 extra_args: Collection of strings, the extra arguments. 905 906 Returns: 907 A string, the launch_cvd command. 908 """ 909 # FIXME: Use the images and launch_cvd in avd_spec.remote_image_dir when 910 # cuttlefish can reliably share images. 911 cmd = ["HOME=" + remote_path.join("$HOME", remote_dir), 912 remote_path.join(remote_dir, "bin", "launch_cvd"), 913 "-daemon"] 914 cmd.extend(extra_args) 915 cmd.extend(_GetLaunchCvdArgs(avd_spec, config)) 916 return " ".join(cmd) 917 918 919def ExecuteRemoteLaunchCvd(ssh_obj, cmd, boot_timeout_secs): 920 """launch_cvd command on a remote host or a GCE instance. 921 922 Args: 923 ssh_obj: An Ssh object. 924 cmd: A string generated by GetRemoteLaunchCvdCmd. 925 boot_timeout_secs: A float, the timeout for the command. 926 927 Returns: 928 The error message as a string if the command fails. 929 An empty string if the command succeeds. 930 """ 931 try: 932 ssh_obj.Run(f"-t '{cmd}'", boot_timeout_secs, retry=0) 933 except (subprocess.CalledProcessError, errors.DeviceConnectionError, 934 errors.LaunchCVDFail) as e: 935 error_msg = ("Device did not finish on boot within " 936 f"{boot_timeout_secs} secs)") 937 if constants.ERROR_MSG_VNC_NOT_SUPPORT in str(e): 938 error_msg = ("VNC is not supported in the current build. Please " 939 "try WebRTC such as '$acloud create' or " 940 "'$acloud create --autoconnect webrtc'") 941 if constants.ERROR_MSG_WEBRTC_NOT_SUPPORT in str(e): 942 error_msg = ("WEBRTC is not supported in the current build. " 943 "Please try VNC such as " 944 "'$acloud create --autoconnect vnc'") 945 utils.PrintColorString(str(e), utils.TextColors.FAIL) 946 return error_msg 947 return "" 948 949 950def _GetRemoteRuntimeDirs(ssh_obj, remote_dir, base_instance_num, 951 num_avds_per_instance): 952 """Get cuttlefish runtime directories on a remote host or a GCE instance. 953 954 Args: 955 ssh_obj: An Ssh object. 956 remote_dir: The remote base directory. 957 base_instance_num: An integer, the instance number of the first device. 958 num_avds_per_instance: An integer, the number of devices. 959 960 Returns: 961 A list of strings, the paths to the runtime directories. 962 """ 963 runtime_dir = remote_path.join( 964 remote_dir, _REMOTE_RUNTIME_DIR_FORMAT % {"num": base_instance_num}) 965 try: 966 ssh_obj.Run(f"test -d {runtime_dir}", retry=0) 967 return [remote_path.join(remote_dir, 968 _REMOTE_RUNTIME_DIR_FORMAT % 969 {"num": base_instance_num + num}) 970 for num in range(num_avds_per_instance)] 971 except subprocess.CalledProcessError: 972 logger.debug("%s is not the runtime directory.", runtime_dir) 973 974 legacy_runtime_dirs = [ 975 remote_path.join(remote_dir, constants.REMOTE_LOG_FOLDER)] 976 legacy_runtime_dirs.extend( 977 remote_path.join(remote_dir, 978 _REMOTE_LEGACY_RUNTIME_DIR_FORMAT % 979 {"num": base_instance_num + num}) 980 for num in range(1, num_avds_per_instance)) 981 return legacy_runtime_dirs 982 983 984def GetRemoteFetcherConfigJson(remote_image_dir): 985 """Get the config created by fetch_cvd on a remote host or a GCE instance. 986 987 Args: 988 remote_image_dir: The remote image directory. 989 990 Returns: 991 An object of report.LogFile. 992 """ 993 return report.LogFile( 994 remote_path.join(remote_image_dir, "fetcher_config.json"), 995 constants.LOG_TYPE_CUTTLEFISH_LOG) 996 997 998def _GetRemoteTombstone(runtime_dir, name_suffix): 999 """Get log object for tombstones in a remote cuttlefish runtime directory. 1000 1001 Args: 1002 runtime_dir: The path to the remote cuttlefish runtime directory. 1003 name_suffix: The string appended to the log name. It is used to 1004 distinguish log files found in different runtime_dirs. 1005 1006 Returns: 1007 A report.LogFile object. 1008 """ 1009 return report.LogFile(remote_path.join(runtime_dir, "tombstones"), 1010 constants.LOG_TYPE_DIR, 1011 "tombstones-zip" + name_suffix) 1012 1013 1014def _GetLogType(file_name): 1015 """Determine log type by file name. 1016 1017 Args: 1018 file_name: A file name. 1019 1020 Returns: 1021 A string, one of the log types defined in constants. 1022 None if the file is not a log file. 1023 """ 1024 if file_name == "kernel.log": 1025 return constants.LOG_TYPE_KERNEL_LOG 1026 if file_name == "logcat": 1027 return constants.LOG_TYPE_LOGCAT 1028 if file_name.endswith(".log") or file_name == "cuttlefish_config.json": 1029 return constants.LOG_TYPE_CUTTLEFISH_LOG 1030 return None 1031 1032 1033def FindRemoteLogs(ssh_obj, remote_dir, base_instance_num, 1034 num_avds_per_instance): 1035 """Find log objects on a remote host or a GCE instance. 1036 1037 Args: 1038 ssh_obj: An Ssh object. 1039 remote_dir: The remote base directory. 1040 base_instance_num: An integer or None, the instance number of the first 1041 device. 1042 num_avds_per_instance: An integer or None, the number of devices. 1043 1044 Returns: 1045 A list of report.LogFile objects. 1046 """ 1047 runtime_dirs = _GetRemoteRuntimeDirs( 1048 ssh_obj, remote_dir, 1049 (base_instance_num or 1), (num_avds_per_instance or 1)) 1050 logs = [] 1051 for log_path in utils.FindRemoteFiles(ssh_obj, runtime_dirs): 1052 file_name = remote_path.basename(log_path) 1053 log_type = _GetLogType(file_name) 1054 if not log_type: 1055 continue 1056 base, ext = remote_path.splitext(file_name) 1057 # The index of the runtime_dir containing log_path. 1058 index_str = "" 1059 for index, runtime_dir in enumerate(runtime_dirs): 1060 if log_path.startswith(runtime_dir + remote_path.sep): 1061 index_str = "." + str(index) if index else "" 1062 log_name = ("full_gce_logcat" + index_str if file_name == "logcat" else 1063 base + index_str + ext) 1064 1065 logs.append(report.LogFile(log_path, log_type, log_name)) 1066 1067 logs.extend(_GetRemoteTombstone(runtime_dir, 1068 ("." + str(index) if index else "")) 1069 for index, runtime_dir in enumerate(runtime_dirs)) 1070 return logs 1071 1072 1073def FindLocalLogs(runtime_dir, instance_num): 1074 """Find log objects in a local runtime directory. 1075 1076 Args: 1077 runtime_dir: A string, the runtime directory path. 1078 instance_num: An integer, the instance number. 1079 1080 Returns: 1081 A list of report.LogFile. 1082 """ 1083 log_dir = _LOCAL_LOG_DIR_FORMAT % {"runtime_dir": runtime_dir, 1084 "num": instance_num} 1085 if not os.path.isdir(log_dir): 1086 log_dir = runtime_dir 1087 1088 logs = [] 1089 for parent_dir, _, file_names in os.walk(log_dir, followlinks=False): 1090 for file_name in file_names: 1091 log_path = os.path.join(parent_dir, file_name) 1092 log_type = _GetLogType(file_name) 1093 if os.path.islink(log_path) or not log_type: 1094 continue 1095 logs.append(report.LogFile(log_path, log_type)) 1096 return logs 1097 1098 1099def GetOpenWrtInfoDict(ssh_obj, remote_dir): 1100 """Return the commands to connect to a remote OpenWrt console. 1101 1102 Args: 1103 ssh_obj: An Ssh object. 1104 remote_dir: The remote base directory. 1105 1106 Returns: 1107 A dict containing the OpenWrt info. 1108 """ 1109 console_path = remote_path.join(remote_dir, "cuttlefish_runtime", 1110 "console") 1111 return {"ssh_command": ssh_obj.GetBaseCmd(constants.SSH_BIN), 1112 "screen_command": "screen " + console_path} 1113 1114 1115def GetRemoteBuildInfoDict(avd_spec): 1116 """Convert remote build infos to a dictionary for reporting. 1117 1118 Args: 1119 avd_spec: An AvdSpec object containing the build infos. 1120 1121 Returns: 1122 A dict containing the build infos. 1123 """ 1124 build_info_dict = { 1125 key: val for key, val in avd_spec.remote_image.items() if val} 1126 1127 # kernel_target has a default value. If the user provides kernel_build_id 1128 # or kernel_branch, then convert kernel build info. 1129 if (avd_spec.kernel_build_info.get(constants.BUILD_ID) or 1130 avd_spec.kernel_build_info.get(constants.BUILD_BRANCH)): 1131 build_info_dict.update( 1132 {"kernel_" + key: val 1133 for key, val in avd_spec.kernel_build_info.items() if val} 1134 ) 1135 build_info_dict.update( 1136 {"system_" + key: val 1137 for key, val in avd_spec.system_build_info.items() if val} 1138 ) 1139 build_info_dict.update( 1140 {"bootloader_" + key: val 1141 for key, val in avd_spec.bootloader_build_info.items() if val} 1142 ) 1143 build_info_dict.update( 1144 {"android_efi_loader_" + key: val 1145 for key, val in avd_spec.android_efi_loader_build_info.items() if val} 1146 ) 1147 return build_info_dict 1148 1149 1150def GetMixBuildTargetFilename(build_target, build_id): 1151 """Get the mix build target filename. 1152 1153 Args: 1154 build_id: String, Build id, e.g. "2263051", "P2804227" 1155 build_target: String, the build target, e.g. cf_x86_phone-userdebug 1156 1157 Returns: 1158 String, a file name, e.g. "cf_x86_phone-target_files-2263051.zip" 1159 """ 1160 return _DOWNLOAD_MIX_IMAGE_NAME.format( 1161 build_target=build_target.split('-')[0], 1162 build_id=build_id) 1163 1164 1165def FindMiscInfo(image_dir): 1166 """Find misc info in build output dir or extracted target files. 1167 1168 Args: 1169 image_dir: The directory to search for misc info. 1170 1171 Returns: 1172 image_dir if the directory structure looks like an output directory 1173 in build environment. 1174 image_dir/META if it looks like extracted target files. 1175 1176 Raises: 1177 errors.CheckPathError if this function cannot find misc info. 1178 """ 1179 misc_info_path = os.path.join(image_dir, _MISC_INFO_FILE_NAME) 1180 if os.path.isfile(misc_info_path): 1181 return misc_info_path 1182 misc_info_path = os.path.join(image_dir, _TARGET_FILES_META_DIR_NAME, 1183 _MISC_INFO_FILE_NAME) 1184 if os.path.isfile(misc_info_path): 1185 return misc_info_path 1186 raise errors.CheckPathError( 1187 f"Cannot find {_MISC_INFO_FILE_NAME} in {image_dir}. The " 1188 f"directory is expected to be an extracted target files zip or " 1189 f"{constants.ENV_ANDROID_PRODUCT_OUT}.") 1190 1191 1192def FindImageDir(image_dir): 1193 """Find images in build output dir or extracted target files. 1194 1195 Args: 1196 image_dir: The directory to search for images. 1197 1198 Returns: 1199 image_dir if the directory structure looks like an output directory 1200 in build environment. 1201 image_dir/IMAGES if it looks like extracted target files. 1202 1203 Raises: 1204 errors.GetLocalImageError if this function cannot find any image. 1205 """ 1206 if glob.glob(os.path.join(image_dir, "*.img")): 1207 return image_dir 1208 subdir = os.path.join(image_dir, _TARGET_FILES_IMAGES_DIR_NAME) 1209 if glob.glob(os.path.join(subdir, "*.img")): 1210 return subdir 1211 raise errors.GetLocalImageError( 1212 "Cannot find images in %s." % image_dir) 1213 1214 1215def RunOnArmMachine(ssh_obj): 1216 """Check if the AVD will be run on an ARM-based machine. 1217 1218 Args: 1219 ssh_obj: An Ssh object. 1220 1221 Returns: 1222 A boolean, whether the AVD will be run on an ARM-based machine. 1223 """ 1224 cmd = "uname -m" 1225 cmd_output = ssh_obj.GetCmdOutput(cmd).strip() 1226 logger.debug("cmd: %s, cmd output: %s", cmd, cmd_output) 1227 return cmd_output == _ARM_MACHINE_TYPE 1228 1229 1230def FindVendorImages(image_dir): 1231 """Find vendor, vendor_dlkm, odm, and odm_dlkm image in build output dir. 1232 1233 Args: 1234 image_dir: The directory to search for images. 1235 1236 Returns: 1237 An object of VendorImagePaths. 1238 1239 Raises: 1240 errors.GetLocalImageError if this function cannot find images. 1241 """ 1242 image_dir = FindImageDir(image_dir) 1243 image_paths = [] 1244 for image_name in _VENDOR_IMAGE_NAMES: 1245 image_path = os.path.join(image_dir, image_name) 1246 if not os.path.isfile(image_path): 1247 raise errors.GetLocalImageError( 1248 f"Cannot find {image_path} in {image_dir}.") 1249 image_paths.append(image_path) 1250 1251 return VendorImagePaths(*image_paths) 1252