• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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