• 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", "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            system_image_path = create_common.FindSystemImage(
344                self._avd_spec.local_system_image)
345        else:
346            system_image_path = self._RetrieveSystemImage()
347
348        # Boot image.
349        if self._avd_spec.local_kernel_image:
350            boot_image_path = create_common.FindBootImage(
351                self._avd_spec.local_kernel_image)
352        else:
353            boot_image_path = self._RetrieveBootImage()
354
355        # OTA tools.
356        ota_tools_dir = None
357        if system_image_path or boot_image_path:
358            if self._avd_spec.image_source == constants.IMAGE_SRC_REMOTE:
359                ota_tools_dir = self._RetrieveOtaTools()
360            else:
361                ota_tools_dir = ota_tools.FindOtaToolsDir(
362                    self._avd_spec.local_tool_dirs +
363                    create_common.GetNonEmptyEnvVars(
364                        constants.ENV_ANDROID_SOONG_HOST_OUT,
365                        constants.ENV_ANDROID_HOST_OUT))
366
367        return ArtifactPaths(image_zip_path, emu_zip_path, ota_tools_dir,
368                             system_image_path, boot_image_path)
369
370    def _RetrieveDeviceImageZip(self):
371        """Retrieve device image zip from cache or Android Build API.
372
373        Returns:
374            The path to the device image zip in download_dir.
375        """
376        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
377        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
378        image_zip_name_format = (_EXTRA_IMAGE_ZIP_NAME_FORMAT if
379                                 self._ShouldMixDiskImage() else
380                                 _SDK_REPO_IMAGE_ZIP_NAME_FORMAT)
381        return self._RetrieveArtifact(
382            build_target, build_id,
383            image_zip_name_format % {"build_id": build_id})
384
385    def _RetrieveEmulatorBuildID(self):
386        """Retrieve required emulator build from a goldfish image build.
387
388        Returns:
389            A string, the emulator build ID.
390            None if the build info is empty.
391        """
392        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
393        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
394        if build_id and build_target:
395            emu_info_path = self._RetrieveArtifact(build_target, build_id,
396                                                   _EMULATOR_INFO_NAME)
397            with open(emu_info_path, "r", encoding="utf-8") as emu_info:
398                for line in emu_info:
399                    match = _EMULATOR_VERSION_PATTERN.fullmatch(line.strip())
400                    if match:
401                        logger.info("Found emulator build ID: %s", line)
402                        return match.group("build_id")
403        return None
404
405    def _RetrieveEmulatorZip(self):
406        """Retrieve emulator zip from cache or Android Build API.
407
408        Returns:
409            The path to the emulator zip in download_dir.
410            None if this method cannot determine the emulator build ID.
411        """
412        emu_build_id = (self._avd_spec.emulator_build_id or
413                        self._RetrieveEmulatorBuildID())
414        if not emu_build_id:
415            return None
416        emu_build_target = (self._avd_spec.emulator_build_target or
417                            self._avd_spec.cfg.emulator_build_target)
418        emu_zip_name = self._InferEmulatorZipName(emu_build_target,
419                                                  emu_build_id)
420        return self._RetrieveArtifact(emu_build_target, emu_build_id,
421                                      emu_zip_name)
422
423    def _RetrieveSystemImage(self):
424        """Retrieve and unzip system image if system build info is not empty.
425
426        Returns:
427            The path to the temporary system image.
428            None if the system build info is empty.
429        """
430        build_id = self._avd_spec.system_build_info.get(constants.BUILD_ID)
431        build_target = self._avd_spec.system_build_info.get(
432            constants.BUILD_TARGET)
433        if not build_id or not build_target:
434            return None
435        image_zip_name = _IMAGE_ZIP_NAME_FORMAT % {
436            "build_target": build_target.split("-", 1)[0],
437            "build_id": build_id}
438        image_zip_path = self._RetrieveArtifact(build_target, build_id,
439                                                image_zip_name)
440        logger.debug("Unzip %s from %s to %s.",
441                     _SYSTEM_IMAGE_NAME, image_zip_path, self._artifact_dir)
442        with zipfile.ZipFile(image_zip_path, "r") as zip_file:
443            zip_file.extract(_SYSTEM_IMAGE_NAME, self._artifact_dir)
444        return os.path.join(self._artifact_dir, _SYSTEM_IMAGE_NAME)
445
446    def _RetrieveBootImage(self):
447        """Retrieve boot image if boot build info is not empty.
448
449        Returns:
450            The path to the boot image in download_dir.
451            None if the boot build info is empty.
452        """
453        build_id = self._avd_spec.boot_build_info.get(constants.BUILD_ID)
454        build_target = self._avd_spec.boot_build_info.get(
455            constants.BUILD_TARGET)
456        image_name = self._avd_spec.boot_build_info.get(
457            constants.BUILD_ARTIFACT)
458        if build_id and build_target and image_name:
459            return self._RetrieveArtifact(build_target, build_id, image_name)
460        return None
461
462    def _RetrieveOtaTools(self):
463        """Retrieve and unzip OTA tools.
464
465        This method retrieves OTA tools from the goldfish build which contains
466        mk_combined_img.
467
468        Returns:
469            The path to the temporary OTA tools directory.
470        """
471        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
472        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
473        zip_path = self._RetrieveArtifact(build_target, build_id,
474                                          _OTA_TOOLS_ZIP_NAME)
475        ota_tools_dir = os.path.join(self._artifact_dir, _OTA_TOOLS_DIR_NAME)
476        logger.debug("Unzip %s to %s.", zip_path, ota_tools_dir)
477        os.mkdir(ota_tools_dir)
478        with zipfile.ZipFile(zip_path, "r") as zip_file:
479            zip_file.extractall(ota_tools_dir)
480        return ota_tools_dir
481
482    @staticmethod
483    def _GetSubdirNameInZip(zip_path):
484        """Get the name of the only subdirectory in a zip.
485
486        In an SDK repository zip, the images and the binaries are located in a
487        subdirectory. This class needs to find out the subdirectory name in
488        order to construct the remote commands.
489
490        For example, in a sdk-repo-linux-system-images-*.zip for arm64, all
491        files are in "arm64-v8a/". The zip entries are:
492
493        arm64-v8a/NOTICE.txt
494        arm64-v8a/system.img
495        arm64-v8a/data/local.prop
496        ...
497
498        This method scans the entries and returns the common subdirectory name.
499        """
500        sep = "/"
501        with zipfile.ZipFile(zip_path, 'r') as zip_obj:
502            entries = zip_obj.namelist()
503            if len(entries) > 0 and sep in entries[0]:
504                subdir = entries[0].split(sep, 1)[0]
505                if all(e.startswith(subdir + sep) for e in entries):
506                    return subdir
507            logger.warning("Expect one subdirectory in %s. Actual entries: %s",
508                           zip_path, " ".join(entries))
509            return ""
510
511    def _UploadArtifacts(self, artifact_paths):
512        """Process and upload all images and tools to the remote host.
513
514        Args:
515            artifact_paths: An object of ArtifactPaths.
516
517        Returns:
518            An object of RemotePaths.
519        """
520        remote_emulator_dir, remote_image_dir = self._UploadDeviceImages(
521            artifact_paths.emulator_zip, artifact_paths.image_zip)
522
523        remote_kernel_path = None
524        remote_ramdisk_path = None
525
526        if artifact_paths.boot_image or artifact_paths.system_image:
527            with tempfile.TemporaryDirectory("host_gf") as temp_dir:
528                ota = ota_tools.OtaTools(artifact_paths.ota_tools_dir)
529
530                image_dir = os.path.join(temp_dir, "images")
531                logger.debug("Unzip %s.", artifact_paths.image_zip)
532                with zipfile.ZipFile(artifact_paths.image_zip,
533                                     "r") as zip_file:
534                    zip_file.extractall(image_dir)
535                image_dir = os.path.join(
536                    image_dir,
537                    self._GetSubdirNameInZip(artifact_paths.image_zip))
538
539                if artifact_paths.system_image:
540                    self._MixAndUploadDiskImage(
541                        remote_image_dir, image_dir,
542                        artifact_paths.system_image, ota)
543
544                if artifact_paths.boot_image:
545                    remote_kernel_path, remote_ramdisk_path = (
546                        self._MixAndUploadKernelImages(
547                            image_dir, artifact_paths.boot_image, ota))
548
549        return RemotePaths(remote_image_dir, remote_emulator_dir,
550                           remote_kernel_path, remote_ramdisk_path)
551
552    def _ShouldMixDiskImage(self):
553        """Determines whether a mixed disk image is required.
554
555        This method checks whether the user requires to replace an image that
556        is part of the disk image. Acloud supports replacing system and kernel
557        images. Only the system is installed on the disk.
558
559        Returns:
560            Boolean, whether a mixed disk image is required.
561        """
562        return self._avd_spec.local_system_image or (
563            self._avd_spec.system_build_info.get(constants.BUILD_ID) and
564            self._avd_spec.system_build_info.get(constants.BUILD_TARGET))
565
566    @utils.TimeExecute(
567        function_description="Processing and uploading tools and images")
568    def _UploadDeviceImages(self, emulator_zip_path, image_zip_path):
569        """Upload artifacts to remote host and extract them.
570
571        Args:
572            emulator_zip_path: The local path to the emulator zip.
573            image_zip_path: The local path to the image zip.
574
575        Returns:
576            The remote paths to the extracted emulator tools and images.
577        """
578        remote_emulator_dir = self._GetInstancePath(_REMOTE_EMULATOR_DIR)
579        remote_image_dir = self._GetInstancePath(_REMOTE_IMAGE_DIR)
580        remote_emulator_zip_path = self._GetInstancePath(
581            _REMOTE_EMULATOR_ZIP_PATH)
582        remote_image_zip_path = self._GetInstancePath(_REMOTE_IMAGE_ZIP_PATH)
583        self._ssh.Run(f"mkdir -p {remote_emulator_dir} {remote_image_dir}")
584        self._ssh.ScpPushFile(emulator_zip_path, remote_emulator_zip_path)
585        self._ssh.ScpPushFile(image_zip_path, remote_image_zip_path)
586
587        self._ssh.Run(f"unzip -d {remote_emulator_dir} "
588                      f"{remote_emulator_zip_path}")
589        self._ssh.Run(f"unzip -d {remote_image_dir} {remote_image_zip_path}")
590        remote_emulator_subdir = remote_path.join(
591            remote_emulator_dir, _SDK_REPO_EMULATOR_DIR_NAME)
592        remote_image_subdir = remote_path.join(
593            remote_image_dir, self._GetSubdirNameInZip(image_zip_path))
594        # TODO(b/141898893): In Android build environment, emulator gets build
595        # information from $ANDROID_PRODUCT_OUT/system/build.prop.
596        # If image_dir is an extacted SDK repository, the file is at
597        # image_dir/build.prop. Acloud copies it to
598        # image_dir/system/build.prop.
599        src_path = remote_path.join(remote_image_subdir, "build.prop")
600        dst_path = remote_path.join(remote_image_subdir, "system",
601                                    "build.prop")
602        self._ssh.Run("'test -f %(dst)s || "
603                      "{ mkdir -p %(dst_dir)s && cp %(src)s %(dst)s ; }'" %
604                      {"src": src_path,
605                       "dst": dst_path,
606                       "dst_dir": remote_path.dirname(dst_path)})
607        return remote_emulator_subdir, remote_image_subdir
608
609    def _MixAndUploadDiskImage(self, remote_image_dir, image_dir,
610                               system_image_path, ota):
611        """Mix emulator images with a system image and upload them.
612
613        Args:
614            remote_image_dir: The remote directory where the mixed disk image
615                              is uploaded.
616            image_dir: The directory containing emulator images.
617            system_image_path: The path to the system image.
618            ota: An instance of ota_tools.OtaTools.
619
620        Returns:
621            The remote path to the mixed disk image.
622        """
623        with tempfile.TemporaryDirectory("host_gf_disk") as temp_dir:
624            mixed_image = goldfish_utils.MixWithSystemImage(
625                temp_dir, image_dir, system_image_path, ota)
626
627            # TODO(b/142228085): Use -system instead of overwriting the file.
628            remote_disk_image_path = os.path.join(
629                remote_image_dir, goldfish_utils.SYSTEM_QEMU_IMAGE_NAME)
630            self._ssh.ScpPushFile(mixed_image, remote_disk_image_path)
631
632        return remote_disk_image_path
633
634    def _MixAndUploadKernelImages(self, image_dir, boot_image_path, ota):
635        """Mix emulator kernel images with a boot image and upload them.
636
637        Args:
638            image_dir: The directory containing emulator images.
639            boot_image_path: The path to the boot image.
640            ota: An instance of ota_tools.OtaTools.
641
642        Returns:
643            The remote paths to the kernel image and the ramdisk image.
644        """
645        remote_kernel_path = self._GetInstancePath(_REMOTE_KERNEL_PATH)
646        remote_ramdisk_path = self._GetInstancePath(_REMOTE_RAMDISK_PATH)
647        with tempfile.TemporaryDirectory("host_gf_kernel") as temp_dir:
648            kernel_path, ramdisk_path = goldfish_utils.MixWithBootImage(
649                temp_dir, image_dir, boot_image_path, ota)
650
651            self._ssh.ScpPushFile(kernel_path, remote_kernel_path)
652            self._ssh.ScpPushFile(ramdisk_path, remote_ramdisk_path)
653
654        return remote_kernel_path, remote_ramdisk_path
655
656    def _GetEmulatorLogs(self):
657        """Return the logs created by the remote emulator command."""
658        return [report.LogFile(self._GetInstancePath(_REMOTE_STDOUT_PATH),
659                               constants.LOG_TYPE_KERNEL_LOG),
660                report.LogFile(self._GetInstancePath(_REMOTE_STDERR_PATH),
661                               constants.LOG_TYPE_TEXT),
662                report.LogFile(self._GetInstancePath(_REMOTE_LOGCAT_PATH),
663                               constants.LOG_TYPE_LOGCAT)]
664
665    @utils.TimeExecute(function_description="Start emulator")
666    def _StartEmulator(self, remote_paths):
667        """Start emulator command as a remote background process.
668
669        Args:
670            remote_emulator_dir: The emulator tool directory on remote host.
671            remote_image_dir: The image directory on remote host.
672        """
673        remote_emulator_bin_path = remote_path.join(
674            remote_paths.emulator_dir, _EMULATOR_BIN_NAME)
675        remote_bin_paths = [
676            remote_path.join(remote_paths.emulator_dir, name) for
677            name in _EMULATOR_BIN_DIR_NAMES]
678        remote_bin_paths.append(remote_emulator_bin_path)
679        self._ssh.Run("chmod -R +x %s" % " ".join(remote_bin_paths))
680
681        remote_runtime_dir = self._GetInstancePath(_REMOTE_RUNTIME_DIR)
682        self._ssh.Run(f"mkdir -p {remote_runtime_dir}")
683        env = {constants.ENV_ANDROID_PRODUCT_OUT: remote_paths.image_dir,
684               constants.ENV_ANDROID_TMP: remote_runtime_dir,
685               constants.ENV_ANDROID_BUILD_TOP: remote_runtime_dir}
686        cmd = ["nohup", remote_emulator_bin_path, "-verbose", "-show-kernel",
687               "-read-only", "-ports",
688               str(self._GetConsolePort()) + "," + str(self.GetAdbPorts()[0]),
689               "-no-window",
690               "-logcat-output", self._GetInstancePath(_REMOTE_LOGCAT_PATH)]
691
692        if remote_paths.kernel:
693            cmd.extend(("-kernel", remote_paths.kernel))
694
695        if remote_paths.ramdisk:
696            cmd.extend(("-ramdisk", remote_paths.ramdisk))
697
698        cmd.extend(goldfish_utils.ConvertAvdSpecToArgs(self._avd_spec))
699
700        # Unlock the device so that the disabled vbmeta takes effect.
701        # These arguments must be at the end of the command line.
702        if self._ShouldMixDiskImage():
703            cmd.extend(("-qemu", "-append",
704                        "androidboot.verifiedbootstate=orange"))
705
706        # Emulator does not support -stdouterr-file on macOS.
707        self._ssh.Run(
708            "'export {env} ; {cmd} 1> {stdout} 2> {stderr} &'".format(
709                env=" ".join(k + "=~/" + v for k, v in env.items()),
710                cmd=" ".join(cmd),
711                stdout=self._GetInstancePath(_REMOTE_STDOUT_PATH),
712                stderr=self._GetInstancePath(_REMOTE_STDERR_PATH)))
713
714    @utils.TimeExecute(function_description="Wait for emulator")
715    def _WaitForEmulator(self):
716        """Wait for remote emulator console to be active.
717
718        Raises:
719            errors.DeviceBootError if connection fails.
720            errors.DeviceBootTimeoutError if boot times out.
721        """
722        ip_addr = self._avd_spec.remote_host
723        console_port = self._GetConsolePort()
724        poll_timeout_secs = (self._avd_spec.boot_timeout_secs or
725                             _DEFAULT_BOOT_TIMEOUT_SECS)
726        try:
727            with emulator_console.RemoteEmulatorConsole(
728                    ip_addr,
729                    console_port,
730                    self._ssh_user,
731                    self._ssh_private_key_path,
732                    self._ssh_extra_args) as console:
733                utils.PollAndWait(
734                    func=lambda: (True if console.Ping() else
735                                  console.Reconnect()),
736                    expected_return=True,
737                    timeout_exception=errors.DeviceBootTimeoutError,
738                    timeout_secs=poll_timeout_secs,
739                    sleep_interval_secs=5)
740        except errors.DeviceConnectionError as e:
741            raise errors.DeviceBootError("Fail to connect to %s:%d." %
742                                         (ip_addr, console_port)) from e
743
744    def GetBuildInfoDict(self):
745        """Get build info dictionary.
746
747        Returns:
748            A build info dictionary.
749        """
750        build_info_dict = {key: val for key, val in
751                           self._avd_spec.remote_image.items() if val}
752        return build_info_dict
753
754    def GetAdbPorts(self):
755        """Get ADB ports of the created devices.
756
757        This class does not support --num-avds-per-instance.
758
759        Returns:
760            The port numbers as a list of integers.
761        """
762        return [self._GetConsolePort() + 1]
763
764    def GetFailures(self):
765        """Get Failures from all devices.
766
767        Returns:
768            A dictionary the contains all the failures.
769            The key is the name of the instance that fails to boot,
770            and the value is an errors.DeviceBootError object.
771        """
772        return self._failures
773
774    def GetLogs(self):
775        """Get log files of created instances.
776
777        Returns:
778            A dictionary that maps instance names to lists of report.LogFile.
779        """
780        return self._logs
781