• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 - 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"""RemoteInstanceDeviceFactory provides basic interface to create a goldfish
16device factory."""
17
18import collections
19import logging
20import os
21import posixpath as remote_path
22import re
23import shutil
24import subprocess
25import tempfile
26import time
27import zipfile
28
29from acloud import errors
30from acloud.create import create_common
31from acloud.internal import constants
32from acloud.internal.lib import android_build_client
33from acloud.internal.lib import auth
34from acloud.internal.lib import goldfish_utils
35from acloud.internal.lib import emulator_console
36from acloud.internal.lib import ota_tools
37from acloud.internal.lib import remote_host_client
38from acloud.internal.lib import utils
39from acloud.internal.lib import ssh
40from acloud.public import report
41from acloud.public.actions import base_device_factory
42
43
44logger = logging.getLogger(__name__)
45# Artifacts
46_SDK_REPO_IMAGE_ZIP_NAME_FORMAT = ("sdk-repo-linux-system-images-"
47                                   "%(build_id)s.zip")
48_EXTRA_IMAGE_ZIP_NAME_FORMAT = "emu-extra-linux-system-images-%(build_id)s.zip"
49_IMAGE_ZIP_NAME_FORMAT = "%(build_target)s-img-%(build_id)s.zip"
50_OTA_TOOLS_ZIP_NAME = "otatools.zip"
51_EMULATOR_INFO_NAME = "emulator-info.txt"
52_EMULATOR_VERSION_PATTERN = re.compile(r"require\s+version-emulator="
53                                       r"(?P<build_id>\w+)")
54_EMULATOR_ZIP_NAME_FORMAT = "sdk-repo-%(os)s-emulator-%(build_id)s.zip"
55_EMULATOR_BIN_DIR_NAMES = ("bin64", "qemu")
56_EMULATOR_BIN_NAME = "emulator"
57_SDK_REPO_EMULATOR_DIR_NAME = "emulator"
58# Files in temporary artifact directory.
59_DOWNLOAD_DIR_NAME = "download"
60_OTA_TOOLS_DIR_NAME = "ota_tools"
61_SYSTEM_IMAGE_NAME = "system.img"
62# Base directory of an instance.
63_REMOTE_INSTANCE_DIR_FORMAT = "acloud_gf_%d"
64# Relative paths in a base directory.
65_REMOTE_IMAGE_ZIP_PATH = "image.zip"
66_REMOTE_EMULATOR_ZIP_PATH = "emulator.zip"
67_REMOTE_IMAGE_DIR = "image"
68_REMOTE_KERNEL_PATH = "kernel"
69_REMOTE_RAMDISK_PATH = "mixed_ramdisk"
70_REMOTE_EMULATOR_DIR = "emulator"
71_REMOTE_RUNTIME_DIR = "instance"
72_REMOTE_LOGCAT_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "logcat.txt")
73_REMOTE_STDOUT_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "kernel.log")
74_REMOTE_STDERR_PATH = remote_path.join(_REMOTE_RUNTIME_DIR, "emu_stderr.txt")
75# Runtime parameters
76_EMULATOR_DEFAULT_CONSOLE_PORT = 5554
77_DEFAULT_BOOT_TIMEOUT_SECS = 150
78# Error messages
79_MISSING_EMULATOR_MSG = ("No emulator zip. Specify "
80                         "--emulator-build-id, or --emulator-zip.")
81
82ArtifactPaths = collections.namedtuple(
83    "ArtifactPaths",
84    ["image_zip", "emulator_zip", "ota_tools_dir",
85     "system_image", "system_dlkm_image", "boot_image"])
86
87RemotePaths = collections.namedtuple(
88    "RemotePaths",
89    ["image_dir", "emulator_dir", "kernel", "ramdisk"])
90
91
92class RemoteHostGoldfishDeviceFactory(base_device_factory.BaseDeviceFactory):
93    """A class that creates a goldfish device on a remote host.
94
95    Attributes:
96        avd_spec: AVDSpec object that tells us what we're going to create.
97        android_build_client: An AndroidBuildClient that is lazily initialized.
98        temp_artifact_dir: The temporary artifact directory that is lazily
99                           initialized during PrepareArtifacts.
100        ssh: Ssh object that executes commands on the remote host.
101        failures: A dictionary the maps instance names to
102                  error.DeviceBootError objects.
103        logs: A dictionary that maps instance names to lists of report.LogFile.
104    """
105    def __init__(self, avd_spec):
106        """Initialize the attributes and the compute client."""
107        self._avd_spec = avd_spec
108        self._android_build_client = None
109        self._temp_artifact_dir = None
110        self._ssh = ssh.Ssh(
111            ip=ssh.IP(ip=self._avd_spec.remote_host),
112            user=self._ssh_user,
113            ssh_private_key_path=self._ssh_private_key_path,
114            extra_args_ssh_tunnel=self._ssh_extra_args,
115            report_internal_ip=False)
116        self._failures = {}
117        self._logs = {}
118        super().__init__(compute_client=(
119            remote_host_client.RemoteHostClient(avd_spec.remote_host)))
120
121    @property
122    def _build_api(self):
123        """Initialize android_build_client."""
124        if not self._android_build_client:
125            credentials = auth.CreateCredentials(self._avd_spec.cfg)
126            self._android_build_client = android_build_client.AndroidBuildClient(
127                credentials)
128        return self._android_build_client
129
130    @property
131    def _artifact_dir(self):
132        """Initialize temp_artifact_dir."""
133        if not self._temp_artifact_dir:
134            self._temp_artifact_dir = tempfile.mkdtemp("host_gf")
135            logger.info("Create temporary artifact directory: %s",
136                        self._temp_artifact_dir)
137        return self._temp_artifact_dir
138
139    @property
140    def _download_dir(self):
141        """Get the directory where the artifacts are downloaded."""
142        if self._avd_spec.image_download_dir:
143            return self._avd_spec.image_download_dir
144        return os.path.join(self._artifact_dir, _DOWNLOAD_DIR_NAME)
145
146    @property
147    def _ssh_user(self):
148        return self._avd_spec.host_user or constants.GCE_USER
149
150    @property
151    def _ssh_private_key_path(self):
152        return (self._avd_spec.host_ssh_private_key_path or
153                self._avd_spec.cfg.ssh_private_key_path)
154
155    @property
156    def _ssh_extra_args(self):
157        return self._avd_spec.cfg.extra_args_ssh_tunnel
158
159    def _GetConsolePort(self):
160        """Calculate the console port from the instance number.
161
162        By convention, the console port is an even number, and the adb port is
163        the console port + 1. The first instance uses port 5554 and 5555. The
164        second instance uses 5556 and 5557, and so on.
165        """
166        return (_EMULATOR_DEFAULT_CONSOLE_PORT +
167                ((self._avd_spec.base_instance_num or 1) - 1) * 2)
168
169    def _GetInstancePath(self, relative_path):
170        """Append a relative path to the instance directory."""
171        return remote_path.join(
172            _REMOTE_INSTANCE_DIR_FORMAT %
173            (self._avd_spec.base_instance_num or 1),
174            relative_path)
175
176    def CreateInstance(self):
177        """Create a goldfish instance on the remote host.
178
179        Returns:
180            The instance name.
181        """
182        instance_name = goldfish_utils.FormatRemoteHostInstanceName(
183            self._avd_spec.remote_host,
184            self._GetConsolePort(),
185            self._avd_spec.remote_image)
186
187        client = self.GetComputeClient()
188        timed_stage = constants.TIME_GCE
189        start_time = time.time()
190        try:
191            client.SetStage(constants.STAGE_SSH_CONNECT)
192            self._InitRemoteHost()
193
194            start_time = client.RecordTime(timed_stage, start_time)
195            timed_stage = constants.TIME_ARTIFACT
196            client.SetStage(constants.STAGE_ARTIFACT)
197            remote_paths = self._PrepareArtifacts()
198
199            start_time = client.RecordTime(timed_stage, start_time)
200            timed_stage = constants.TIME_LAUNCH
201            client.SetStage(constants.STAGE_BOOT_UP)
202            self._logs[instance_name] = self._GetEmulatorLogs()
203            self._StartEmulator(remote_paths)
204            self._WaitForEmulator()
205        except (errors.DriverError, subprocess.CalledProcessError) as e:
206            # Catch the generic runtime error and CalledProcessError which is
207            # raised by the ssh module.
208            self._failures[instance_name] = e
209        finally:
210            client.RecordTime(timed_stage, start_time)
211
212        return instance_name
213
214    def _InitRemoteHost(self):
215        """Remove the existing instance and the instance directory."""
216        # Disable authentication for emulator console.
217        self._ssh.Run("""'echo -n "" > .emulator_console_auth_token'""")
218        try:
219            with emulator_console.RemoteEmulatorConsole(
220                    self._avd_spec.remote_host,
221                    self._GetConsolePort(),
222                    self._ssh_user,
223                    self._ssh_private_key_path,
224                    self._ssh_extra_args) as console:
225                console.Kill()
226            logger.info("Killed existing emulator.")
227        except errors.DeviceConnectionError as e:
228            logger.info("Did not kill existing emulator: %s", str(e))
229        # Delete instance files.
230        self._ssh.Run(f"rm -rf {self._GetInstancePath('')}")
231
232    def _PrepareArtifacts(self):
233        """Prepare artifacts on remote host.
234
235        This method retrieves artifacts from cache or Android Build API and
236        uploads them to the remote host.
237
238        Returns:
239            An object of RemotePaths.
240        """
241        try:
242            artifact_paths = self._RetrieveArtifacts()
243            return self._UploadArtifacts(artifact_paths)
244        finally:
245            if self._temp_artifact_dir:
246                shutil.rmtree(self._temp_artifact_dir, ignore_errors=True)
247                self._temp_artifact_dir = None
248
249    @staticmethod
250    def _InferEmulatorZipName(build_target, build_id):
251        """Determine the emulator zip name in build artifacts.
252
253        The emulator zip name is composed of build variables that are not
254        revealed in the artifacts. This method infers the emulator zip name
255        from its build target name.
256
257        Args:
258            build_target: The emulator build target name, e.g.,
259                          "emulator-linux_x64_nolocationui", "aarch64_sdk_tools_mac".
260            build_id: A string, the emulator build ID.
261
262        Returns:
263            The name of the emulator zip. e.g.,
264            "sdk-repo-linux-emulator-123456.zip",
265            "sdk-repo-darwin_aarch64-emulator-123456.zip".
266        """
267        split_target = [x for product_variant in build_target.split("-")
268                        for x in product_variant.split("_")]
269        if "darwin" in split_target or "mac" in split_target:
270            os_name = "darwin"
271        else:
272            os_name = "linux"
273        if "aarch64" in split_target:
274            os_name = os_name + "_aarch64"
275        return _EMULATOR_ZIP_NAME_FORMAT % {"os": os_name,
276                                            "build_id": build_id}
277
278    def _RetrieveArtifact(self, build_target, build_id,
279                          resource_id):
280        """Retrieve an artifact from cache or Android Build API.
281
282        Args:
283            build_target: A string, the build target of the artifact. e.g.,
284                          "sdk_phone_x86_64-userdebug".
285            build_id: A string, the build ID of the artifact.
286            resource_id: A string, the name of the artifact. e.g.,
287                         "sdk-repo-linux-system-images-123456.zip".
288
289        Returns:
290            The path to the artifact in download_dir.
291        """
292        local_path = os.path.join(self._download_dir, build_id, build_target,
293                                  resource_id)
294        if os.path.isfile(local_path):
295            logger.info("Skip downloading existing artifact: %s", local_path)
296            return local_path
297
298        complete = False
299        try:
300            os.makedirs(os.path.dirname(local_path), exist_ok=True)
301            self._build_api.DownloadArtifact(
302                build_target, build_id, resource_id, local_path,
303                self._build_api.LATEST)
304            complete = True
305        finally:
306            if not complete and os.path.isfile(local_path):
307                os.remove(local_path)
308        return local_path
309
310    @utils.TimeExecute(function_description="Download Android Build artifacts")
311    def _RetrieveArtifacts(self):
312        """Retrieve goldfish images and tools from cache or Android Build API.
313
314        Returns:
315            An object of ArtifactPaths.
316
317        Raises:
318            errors.GetRemoteImageError: Fails to download rom images.
319            errors.GetLocalImageError: Fails to validate local image zip.
320            errors.GetSdkRepoPackageError: Fails to retrieve emulator zip.
321        """
322        # Device images.
323        if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
324            image_zip_path = self._RetrieveDeviceImageZip()
325        elif self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
326            image_zip_path = self._avd_spec.local_image_artifact
327            if not image_zip_path or not zipfile.is_zipfile(image_zip_path):
328                raise errors.GetLocalImageError(
329                    f"{image_zip_path or self._avd_spec.local_image_dir} is "
330                    "not an SDK repository zip.")
331        else:
332            raise errors.CreateError(
333                f"Unknown image source: {self._avd_spec.image_source}")
334
335        # Emulator tools.
336        emu_zip_path = (self._avd_spec.emulator_zip or
337                        self._RetrieveEmulatorZip())
338        if not emu_zip_path:
339            raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG)
340
341        # System image.
342        if self._avd_spec.local_system_image:
343            # No known use case requires replacing system_ext and product.
344            system_image_path = create_common.FindSystemImages(
345                self._avd_spec.local_system_image).system
346        else:
347            system_image_path = self._RetrieveSystemImage()
348
349        # system_dlkm image.
350        if self._avd_spec.local_system_dlkm_image:
351            system_dlkm_image_path = goldfish_utils.FindSystemDlkmImage(
352                self._avd_spec.local_system_dlkm_image)
353        else:
354            # No known use case requires remote system_dlkm.
355            system_dlkm_image_path = None
356
357        # Boot image.
358        if self._avd_spec.local_kernel_image:
359            boot_image_path = create_common.FindBootImage(
360                self._avd_spec.local_kernel_image)
361        else:
362            boot_image_path = self._RetrieveBootImage()
363
364        # OTA tools.
365        ota_tools_dir = None
366        if system_image_path or system_dlkm_image_path or boot_image_path:
367            if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
368                ota_tools_dir = self._RetrieveOtaTools()
369            else:
370                ota_tools_dir = ota_tools.FindOtaToolsDir(
371                    self._avd_spec.local_tool_dirs +
372                    create_common.GetNonEmptyEnvVars(
373                        constants.ENV_ANDROID_SOONG_HOST_OUT,
374                        constants.ENV_ANDROID_HOST_OUT))
375
376        return ArtifactPaths(image_zip_path, emu_zip_path, ota_tools_dir,
377                             system_image_path, system_dlkm_image_path,
378                             boot_image_path)
379
380    def _RetrieveDeviceImageZip(self):
381        """Retrieve device image zip from cache or Android Build API.
382
383        Returns:
384            The path to the device image zip in download_dir.
385        """
386        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
387        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
388        image_zip_name_format = (_EXTRA_IMAGE_ZIP_NAME_FORMAT if
389                                 self._ShouldMixDiskImage() else
390                                 _SDK_REPO_IMAGE_ZIP_NAME_FORMAT)
391        return self._RetrieveArtifact(
392            build_target, build_id,
393            image_zip_name_format % {"build_id": build_id})
394
395    def _RetrieveEmulatorBuildID(self):
396        """Retrieve required emulator build from a goldfish image build.
397
398        Returns:
399            A string, the emulator build ID.
400            None if the build info is empty.
401        """
402        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
403        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
404        if build_id and build_target:
405            emu_info_path = self._RetrieveArtifact(build_target, build_id,
406                                                   _EMULATOR_INFO_NAME)
407            with open(emu_info_path, "r", encoding="utf-8") as emu_info:
408                for line in emu_info:
409                    match = _EMULATOR_VERSION_PATTERN.fullmatch(line.strip())
410                    if match:
411                        logger.info("Found emulator build ID: %s", line)
412                        return match.group("build_id")
413        return None
414
415    def _RetrieveEmulatorZip(self):
416        """Retrieve emulator zip from cache or Android Build API.
417
418        Returns:
419            The path to the emulator zip in download_dir.
420            None if this method cannot determine the emulator build ID.
421        """
422        emu_build_id = (self._avd_spec.emulator_build_id or
423                        self._RetrieveEmulatorBuildID())
424        if not emu_build_id:
425            return None
426        emu_build_target = (self._avd_spec.emulator_build_target or
427                            self._avd_spec.cfg.emulator_build_target)
428        emu_zip_name = self._InferEmulatorZipName(emu_build_target,
429                                                  emu_build_id)
430        return self._RetrieveArtifact(emu_build_target, emu_build_id,
431                                      emu_zip_name)
432
433    def _RetrieveSystemImage(self):
434        """Retrieve and unzip system image if system build info is not empty.
435
436        Returns:
437            The path to the temporary system image.
438            None if the system build info is empty.
439        """
440        build_id = self._avd_spec.system_build_info.get(constants.BUILD_ID)
441        build_target = self._avd_spec.system_build_info.get(
442            constants.BUILD_TARGET)
443        if not build_id or not build_target:
444            return None
445        image_zip_name = _IMAGE_ZIP_NAME_FORMAT % {
446            "build_target": build_target.split("-", 1)[0],
447            "build_id": build_id}
448        image_zip_path = self._RetrieveArtifact(build_target, build_id,
449                                                image_zip_name)
450        logger.debug("Unzip %s from %s to %s.",
451                     _SYSTEM_IMAGE_NAME, image_zip_path, self._artifact_dir)
452        with zipfile.ZipFile(image_zip_path, "r") as zip_file:
453            zip_file.extract(_SYSTEM_IMAGE_NAME, self._artifact_dir)
454        return os.path.join(self._artifact_dir, _SYSTEM_IMAGE_NAME)
455
456    def _RetrieveBootImage(self):
457        """Retrieve boot image if boot build info is not empty.
458
459        Returns:
460            The path to the boot image in download_dir.
461            None if the boot build info is empty.
462        """
463        build_id = self._avd_spec.boot_build_info.get(constants.BUILD_ID)
464        build_target = self._avd_spec.boot_build_info.get(
465            constants.BUILD_TARGET)
466        image_name = self._avd_spec.boot_build_info.get(
467            constants.BUILD_ARTIFACT)
468        if build_id and build_target and image_name:
469            return self._RetrieveArtifact(build_target, build_id, image_name)
470        return None
471
472    def _RetrieveOtaTools(self):
473        """Retrieve and unzip OTA tools.
474
475        This method retrieves OTA tools from the goldfish build which contains
476        mk_combined_img.
477
478        Returns:
479            The path to the temporary OTA tools directory.
480        """
481        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
482        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
483        zip_path = self._RetrieveArtifact(build_target, build_id,
484                                          _OTA_TOOLS_ZIP_NAME)
485        ota_tools_dir = os.path.join(self._artifact_dir, _OTA_TOOLS_DIR_NAME)
486        logger.debug("Unzip %s to %s.", zip_path, ota_tools_dir)
487        os.mkdir(ota_tools_dir)
488        with zipfile.ZipFile(zip_path, "r") as zip_file:
489            zip_file.extractall(ota_tools_dir)
490        return ota_tools_dir
491
492    @staticmethod
493    def _GetSubdirNameInZip(zip_path):
494        """Get the name of the only subdirectory in a zip.
495
496        In an SDK repository zip, the images and the binaries are located in a
497        subdirectory. This class needs to find out the subdirectory name in
498        order to construct the remote commands.
499
500        For example, in a sdk-repo-linux-system-images-*.zip for arm64, all
501        files are in "arm64-v8a/". The zip entries are:
502
503        arm64-v8a/NOTICE.txt
504        arm64-v8a/system.img
505        arm64-v8a/data/local.prop
506        ...
507
508        This method scans the entries and returns the common subdirectory name.
509        """
510        sep = "/"
511        with zipfile.ZipFile(zip_path, 'r') as zip_obj:
512            entries = zip_obj.namelist()
513            if len(entries) > 0 and sep in entries[0]:
514                subdir = entries[0].split(sep, 1)[0]
515                if all(e.startswith(subdir + sep) for e in entries):
516                    return subdir
517            logger.warning("Expect one subdirectory in %s. Actual entries: %s",
518                           zip_path, " ".join(entries))
519            return ""
520
521    def _UploadArtifacts(self, artifact_paths):
522        """Process and upload all images and tools to the remote host.
523
524        Args:
525            artifact_paths: An object of ArtifactPaths.
526
527        Returns:
528            An object of RemotePaths.
529        """
530        remote_emulator_dir, remote_image_dir = self._UploadDeviceImages(
531            artifact_paths.emulator_zip, artifact_paths.image_zip)
532
533        remote_kernel_path = None
534        remote_ramdisk_path = None
535
536        if (artifact_paths.boot_image or artifact_paths.system_image or
537                artifact_paths.system_dlkm_image):
538            with tempfile.TemporaryDirectory("host_gf") as temp_dir:
539                ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir)
540
541                image_dir = os.path.join(temp_dir, "images")
542                logger.debug("Unzip %s.", artifact_paths.image_zip)
543                with zipfile.ZipFile(artifact_paths.image_zip,
544                                     "r") as zip_file:
545                    zip_file.extractall(image_dir)
546                image_dir = os.path.join(
547                    image_dir,
548                    self._GetSubdirNameInZip(artifact_paths.image_zip))
549
550                if (artifact_paths.system_image or
551                        artifact_paths.system_dlkm_image):
552                    self._MixAndUploadDiskImage(
553                        remote_image_dir, image_dir,
554                        artifact_paths.system_image,
555                        artifact_paths.system_dlkm_image, ota)
556
557                if artifact_paths.boot_image:
558                    remote_kernel_path, remote_ramdisk_path = (
559                        self._MixAndUploadKernelImages(
560                            image_dir, artifact_paths.boot_image, ota))
561
562        return RemotePaths(remote_image_dir, remote_emulator_dir,
563                           remote_kernel_path, remote_ramdisk_path)
564
565    def _ShouldMixDiskImage(self):
566        """Determines whether a mixed disk image is required.
567
568        This method checks whether the user requires to replace an image that
569        is part of the disk image. Acloud supports replacing system,
570        system_dlkm, and kernel images. system and system_dlkm are installed
571        on the disk.
572
573        Returns:
574            Boolean, whether a mixed disk image is required.
575        """
576        return (self._avd_spec.local_system_image or
577                self._avd_spec.local_system_dlkm_image or
578                (self._avd_spec.system_build_info.get(constants.BUILD_ID) and
579                 self._avd_spec.system_build_info.get(constants.BUILD_TARGET)))
580
581    @utils.TimeExecute(
582        function_description="Processing and uploading tools and images")
583    def _UploadDeviceImages(self, emulator_zip_path, image_zip_path):
584        """Upload artifacts to remote host and extract them.
585
586        Args:
587            emulator_zip_path: The local path to the emulator zip.
588            image_zip_path: The local path to the image zip.
589
590        Returns:
591            The remote paths to the extracted emulator tools and images.
592        """
593        remote_emulator_dir = self._GetInstancePath(_REMOTE_EMULATOR_DIR)
594        remote_image_dir = self._GetInstancePath(_REMOTE_IMAGE_DIR)
595        remote_emulator_zip_path = self._GetInstancePath(
596            _REMOTE_EMULATOR_ZIP_PATH)
597        remote_image_zip_path = self._GetInstancePath(_REMOTE_IMAGE_ZIP_PATH)
598        self._ssh.Run(f"mkdir -p {remote_emulator_dir} {remote_image_dir}")
599        self._ssh.ScpPushFile(emulator_zip_path, remote_emulator_zip_path)
600        self._ssh.ScpPushFile(image_zip_path, remote_image_zip_path)
601
602        self._ssh.Run(f"unzip -d {remote_emulator_dir} "
603                      f"{remote_emulator_zip_path}")
604        self._ssh.Run(f"unzip -d {remote_image_dir} {remote_image_zip_path}")
605        remote_emulator_subdir = remote_path.join(
606            remote_emulator_dir, _SDK_REPO_EMULATOR_DIR_NAME)
607        remote_image_subdir = remote_path.join(
608            remote_image_dir, self._GetSubdirNameInZip(image_zip_path))
609        # TODO(b/141898893): In Android build environment, emulator gets build
610        # information from $ANDROID_PRODUCT_OUT/system/build.prop.
611        # If image_dir is an extacted SDK repository, the file is at
612        # image_dir/build.prop. Acloud copies it to
613        # image_dir/system/build.prop.
614        src_path = remote_path.join(remote_image_subdir, "build.prop")
615        dst_path = remote_path.join(remote_image_subdir, "system",
616                                    "build.prop")
617        self._ssh.Run("'test -f %(dst)s || "
618                      "{ mkdir -p %(dst_dir)s && cp %(src)s %(dst)s ; }'" %
619                      {"src": src_path,
620                       "dst": dst_path,
621                       "dst_dir": remote_path.dirname(dst_path)})
622        return remote_emulator_subdir, remote_image_subdir
623
624    def _MixAndUploadDiskImage(self, remote_image_dir, image_dir,
625                               system_image_path, system_dlkm_image_path, ota):
626        """Mix emulator, system, and system_dlkm images and upload them.
627
628        Args:
629            remote_image_dir: The remote directory where the mixed disk image
630                              is uploaded.
631            image_dir: The directory containing emulator images.
632            system_image_path: The path to the system image.
633            system_dlkm_image_path: The path to the system_dlkm image.
634            ota: An instance of ota_tools.OtaTools.
635
636        Returns:
637            The remote path to the mixed disk image.
638        """
639        with tempfile.TemporaryDirectory("host_gf_disk") as temp_dir:
640            mixed_image = goldfish_utils.MixDiskImage(
641                temp_dir, image_dir, system_image_path, system_dlkm_image_path,
642                ota)
643
644            # TODO(b/142228085): Use -system instead of overwriting the file.
645            remote_disk_image_path = os.path.join(
646                remote_image_dir, goldfish_utils.SYSTEM_QEMU_IMAGE_NAME)
647            self._ssh.ScpPushFile(mixed_image, remote_disk_image_path)
648
649        # Adding the parameter to remote VerifiedBootParams.textproto unlocks
650        # the device so that the disabled vbmeta takes effect. An alternative
651        # is to append the parameter to the kernel command line by
652        # `emulator -qemu -append`, but that does not pass the compliance test.
653        remote_params_path = remote_path.join(
654            remote_image_dir, goldfish_utils.VERIFIED_BOOT_PARAMS_FILE_NAME)
655        # \\n is interpreted by shell and echo. \" is interpreted by shell.
656        param = r'\\nparam: \"androidboot.verifiedbootstate=orange\"'
657        self._ssh.Run(f"'test -f {remote_params_path} && "
658                      f"echo -e {param} >> {remote_params_path}'")
659
660        return remote_disk_image_path
661
662    def _MixAndUploadKernelImages(self, image_dir, boot_image_path, ota):
663        """Mix emulator kernel images with a boot image and upload them.
664
665        Args:
666            image_dir: The directory containing emulator images.
667            boot_image_path: The path to the boot image.
668            ota: An instance of ota_tools.OtaTools.
669
670        Returns:
671            The remote paths to the kernel image and the ramdisk image.
672        """
673        remote_kernel_path = self._GetInstancePath(_REMOTE_KERNEL_PATH)
674        remote_ramdisk_path = self._GetInstancePath(_REMOTE_RAMDISK_PATH)
675        with tempfile.TemporaryDirectory("host_gf_kernel") as temp_dir:
676            kernel_path, ramdisk_path = goldfish_utils.MixWithBootImage(
677                temp_dir, image_dir, boot_image_path, ota)
678
679            self._ssh.ScpPushFile(kernel_path, remote_kernel_path)
680            self._ssh.ScpPushFile(ramdisk_path, remote_ramdisk_path)
681
682        return remote_kernel_path, remote_ramdisk_path
683
684    def _GetEmulatorLogs(self):
685        """Return the logs created by the remote emulator command."""
686        return [report.LogFile(self._GetInstancePath(_REMOTE_STDOUT_PATH),
687                               constants.LOG_TYPE_KERNEL_LOG),
688                report.LogFile(self._GetInstancePath(_REMOTE_STDERR_PATH),
689                               constants.LOG_TYPE_TEXT),
690                report.LogFile(self._GetInstancePath(_REMOTE_LOGCAT_PATH),
691                               constants.LOG_TYPE_LOGCAT)]
692
693    @utils.TimeExecute(function_description="Start emulator")
694    def _StartEmulator(self, remote_paths):
695        """Start emulator command as a remote background process.
696
697        Args:
698            remote_emulator_dir: The emulator tool directory on remote host.
699            remote_image_dir: The image directory on remote host.
700        """
701        remote_emulator_bin_path = remote_path.join(
702            remote_paths.emulator_dir, _EMULATOR_BIN_NAME)
703        remote_bin_paths = [
704            remote_path.join(remote_paths.emulator_dir, name) for
705            name in _EMULATOR_BIN_DIR_NAMES]
706        remote_bin_paths.append(remote_emulator_bin_path)
707        self._ssh.Run("chmod -R +x %s" % " ".join(remote_bin_paths))
708
709        remote_runtime_dir = self._GetInstancePath(_REMOTE_RUNTIME_DIR)
710        self._ssh.Run(f"mkdir -p {remote_runtime_dir}")
711        env = {constants.ENV_ANDROID_PRODUCT_OUT: remote_paths.image_dir,
712               constants.ENV_ANDROID_TMP: remote_runtime_dir,
713               constants.ENV_ANDROID_BUILD_TOP: remote_runtime_dir}
714        cmd = ["nohup", remote_emulator_bin_path, "-verbose", "-show-kernel",
715               "-read-only", "-ports",
716               str(self._GetConsolePort()) + "," + str(self.GetAdbPorts()[0]),
717               "-no-window",
718               "-logcat-output", self._GetInstancePath(_REMOTE_LOGCAT_PATH)]
719
720        if remote_paths.kernel:
721            cmd.extend(("-kernel", remote_paths.kernel))
722
723        if remote_paths.ramdisk:
724            cmd.extend(("-ramdisk", remote_paths.ramdisk))
725
726        cmd.extend(goldfish_utils.ConvertAvdSpecToArgs(self._avd_spec))
727
728        # Emulator does not support -stdouterr-file on macOS.
729        self._ssh.Run(
730            "'export {env} ; {cmd} 1> {stdout} 2> {stderr} &'".format(
731                env=" ".join(k + "=~/" + v for k, v in env.items()),
732                cmd=" ".join(cmd),
733                stdout=self._GetInstancePath(_REMOTE_STDOUT_PATH),
734                stderr=self._GetInstancePath(_REMOTE_STDERR_PATH)))
735
736    @utils.TimeExecute(function_description="Wait for emulator")
737    def _WaitForEmulator(self):
738        """Wait for remote emulator console to be active.
739
740        Raises:
741            errors.DeviceBootError if connection fails.
742            errors.DeviceBootTimeoutError if boot times out.
743        """
744        ip_addr = self._avd_spec.remote_host
745        console_port = self._GetConsolePort()
746        poll_timeout_secs = (self._avd_spec.boot_timeout_secs or
747                             _DEFAULT_BOOT_TIMEOUT_SECS)
748        try:
749            with emulator_console.RemoteEmulatorConsole(
750                    ip_addr,
751                    console_port,
752                    self._ssh_user,
753                    self._ssh_private_key_path,
754                    self._ssh_extra_args) as console:
755                utils.PollAndWait(
756                    func=lambda: (True if console.Ping() else
757                                  console.Reconnect()),
758                    expected_return=True,
759                    timeout_exception=errors.DeviceBootTimeoutError,
760                    timeout_secs=poll_timeout_secs,
761                    sleep_interval_secs=5)
762        except errors.DeviceConnectionError as e:
763            raise errors.DeviceBootError("Fail to connect to %s:%d." %
764                                         (ip_addr, console_port)) from e
765
766    def GetBuildInfoDict(self):
767        """Get build info dictionary.
768
769        Returns:
770            A build info dictionary.
771        """
772        build_info_dict = {key: val for key, val in
773                           self._avd_spec.remote_image.items() if val}
774        return build_info_dict
775
776    def GetAdbPorts(self):
777        """Get ADB ports of the created devices.
778
779        This class does not support --num-avds-per-instance.
780
781        Returns:
782            The port numbers as a list of integers.
783        """
784        return [self._GetConsolePort() + 1]
785
786    def GetFailures(self):
787        """Get Failures from all devices.
788
789        Returns:
790            A dictionary the contains all the failures.
791            The key is the name of the instance that fails to boot,
792            and the value is an errors.DeviceBootError object.
793        """
794        return self._failures
795
796    def GetLogs(self):
797        """Get log files of created instances.
798
799        Returns:
800            A dictionary that maps instance names to lists of report.LogFile.
801        """
802        return self._logs
803