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