• 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,
206                subprocess.TimeoutExpired) as e:
207            # Catch the generic runtime error and CalledProcessError which is
208            # raised by the ssh module.
209            self._failures[instance_name] = e
210        finally:
211            client.RecordTime(timed_stage, start_time)
212
213        return instance_name
214
215    def _InitRemoteHost(self):
216        """Remove the existing instance and the instance directory."""
217        # Disable authentication for emulator console.
218        self._ssh.Run("""'echo -n "" > .emulator_console_auth_token'""")
219        try:
220            with emulator_console.RemoteEmulatorConsole(
221                    self._avd_spec.remote_host,
222                    self._GetConsolePort(),
223                    self._ssh_user,
224                    self._ssh_private_key_path,
225                    self._ssh_extra_args) as console:
226                console.Kill()
227            logger.info("Killed existing emulator.")
228        except errors.DeviceConnectionError as e:
229            logger.info("Did not kill existing emulator: %s", str(e))
230        # Delete instance files.
231        self._ssh.Run(f"rm -rf {self._GetInstancePath('')}")
232
233    def _PrepareArtifacts(self):
234        """Prepare artifacts on remote host.
235
236        This method retrieves artifacts from cache or Android Build API and
237        uploads them to the remote host.
238
239        Returns:
240            An object of RemotePaths.
241        """
242        try:
243            artifact_paths = self._RetrieveArtifacts()
244            return self._UploadArtifacts(artifact_paths)
245        finally:
246            if self._temp_artifact_dir:
247                shutil.rmtree(self._temp_artifact_dir, ignore_errors=True)
248                self._temp_artifact_dir = None
249
250    @staticmethod
251    def _InferEmulatorZipName(build_target, build_id):
252        """Determine the emulator zip name in build artifacts.
253
254        The emulator zip name is composed of build variables that are not
255        revealed in the artifacts. This method infers the emulator zip name
256        from its build target name.
257
258        Args:
259            build_target: The emulator build target name, e.g.,
260                          "emulator-linux_x64_nolocationui", "aarch64_sdk_tools_mac".
261            build_id: A string, the emulator build ID.
262
263        Returns:
264            The name of the emulator zip. e.g.,
265            "sdk-repo-linux-emulator-123456.zip",
266            "sdk-repo-darwin_aarch64-emulator-123456.zip".
267        """
268        split_target = [x for product_variant in build_target.split("-")
269                        for x in product_variant.split("_")]
270        if "darwin" in split_target or "mac" in split_target:
271            os_name = "darwin"
272        else:
273            os_name = "linux"
274        if "aarch64" in split_target:
275            os_name = os_name + "_aarch64"
276        return _EMULATOR_ZIP_NAME_FORMAT % {"os": os_name,
277                                            "build_id": build_id}
278
279    def _RetrieveArtifact(self, build_target, build_id,
280                          resource_id):
281        """Retrieve an artifact from cache or Android Build API.
282
283        Args:
284            build_target: A string, the build target of the artifact. e.g.,
285                          "sdk_phone_x86_64-userdebug".
286            build_id: A string, the build ID of the artifact.
287            resource_id: A string, the name of the artifact. e.g.,
288                         "sdk-repo-linux-system-images-123456.zip".
289
290        Returns:
291            The path to the artifact in download_dir.
292        """
293        local_path = os.path.join(self._download_dir, build_id, build_target,
294                                  resource_id)
295        if os.path.isfile(local_path):
296            logger.info("Skip downloading existing artifact: %s", local_path)
297            return local_path
298
299        complete = False
300        try:
301            os.makedirs(os.path.dirname(local_path), exist_ok=True)
302            self._build_api.DownloadArtifact(
303                build_target, build_id, resource_id, local_path,
304                self._build_api.LATEST)
305            complete = True
306        finally:
307            if not complete and os.path.isfile(local_path):
308                os.remove(local_path)
309        return local_path
310
311    @utils.TimeExecute(function_description="Download Android Build artifacts")
312    def _RetrieveArtifacts(self):
313        """Retrieve goldfish images and tools from cache or Android Build API.
314
315        Returns:
316            An object of ArtifactPaths.
317
318        Raises:
319            errors.GetRemoteImageError: Fails to download rom images.
320            errors.GetLocalImageError: Fails to validate local image zip.
321            errors.GetSdkRepoPackageError: Fails to retrieve emulator zip.
322        """
323        # Device images.
324        if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
325            image_zip_path = self._RetrieveDeviceImageZip()
326        elif self._avd_spec.image_source == constants.IMAGE_SRC_LOCAL:
327            image_zip_path = self._avd_spec.local_image_artifact
328            if not image_zip_path or not zipfile.is_zipfile(image_zip_path):
329                raise errors.GetLocalImageError(
330                    f"{image_zip_path or self._avd_spec.local_image_dir} is "
331                    "not an SDK repository zip.")
332        else:
333            raise errors.CreateError(
334                f"Unknown image source: {self._avd_spec.image_source}")
335
336        # Emulator tools.
337        emu_zip_path = (self._avd_spec.emulator_zip or
338                        self._RetrieveEmulatorZip())
339        if not emu_zip_path:
340            raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG)
341
342        # System image.
343        if self._avd_spec.local_system_image:
344            # No known use case requires replacing system_ext and product.
345            system_image_path = create_common.FindSystemImages(
346                self._avd_spec.local_system_image).system
347        else:
348            system_image_path = self._RetrieveSystemImage()
349
350        # system_dlkm image.
351        if self._avd_spec.local_system_dlkm_image:
352            system_dlkm_image_path = goldfish_utils.FindSystemDlkmImage(
353                self._avd_spec.local_system_dlkm_image)
354        else:
355            # No known use case requires remote system_dlkm.
356            system_dlkm_image_path = None
357
358        # Boot image.
359        if self._avd_spec.local_kernel_image:
360            boot_image_path = create_common.FindBootImage(
361                self._avd_spec.local_kernel_image)
362        else:
363            boot_image_path = self._RetrieveBootImage()
364
365        # OTA tools.
366        ota_tools_dir = None
367        if system_image_path or system_dlkm_image_path or boot_image_path:
368            if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
369                ota_tools_dir = self._RetrieveOtaTools()
370            else:
371                ota_tools_dir = ota_tools.FindOtaToolsDir(
372                    self._avd_spec.local_tool_dirs +
373                    create_common.GetNonEmptyEnvVars(
374                        constants.ENV_ANDROID_SOONG_HOST_OUT,
375                        constants.ENV_ANDROID_HOST_OUT))
376
377        return ArtifactPaths(image_zip_path, emu_zip_path, ota_tools_dir,
378                             system_image_path, system_dlkm_image_path,
379                             boot_image_path)
380
381    def _RetrieveDeviceImageZip(self):
382        """Retrieve device image zip from cache or Android Build API.
383
384        Returns:
385            The path to the device image zip in download_dir.
386        """
387        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
388        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
389        image_zip_name_format = (_EXTRA_IMAGE_ZIP_NAME_FORMAT if
390                                 self._ShouldMixDiskImage() else
391                                 _SDK_REPO_IMAGE_ZIP_NAME_FORMAT)
392        return self._RetrieveArtifact(
393            build_target, build_id,
394            image_zip_name_format % {"build_id": build_id})
395
396    def _RetrieveEmulatorBuildID(self):
397        """Retrieve required emulator build from a goldfish image build.
398
399        Returns:
400            A string, the emulator build ID.
401            None if the build info is empty.
402        """
403        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
404        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
405        if build_id and build_target:
406            emu_info_path = self._RetrieveArtifact(build_target, build_id,
407                                                   _EMULATOR_INFO_NAME)
408            with open(emu_info_path, "r", encoding="utf-8") as emu_info:
409                for line in emu_info:
410                    match = _EMULATOR_VERSION_PATTERN.fullmatch(line.strip())
411                    if match:
412                        logger.info("Found emulator build ID: %s", line)
413                        return match.group("build_id")
414        return None
415
416    def _RetrieveEmulatorZip(self):
417        """Retrieve emulator zip from cache or Android Build API.
418
419        Returns:
420            The path to the emulator zip in download_dir.
421            None if this method cannot determine the emulator build ID.
422        """
423        emu_build_id = (self._avd_spec.emulator_build_id or
424                        self._RetrieveEmulatorBuildID())
425        if not emu_build_id:
426            return None
427        emu_build_target = (self._avd_spec.emulator_build_target or
428                            self._avd_spec.cfg.emulator_build_target)
429        emu_zip_name = self._InferEmulatorZipName(emu_build_target,
430                                                  emu_build_id)
431        return self._RetrieveArtifact(emu_build_target, emu_build_id,
432                                      emu_zip_name)
433
434    def _RetrieveSystemImage(self):
435        """Retrieve and unzip system image if system build info is not empty.
436
437        Returns:
438            The path to the temporary system image.
439            None if the system build info is empty.
440        """
441        build_id = self._avd_spec.system_build_info.get(constants.BUILD_ID)
442        build_target = self._avd_spec.system_build_info.get(
443            constants.BUILD_TARGET)
444        if not build_id or not build_target:
445            return None
446        image_zip_name = _IMAGE_ZIP_NAME_FORMAT % {
447            "build_target": build_target.split("-", 1)[0],
448            "build_id": build_id}
449        image_zip_path = self._RetrieveArtifact(build_target, build_id,
450                                                image_zip_name)
451        logger.debug("Unzip %s from %s to %s.",
452                     _SYSTEM_IMAGE_NAME, image_zip_path, self._artifact_dir)
453        with zipfile.ZipFile(image_zip_path, "r") as zip_file:
454            zip_file.extract(_SYSTEM_IMAGE_NAME, self._artifact_dir)
455        return os.path.join(self._artifact_dir, _SYSTEM_IMAGE_NAME)
456
457    def _RetrieveBootImage(self):
458        """Retrieve boot image if boot build info is not empty.
459
460        Returns:
461            The path to the boot image in download_dir.
462            None if the boot build info is empty.
463        """
464        build_id = self._avd_spec.boot_build_info.get(constants.BUILD_ID)
465        build_target = self._avd_spec.boot_build_info.get(
466            constants.BUILD_TARGET)
467        image_name = self._avd_spec.boot_build_info.get(
468            constants.BUILD_ARTIFACT)
469        if build_id and build_target and image_name:
470            return self._RetrieveArtifact(build_target, build_id, image_name)
471        return None
472
473    def _RetrieveOtaTools(self):
474        """Retrieve and unzip OTA tools.
475
476        This method retrieves OTA tools from the goldfish build which contains
477        mk_combined_img.
478
479        Returns:
480            The path to the temporary OTA tools directory.
481        """
482        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
483        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
484        zip_path = self._RetrieveArtifact(build_target, build_id,
485                                          _OTA_TOOLS_ZIP_NAME)
486        ota_tools_dir = os.path.join(self._artifact_dir, _OTA_TOOLS_DIR_NAME)
487        logger.debug("Unzip %s to %s.", zip_path, ota_tools_dir)
488        os.mkdir(ota_tools_dir)
489        with zipfile.ZipFile(zip_path, "r") as zip_file:
490            zip_file.extractall(ota_tools_dir)
491        return ota_tools_dir
492
493    @staticmethod
494    def _GetSubdirNameInZip(zip_path):
495        """Get the name of the only subdirectory in a zip.
496
497        In an SDK repository zip, the images and the binaries are located in a
498        subdirectory. This class needs to find out the subdirectory name in
499        order to construct the remote commands.
500
501        For example, in a sdk-repo-linux-system-images-*.zip for arm64, all
502        files are in "arm64-v8a/". The zip entries are:
503
504        arm64-v8a/NOTICE.txt
505        arm64-v8a/system.img
506        arm64-v8a/data/local.prop
507        ...
508
509        This method scans the entries and returns the common subdirectory name.
510        """
511        sep = "/"
512        with zipfile.ZipFile(zip_path, 'r') as zip_obj:
513            entries = zip_obj.namelist()
514            if len(entries) > 0 and sep in entries[0]:
515                subdir = entries[0].split(sep, 1)[0]
516                if all(e.startswith(subdir + sep) for e in entries):
517                    return subdir
518            logger.warning("Expect one subdirectory in %s. Actual entries: %s",
519                           zip_path, " ".join(entries))
520            return ""
521
522    def _UploadArtifacts(self, artifact_paths):
523        """Process and upload all images and tools to the remote host.
524
525        Args:
526            artifact_paths: An object of ArtifactPaths.
527
528        Returns:
529            An object of RemotePaths.
530        """
531        remote_emulator_dir, remote_image_dir = self._UploadDeviceImages(
532            artifact_paths.emulator_zip, artifact_paths.image_zip)
533
534        remote_kernel_path = None
535        remote_ramdisk_path = None
536
537        if (artifact_paths.boot_image or artifact_paths.system_image or
538                artifact_paths.system_dlkm_image):
539            with tempfile.TemporaryDirectory("host_gf") as temp_dir:
540                ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir)
541
542                image_dir = os.path.join(temp_dir, "images")
543                logger.debug("Unzip %s.", artifact_paths.image_zip)
544                with zipfile.ZipFile(artifact_paths.image_zip,
545                                     "r") as zip_file:
546                    zip_file.extractall(image_dir)
547                image_dir = os.path.join(
548                    image_dir,
549                    self._GetSubdirNameInZip(artifact_paths.image_zip))
550
551                if (artifact_paths.system_image or
552                        artifact_paths.system_dlkm_image):
553                    self._MixAndUploadDiskImage(
554                        remote_image_dir, image_dir,
555                        artifact_paths.system_image,
556                        artifact_paths.system_dlkm_image, ota)
557
558                if artifact_paths.boot_image:
559                    remote_kernel_path, remote_ramdisk_path = (
560                        self._MixAndUploadKernelImages(
561                            image_dir, artifact_paths.boot_image,
562                            artifact_paths.system_dlkm_image, ota))
563
564        return RemotePaths(remote_image_dir, remote_emulator_dir,
565                           remote_kernel_path, remote_ramdisk_path)
566
567    def _ShouldMixDiskImage(self):
568        """Determines whether a mixed disk image is required.
569
570        This method checks whether the user requires to replace an image that
571        is part of the disk image. Acloud supports replacing system,
572        system_dlkm, and kernel images. system and system_dlkm are installed
573        on the disk.
574
575        Returns:
576            Boolean, whether a mixed disk image is required.
577        """
578        return (self._avd_spec.local_system_image or
579                self._avd_spec.local_system_dlkm_image or
580                (self._avd_spec.system_build_info.get(constants.BUILD_ID) and
581                 self._avd_spec.system_build_info.get(constants.BUILD_TARGET)))
582
583    @utils.TimeExecute(
584        function_description="Processing and uploading tools and images")
585    def _UploadDeviceImages(self, emulator_zip_path, image_zip_path):
586        """Upload artifacts to remote host and extract them.
587
588        Args:
589            emulator_zip_path: The local path to the emulator zip.
590            image_zip_path: The local path to the image zip.
591
592        Returns:
593            The remote paths to the extracted emulator tools and images.
594        """
595        remote_emulator_dir = self._GetInstancePath(_REMOTE_EMULATOR_DIR)
596        remote_image_dir = self._GetInstancePath(_REMOTE_IMAGE_DIR)
597        remote_emulator_zip_path = self._GetInstancePath(
598            _REMOTE_EMULATOR_ZIP_PATH)
599        remote_image_zip_path = self._GetInstancePath(_REMOTE_IMAGE_ZIP_PATH)
600        self._ssh.Run(f"mkdir -p {remote_emulator_dir} {remote_image_dir}")
601        self._ssh.ScpPushFile(emulator_zip_path, remote_emulator_zip_path)
602        self._ssh.ScpPushFile(image_zip_path, remote_image_zip_path)
603
604        self._ssh.Run(f"unzip -d {remote_emulator_dir} "
605                      f"{remote_emulator_zip_path}")
606        self._ssh.Run(f"unzip -d {remote_image_dir} {remote_image_zip_path}")
607        remote_emulator_subdir = remote_path.join(
608            remote_emulator_dir, _SDK_REPO_EMULATOR_DIR_NAME)
609        remote_image_subdir = remote_path.join(
610            remote_image_dir, self._GetSubdirNameInZip(image_zip_path))
611        # TODO(b/141898893): In Android build environment, emulator gets build
612        # information from $ANDROID_PRODUCT_OUT/system/build.prop.
613        # If image_dir is an extacted SDK repository, the file is at
614        # image_dir/build.prop. Acloud copies it to
615        # image_dir/system/build.prop.
616        src_path = remote_path.join(remote_image_subdir, "build.prop")
617        dst_path = remote_path.join(remote_image_subdir, "system",
618                                    "build.prop")
619        self._ssh.Run("'test -f %(dst)s || "
620                      "{ mkdir -p %(dst_dir)s && cp %(src)s %(dst)s ; }'" %
621                      {"src": src_path,
622                       "dst": dst_path,
623                       "dst_dir": remote_path.dirname(dst_path)})
624        return remote_emulator_subdir, remote_image_subdir
625
626    def _MixAndUploadDiskImage(self, remote_image_dir, image_dir,
627                               system_image_path, system_dlkm_image_path, ota):
628        """Mix emulator, system, and system_dlkm images and upload them.
629
630        Args:
631            remote_image_dir: The remote directory where the mixed disk image
632                              is uploaded.
633            image_dir: The directory containing emulator images.
634            system_image_path: The path to the system image.
635            system_dlkm_image_path: The path to the system_dlkm image.
636            ota: An instance of ota_tools.OtaTools.
637
638        Returns:
639            The remote path to the mixed disk image.
640        """
641        with tempfile.TemporaryDirectory("host_gf_disk") as temp_dir:
642            mixed_image = goldfish_utils.MixDiskImage(
643                temp_dir, image_dir, system_image_path, system_dlkm_image_path,
644                ota)
645
646            # TODO(b/142228085): Use -system instead of overwriting the file.
647            remote_disk_image_path = os.path.join(
648                remote_image_dir, goldfish_utils.SYSTEM_QEMU_IMAGE_NAME)
649            self._ssh.ScpPushFile(mixed_image, remote_disk_image_path)
650
651        # Adding the parameter to remote VerifiedBootParams.textproto unlocks
652        # the device so that the disabled vbmeta takes effect. An alternative
653        # is to append the parameter to the kernel command line by
654        # `emulator -qemu -append`, but that does not pass the compliance test.
655        remote_params_path = remote_path.join(
656            remote_image_dir, goldfish_utils.VERIFIED_BOOT_PARAMS_FILE_NAME)
657        # \\n is interpreted by shell and echo. \" is interpreted by shell.
658        param = r'\\nparam: \"androidboot.verifiedbootstate=orange\"'
659        self._ssh.Run(f"'test -f {remote_params_path} && "
660                      f"echo -e {param} >> {remote_params_path}'")
661
662        return remote_disk_image_path
663
664    def _MixAndUploadKernelImages(self, image_dir, boot_image_path,
665                                  system_dlkm_image_path, ota):
666        """Mix emulator kernel images with a boot image and upload them.
667
668        Args:
669            image_dir: The directory containing emulator images.
670            boot_image_path: The path to the boot image.
671            system_dlkm_image_path: The path to the system_dlkm image.
672                                    Can be None.
673            ota: An instance of ota_tools.OtaTools.
674
675        Returns:
676            The remote paths to the kernel image and the ramdisk image.
677        """
678        remote_kernel_path = self._GetInstancePath(_REMOTE_KERNEL_PATH)
679        remote_ramdisk_path = self._GetInstancePath(_REMOTE_RAMDISK_PATH)
680        with tempfile.TemporaryDirectory("host_gf_kernel") as temp_dir:
681            kernel_path, ramdisk_path = goldfish_utils.MixWithBootImage(
682                temp_dir, image_dir, boot_image_path,
683                (system_dlkm_image_path if
684                 self._avd_spec.mix_system_dlkm_into_vendor_ramdisk else None),
685                ota)
686
687            self._ssh.ScpPushFile(kernel_path, remote_kernel_path)
688            self._ssh.ScpPushFile(ramdisk_path, remote_ramdisk_path)
689
690        return remote_kernel_path, remote_ramdisk_path
691
692    def _GetEmulatorLogs(self):
693        """Return the logs created by the remote emulator command."""
694        return [report.LogFile(self._GetInstancePath(_REMOTE_STDOUT_PATH),
695                               constants.LOG_TYPE_KERNEL_LOG),
696                report.LogFile(self._GetInstancePath(_REMOTE_STDERR_PATH),
697                               constants.LOG_TYPE_TEXT),
698                report.LogFile(self._GetInstancePath(_REMOTE_LOGCAT_PATH),
699                               constants.LOG_TYPE_LOGCAT)]
700
701    @utils.TimeExecute(function_description="Start emulator")
702    def _StartEmulator(self, remote_paths):
703        """Start emulator command as a remote background process.
704
705        Args:
706            remote_emulator_dir: The emulator tool directory on remote host.
707            remote_image_dir: The image directory on remote host.
708        """
709        remote_emulator_bin_path = remote_path.join(
710            remote_paths.emulator_dir, _EMULATOR_BIN_NAME)
711        remote_bin_paths = [
712            remote_path.join(remote_paths.emulator_dir, name) for
713            name in _EMULATOR_BIN_DIR_NAMES]
714        remote_bin_paths.append(remote_emulator_bin_path)
715        self._ssh.Run("chmod -R +x %s" % " ".join(remote_bin_paths))
716
717        remote_runtime_dir = self._GetInstancePath(_REMOTE_RUNTIME_DIR)
718        self._ssh.Run(f"mkdir -p {remote_runtime_dir}")
719        env = {constants.ENV_ANDROID_PRODUCT_OUT: remote_paths.image_dir,
720               constants.ENV_ANDROID_TMP: remote_runtime_dir,
721               constants.ENV_ANDROID_BUILD_TOP: remote_runtime_dir}
722        cmd = ["nohup", remote_emulator_bin_path, "-verbose", "-show-kernel",
723               "-read-only", "-ports",
724               str(self._GetConsolePort()) + "," + str(self.GetAdbPorts()[0]),
725               "-no-window",
726               "-logcat-output", self._GetInstancePath(_REMOTE_LOGCAT_PATH)]
727
728        if remote_paths.kernel:
729            cmd.extend(("-kernel", remote_paths.kernel))
730
731        if remote_paths.ramdisk:
732            cmd.extend(("-ramdisk", remote_paths.ramdisk))
733
734        cmd.extend(goldfish_utils.ConvertAvdSpecToArgs(self._avd_spec))
735
736        # Emulator does not support -stdouterr-file on macOS.
737        self._ssh.Run(
738            "'export {env} ; {cmd} 1> {stdout} 2> {stderr} &'".format(
739                env=" ".join(k + "=~/" + v for k, v in env.items()),
740                cmd=" ".join(cmd),
741                stdout=self._GetInstancePath(_REMOTE_STDOUT_PATH),
742                stderr=self._GetInstancePath(_REMOTE_STDERR_PATH)))
743
744    @utils.TimeExecute(function_description="Wait for emulator")
745    def _WaitForEmulator(self):
746        """Wait for remote emulator console to be active.
747
748        Raises:
749            errors.DeviceBootError if connection fails.
750            errors.DeviceBootTimeoutError if boot times out.
751        """
752        ip_addr = self._avd_spec.remote_host
753        console_port = self._GetConsolePort()
754        poll_timeout_secs = (self._avd_spec.boot_timeout_secs or
755                             _DEFAULT_BOOT_TIMEOUT_SECS)
756        try:
757            with emulator_console.RemoteEmulatorConsole(
758                    ip_addr,
759                    console_port,
760                    self._ssh_user,
761                    self._ssh_private_key_path,
762                    self._ssh_extra_args) as console:
763                utils.PollAndWait(
764                    func=lambda: (True if console.Ping() else
765                                  console.Reconnect()),
766                    expected_return=True,
767                    timeout_exception=errors.DeviceBootTimeoutError,
768                    timeout_secs=poll_timeout_secs,
769                    sleep_interval_secs=5)
770        except errors.DeviceConnectionError as e:
771            raise errors.DeviceBootError("Fail to connect to %s:%d." %
772                                         (ip_addr, console_port)) from e
773
774    def GetBuildInfoDict(self):
775        """Get build info dictionary.
776
777        Returns:
778            A build info dictionary.
779        """
780        build_info_dict = {key: val for key, val in
781                           self._avd_spec.remote_image.items() if val}
782        return build_info_dict
783
784    def GetAdbPorts(self):
785        """Get ADB ports of the created devices.
786
787        This class does not support --num-avds-per-instance.
788
789        Returns:
790            The port numbers as a list of integers.
791        """
792        return [self._GetConsolePort() + 1]
793
794    def GetFailures(self):
795        """Get Failures from all devices.
796
797        Returns:
798            A dictionary the contains all the failures.
799            The key is the name of the instance that fails to boot,
800            and the value is an errors.DeviceBootError object.
801        """
802        return self._failures
803
804    def GetLogs(self):
805        """Get log files of created instances.
806
807        Returns:
808            A dictionary that maps instance names to lists of report.LogFile.
809        """
810        return self._logs
811