xref: /aosp_15_r20/tools/acloud/internal/lib/cvd_utils.py (revision 800a58d989c669b8eb8a71d8df53b1ba3d411444)
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