• 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# 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, subprocess.TimeoutExpired,
936            errors.DeviceConnectionError, errors.LaunchCVDFail) as e:
937        error_msg = "Device did not boot"
938        if isinstance(e, subprocess.TimeoutExpired):
939            error_msg = ("Device did not finish on boot within "
940                         f"{boot_timeout_secs} secs)")
941        if constants.ERROR_MSG_VNC_NOT_SUPPORT in str(e):
942            error_msg = ("VNC is not supported in the current build. Please "
943                         "try WebRTC such as '$acloud create' or "
944                         "'$acloud create --autoconnect webrtc'")
945        if constants.ERROR_MSG_WEBRTC_NOT_SUPPORT in str(e):
946            error_msg = ("WEBRTC is not supported in the current build. "
947                         "Please try VNC such as "
948                         "'$acloud create --autoconnect vnc'")
949        utils.PrintColorString(str(e), utils.TextColors.FAIL)
950        return error_msg
951    return ""
952
953
954def _GetRemoteRuntimeDirs(ssh_obj, remote_dir, base_instance_num,
955                          num_avds_per_instance):
956    """Get cuttlefish runtime directories on a remote host or a GCE instance.
957
958    Args:
959        ssh_obj: An Ssh object.
960        remote_dir: The remote base directory.
961        base_instance_num: An integer, the instance number of the first device.
962        num_avds_per_instance: An integer, the number of devices.
963
964    Returns:
965        A list of strings, the paths to the runtime directories.
966    """
967    runtime_dir = remote_path.join(
968        remote_dir, _REMOTE_RUNTIME_DIR_FORMAT % {"num": base_instance_num})
969    try:
970        ssh_obj.Run(f"test -d {runtime_dir}", retry=0)
971        return [remote_path.join(remote_dir,
972                                 _REMOTE_RUNTIME_DIR_FORMAT %
973                                 {"num": base_instance_num + num})
974                for num in range(num_avds_per_instance)]
975    except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
976        logger.debug("%s is not the runtime directory.", runtime_dir)
977
978    legacy_runtime_dirs = [
979        remote_path.join(remote_dir, constants.REMOTE_LOG_FOLDER)]
980    legacy_runtime_dirs.extend(
981        remote_path.join(remote_dir,
982                         _REMOTE_LEGACY_RUNTIME_DIR_FORMAT %
983                         {"num": base_instance_num + num})
984        for num in range(1, num_avds_per_instance))
985    return legacy_runtime_dirs
986
987
988def GetRemoteFetcherConfigJson(remote_image_dir):
989    """Get the config created by fetch_cvd on a remote host or a GCE instance.
990
991    Args:
992        remote_image_dir: The remote image directory.
993
994    Returns:
995        An object of report.LogFile.
996    """
997    return report.LogFile(
998        remote_path.join(remote_image_dir, "fetcher_config.json"),
999        constants.LOG_TYPE_CUTTLEFISH_LOG)
1000
1001
1002def _GetRemoteTombstone(runtime_dir, name_suffix):
1003    """Get log object for tombstones in a remote cuttlefish runtime directory.
1004
1005    Args:
1006        runtime_dir: The path to the remote cuttlefish runtime directory.
1007        name_suffix: The string appended to the log name. It is used to
1008                     distinguish log files found in different runtime_dirs.
1009
1010    Returns:
1011        A report.LogFile object.
1012    """
1013    return report.LogFile(remote_path.join(runtime_dir, "tombstones"),
1014                          constants.LOG_TYPE_DIR,
1015                          "tombstones-zip" + name_suffix)
1016
1017
1018def _GetLogType(file_name):
1019    """Determine log type by file name.
1020
1021    Args:
1022        file_name: A file name.
1023
1024    Returns:
1025        A string, one of the log types defined in constants.
1026        None if the file is not a log file.
1027    """
1028    if file_name == "kernel.log":
1029        return constants.LOG_TYPE_KERNEL_LOG
1030    if file_name == "logcat":
1031        return constants.LOG_TYPE_LOGCAT
1032    if file_name.endswith(".log") or file_name == "cuttlefish_config.json":
1033        return constants.LOG_TYPE_CUTTLEFISH_LOG
1034    return None
1035
1036
1037def FindRemoteLogs(ssh_obj, remote_dir, base_instance_num,
1038                   num_avds_per_instance):
1039    """Find log objects on a remote host or a GCE instance.
1040
1041    Args:
1042        ssh_obj: An Ssh object.
1043        remote_dir: The remote base directory.
1044        base_instance_num: An integer or None, the instance number of the first
1045                           device.
1046        num_avds_per_instance: An integer or None, the number of devices.
1047
1048    Returns:
1049        A list of report.LogFile objects.
1050    """
1051    runtime_dirs = _GetRemoteRuntimeDirs(
1052        ssh_obj, remote_dir,
1053        (base_instance_num or 1), (num_avds_per_instance or 1))
1054    logs = []
1055    for log_path in utils.FindRemoteFiles(ssh_obj, runtime_dirs):
1056        file_name = remote_path.basename(log_path)
1057        log_type = _GetLogType(file_name)
1058        if not log_type:
1059            continue
1060        base, ext = remote_path.splitext(file_name)
1061        # The index of the runtime_dir containing log_path.
1062        index_str = ""
1063        for index, runtime_dir in enumerate(runtime_dirs):
1064            if log_path.startswith(runtime_dir + remote_path.sep):
1065                index_str = "." + str(index) if index else ""
1066        log_name = ("full_gce_logcat" + index_str if file_name == "logcat" else
1067                    base + index_str + ext)
1068
1069        logs.append(report.LogFile(log_path, log_type, log_name))
1070
1071    logs.extend(_GetRemoteTombstone(runtime_dir,
1072                                    ("." + str(index) if index else ""))
1073                for index, runtime_dir in enumerate(runtime_dirs))
1074    return logs
1075
1076
1077def FindLocalLogs(runtime_dir, instance_num):
1078    """Find log objects in a local runtime directory.
1079
1080    Args:
1081        runtime_dir: A string, the runtime directory path.
1082        instance_num: An integer, the instance number.
1083
1084    Returns:
1085        A list of report.LogFile.
1086    """
1087    log_dir = _LOCAL_LOG_DIR_FORMAT % {"runtime_dir": runtime_dir,
1088                                       "num": instance_num}
1089    if not os.path.isdir(log_dir):
1090        log_dir = runtime_dir
1091
1092    logs = []
1093    for parent_dir, _, file_names in os.walk(log_dir, followlinks=False):
1094        for file_name in file_names:
1095            log_path = os.path.join(parent_dir, file_name)
1096            log_type = _GetLogType(file_name)
1097            if os.path.islink(log_path) or not log_type:
1098                continue
1099            logs.append(report.LogFile(log_path, log_type))
1100    return logs
1101
1102
1103def GetOpenWrtInfoDict(ssh_obj, remote_dir):
1104    """Return the commands to connect to a remote OpenWrt console.
1105
1106    Args:
1107        ssh_obj: An Ssh object.
1108        remote_dir: The remote base directory.
1109
1110    Returns:
1111        A dict containing the OpenWrt info.
1112    """
1113    console_path = remote_path.join(remote_dir, "cuttlefish_runtime",
1114                                    "console")
1115    return {"ssh_command": ssh_obj.GetBaseCmd(constants.SSH_BIN),
1116            "screen_command": "screen " + console_path}
1117
1118
1119def GetRemoteBuildInfoDict(avd_spec):
1120    """Convert remote build infos to a dictionary for reporting.
1121
1122    Args:
1123        avd_spec: An AvdSpec object containing the build infos.
1124
1125    Returns:
1126        A dict containing the build infos.
1127    """
1128    build_info_dict = {
1129        key: val for key, val in avd_spec.remote_image.items() if val}
1130
1131    # kernel_target has a default value. If the user provides kernel_build_id
1132    # or kernel_branch, then convert kernel build info.
1133    if (avd_spec.kernel_build_info.get(constants.BUILD_ID) or
1134            avd_spec.kernel_build_info.get(constants.BUILD_BRANCH)):
1135        build_info_dict.update(
1136            {"kernel_" + key: val
1137             for key, val in avd_spec.kernel_build_info.items() if val}
1138        )
1139    build_info_dict.update(
1140        {"system_" + key: val
1141         for key, val in avd_spec.system_build_info.items() if val}
1142    )
1143    build_info_dict.update(
1144        {"bootloader_" + key: val
1145         for key, val in avd_spec.bootloader_build_info.items() if val}
1146    )
1147    build_info_dict.update(
1148        {"android_efi_loader_" + key: val
1149         for key, val in avd_spec.android_efi_loader_build_info.items() if val}
1150    )
1151    return build_info_dict
1152
1153
1154def GetMixBuildTargetFilename(build_target, build_id):
1155    """Get the mix build target filename.
1156
1157    Args:
1158        build_id: String, Build id, e.g. "2263051", "P2804227"
1159        build_target: String, the build target, e.g. cf_x86_phone-userdebug
1160
1161    Returns:
1162        String, a file name, e.g. "cf_x86_phone-target_files-2263051.zip"
1163    """
1164    return _DOWNLOAD_MIX_IMAGE_NAME.format(
1165        build_target=build_target.split('-')[0],
1166        build_id=build_id)
1167
1168
1169def FindMiscInfo(image_dir):
1170    """Find misc info in build output dir or extracted target files.
1171
1172    Args:
1173        image_dir: The directory to search for misc info.
1174
1175    Returns:
1176        image_dir if the directory structure looks like an output directory
1177        in build environment.
1178        image_dir/META if it looks like extracted target files.
1179
1180    Raises:
1181        errors.CheckPathError if this function cannot find misc info.
1182    """
1183    misc_info_path = os.path.join(image_dir, _MISC_INFO_FILE_NAME)
1184    if os.path.isfile(misc_info_path):
1185        return misc_info_path
1186    misc_info_path = os.path.join(image_dir, _TARGET_FILES_META_DIR_NAME,
1187                                  _MISC_INFO_FILE_NAME)
1188    if os.path.isfile(misc_info_path):
1189        return misc_info_path
1190    raise errors.CheckPathError(
1191        f"Cannot find {_MISC_INFO_FILE_NAME} in {image_dir}. The "
1192        f"directory is expected to be an extracted target files zip or "
1193        f"{constants.ENV_ANDROID_PRODUCT_OUT}.")
1194
1195
1196def FindImageDir(image_dir):
1197    """Find images in build output dir or extracted target files.
1198
1199    Args:
1200        image_dir: The directory to search for images.
1201
1202    Returns:
1203        image_dir if the directory structure looks like an output directory
1204        in build environment.
1205        image_dir/IMAGES if it looks like extracted target files.
1206
1207    Raises:
1208        errors.GetLocalImageError if this function cannot find any image.
1209    """
1210    if glob.glob(os.path.join(image_dir, "*.img")):
1211        return image_dir
1212    subdir = os.path.join(image_dir, _TARGET_FILES_IMAGES_DIR_NAME)
1213    if glob.glob(os.path.join(subdir, "*.img")):
1214        return subdir
1215    raise errors.GetLocalImageError(
1216        "Cannot find images in %s." % image_dir)
1217
1218
1219def RunOnArmMachine(ssh_obj):
1220    """Check if the AVD will be run on an ARM-based machine.
1221
1222    Args:
1223        ssh_obj: An Ssh object.
1224
1225    Returns:
1226        A boolean, whether the AVD will be run on an ARM-based machine.
1227    """
1228    cmd = "uname -m"
1229    cmd_output = ssh_obj.GetCmdOutput(cmd).strip()
1230    logger.debug("cmd: %s, cmd output: %s", cmd, cmd_output)
1231    return cmd_output == _ARM_MACHINE_TYPE
1232
1233
1234def FindVendorImages(image_dir):
1235    """Find vendor, vendor_dlkm, odm, and odm_dlkm image in build output dir.
1236
1237    Args:
1238        image_dir: The directory to search for images.
1239
1240    Returns:
1241        An object of VendorImagePaths.
1242
1243    Raises:
1244        errors.GetLocalImageError if this function cannot find images.
1245    """
1246    image_dir = FindImageDir(image_dir)
1247    image_paths = []
1248    for image_name in _VENDOR_IMAGE_NAMES:
1249        image_path = os.path.join(image_dir, image_name)
1250        if not os.path.isfile(image_path):
1251            raise errors.GetLocalImageError(
1252                f"Cannot find {image_path} in {image_dir}.")
1253        image_paths.append(image_path)
1254
1255    return VendorImagePaths(*image_paths)
1256