• 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 tempfile
25import zipfile
26
27from acloud import errors
28from acloud.internal import constants
29from acloud.internal.lib import android_build_client
30from acloud.internal.lib import auth
31from acloud.internal.lib import goldfish_remote_host_client
32from acloud.internal.lib import goldfish_utils
33from acloud.internal.lib import emulator_console
34from acloud.internal.lib import ota_tools
35from acloud.internal.lib import utils
36from acloud.internal.lib import ssh
37from acloud.public import report
38from acloud.public.actions import base_device_factory
39
40
41logger = logging.getLogger(__name__)
42# Artifacts
43_SDK_REPO_IMAGE_ZIP_NAME_FORMAT = ("sdk-repo-linux-system-images-"
44                                   "%(build_id)s.zip")
45_EXTRA_IMAGE_ZIP_NAME_FORMAT = "emu-extra-linux-system-images-%(build_id)s.zip"
46_IMAGE_ZIP_NAME_FORMAT = "%(build_target)s-img-%(build_id)s.zip"
47_OTA_TOOLS_ZIP_NAME = "otatools.zip"
48_SYSTEM_IMAGE_NAME = "system.img"
49
50_EMULATOR_INFO_NAME = "emulator-info.txt"
51_EMULATOR_VERSION_PATTERN = re.compile(r"require\s+version-emulator="
52                                       r"(?P<build_id>\w+)")
53_EMULATOR_ZIP_NAME_FORMAT = "sdk-repo-%(os)s-emulator-%(build_id)s.zip"
54_EMULATOR_BIN_DIR_NAMES = ("bin64", "qemu")
55_EMULATOR_BIN_NAME = "emulator"
56# Remote paths
57_REMOTE_WORKING_DIR = "acloud_gf"
58_REMOTE_ARTIFACT_DIR = remote_path.join(_REMOTE_WORKING_DIR, "artifact")
59_REMOTE_IMAGE_DIR = remote_path.join(_REMOTE_WORKING_DIR, "image")
60_REMOTE_KERNEL_PATH = remote_path.join(_REMOTE_WORKING_DIR, "kernel")
61_REMOTE_RAMDISK_PATH = remote_path.join(_REMOTE_WORKING_DIR, "mixed_ramdisk")
62_REMOTE_EMULATOR_DIR = remote_path.join(_REMOTE_WORKING_DIR, "emulator")
63_REMOTE_INSTANCE_DIR = remote_path.join(_REMOTE_WORKING_DIR, "instance")
64_REMOTE_LOGCAT_PATH = os.path.join(_REMOTE_INSTANCE_DIR, "logcat.txt")
65_REMOTE_STDOUTERR_PATH = os.path.join(_REMOTE_INSTANCE_DIR, "kernel.log")
66# Runtime parameters
67_EMULATOR_DEFAULT_CONSOLE_PORT = 5554
68_DEFAULT_BOOT_TIMEOUT_SECS = 150
69
70ArtifactPaths = collections.namedtuple(
71    "ArtifactPaths",
72    ["image_zip", "emulator_zip", "ota_tools_zip",
73     "system_image_zip", "boot_image"])
74
75RemotePaths = collections.namedtuple(
76    "RemotePaths",
77    ["image_dir", "emulator_dir", "kernel", "ramdisk"])
78
79
80class RemoteHostGoldfishDeviceFactory(base_device_factory.BaseDeviceFactory):
81    """A class that creates a goldfish device on a remote host.
82
83    Attributes:
84        avd_spec: AVDSpec object that tells us what we're going to create.
85        ssh: Ssh object that executes commands on the remote host.
86        failures: A dictionary the maps instance names to
87                  error.DeviceBootError objects.
88        logs: A dictionary that maps instance names to lists of report.LogFile.
89    """
90    def __init__(self, avd_spec):
91        """Initialize the attributes and the compute client."""
92        self._avd_spec = avd_spec
93        self._ssh = ssh.Ssh(
94            ip=ssh.IP(ip=self._avd_spec.remote_host),
95            user=self._ssh_user,
96            ssh_private_key_path=self._ssh_private_key_path,
97            extra_args_ssh_tunnel=self._ssh_extra_args,
98            report_internal_ip=False)
99        self._failures = {}
100        self._logs = {}
101        super().__init__(compute_client=(
102            goldfish_remote_host_client.GoldfishRemoteHostClient()))
103
104    @property
105    def _ssh_user(self):
106        return self._avd_spec.host_user or constants.GCE_USER
107
108    @property
109    def _ssh_private_key_path(self):
110        return (self._avd_spec.host_ssh_private_key_path or
111                self._avd_spec.cfg.ssh_private_key_path)
112
113    @property
114    def _ssh_extra_args(self):
115        return self._avd_spec.cfg.extra_args_ssh_tunnel
116
117    def CreateInstance(self):
118        """Create a goldfish instance on the remote host.
119
120        Returns:
121            The instance name.
122        """
123        self._InitRemoteHost()
124        remote_paths = self._PrepareArtifacts()
125
126        instance_name = goldfish_remote_host_client.FormatInstanceName(
127            self._avd_spec.remote_host,
128            _EMULATOR_DEFAULT_CONSOLE_PORT,
129            self._avd_spec.remote_image)
130        self._logs[instance_name] = [
131            report.LogFile(_REMOTE_STDOUTERR_PATH,
132                           constants.LOG_TYPE_KERNEL_LOG),
133            report.LogFile(_REMOTE_LOGCAT_PATH, constants.LOG_TYPE_LOGCAT)]
134        try:
135            self._StartEmulator(remote_paths)
136            self._WaitForEmulator()
137        except errors.DeviceBootError as e:
138            self._failures[instance_name] = e
139        return instance_name
140
141    def _InitRemoteHost(self):
142        """Remove existing instance and working directory."""
143        # Disable authentication for emulator console.
144        self._ssh.Run("""'echo -n "" > .emulator_console_auth_token'""")
145        try:
146            with emulator_console.RemoteEmulatorConsole(
147                    self._avd_spec.remote_host,
148                    _EMULATOR_DEFAULT_CONSOLE_PORT,
149                    self._ssh_user,
150                    self._ssh_private_key_path,
151                    self._ssh_extra_args) as console:
152                console.Kill()
153            logger.info("Killed existing emulator.")
154        except errors.DeviceConnectionError as e:
155            logger.info("Did not kill existing emulator: %s", str(e))
156        # Delete instance files.
157        self._ssh.Run("rm -rf %s" % _REMOTE_WORKING_DIR)
158
159    def _PrepareArtifacts(self):
160        """Prepare artifacts on remote host.
161
162        This method retrieves artifacts from cache or Android Build API and
163        uploads them to the remote host.
164
165        Returns:
166            An object of RemotePaths.
167        """
168        if self._avd_spec.image_download_dir:
169            temp_download_dir = None
170            download_dir = self._avd_spec.image_download_dir
171        else:
172            temp_download_dir = tempfile.mkdtemp()
173            download_dir = temp_download_dir
174            logger.info("--image-download-dir is not specified. Create "
175                        "temporary download directory: %s", download_dir)
176
177        try:
178            artifact_paths = self._RetrieveArtifacts(download_dir)
179            return self._UploadArtifacts(artifact_paths)
180        finally:
181            if temp_download_dir:
182                shutil.rmtree(temp_download_dir, ignore_errors=True)
183
184    @staticmethod
185    def _InferEmulatorZipName(build_target, build_id):
186        """Determine the emulator zip name in build artifacts.
187
188        The emulator zip name is composed of build variables that are not
189        revealed in the artifacts. This method infers the emulator zip name
190        from its build target name.
191
192        Args:
193            build_target: The emulator build target name, e.g.,
194                          "sdk_tools_linux", "aarch64_sdk_tools_mac".
195            build_id: A string, the emulator build ID.
196
197        Returns:
198            The name of the emulator zip. e.g.,
199            "sdk-repo-linux-emulator-123456.zip",
200            "sdk-repo-darwin_aarch64-emulator-123456.zip".
201        """
202        split_target = [x for product_variant in build_target.split("-")
203                        for x in product_variant.split("_")]
204        if "darwin" in split_target or "mac" in split_target:
205            os_name = "darwin"
206        else:
207            os_name = "linux"
208        if "aarch64" in split_target:
209            os_name = os_name + "_aarch64"
210        return _EMULATOR_ZIP_NAME_FORMAT % {"os": os_name,
211                                            "build_id": build_id}
212
213    @staticmethod
214    def _RetrieveArtifact(download_dir, build_api, build_target, build_id,
215                          resource_id):
216        """Retrieve an artifact from cache or Android Build API.
217
218        Args:
219            download_dir: The cache directory.
220            build_api: An AndroidBuildClient object.
221            build_target: A string, the build target of the artifact. e.g.,
222                          "sdk_phone_x86_64-userdebug".
223            build_id: A string, the build ID of the artifact.
224            resource_id: A string, the name of the artifact. e.g.,
225                         "sdk-repo-linux-system-images-123456.zip".
226
227        Returns:
228            The path to the artifact in download_dir.
229        """
230        local_path = os.path.join(download_dir, build_id, build_target,
231                                  resource_id)
232        if os.path.isfile(local_path):
233            logger.info("Skip downloading existing artifact: %s", local_path)
234            return local_path
235
236        complete = False
237        try:
238            os.makedirs(os.path.dirname(local_path), exist_ok=True)
239            build_api.DownloadArtifact(build_target, build_id, resource_id,
240                                       local_path, build_api.LATEST)
241            complete = True
242        finally:
243            if not complete and os.path.isfile(local_path):
244                os.remove(local_path)
245        return local_path
246
247    def _RetrieveEmulatorBuildID(self, download_dir, build_api, build_target,
248                                 build_id):
249        """Retrieve required emulator build from a goldfish image build."""
250        emulator_info_path = self._RetrieveArtifact(download_dir, build_api,
251                                                    build_target, build_id,
252                                                    _EMULATOR_INFO_NAME)
253        with open(emulator_info_path, 'r') as emulator_info:
254            for line in emulator_info:
255                match = _EMULATOR_VERSION_PATTERN.fullmatch(line.strip())
256                if match:
257                    logger.info("Found emulator build ID: %s", line)
258                    return match.group("build_id")
259        return None
260
261    @utils.TimeExecute(function_description="Download Android Build artifacts")
262    def _RetrieveArtifacts(self, download_dir):
263        """Retrieve goldfish images and tools from cache or Android Build API.
264
265        Args:
266            download_dir: The cache directory.
267
268        Returns:
269            An object of ArtifactPaths.
270
271        Raises:
272            errors.GetRemoteImageError: Fails to download rom images.
273        """
274        credentials = auth.CreateCredentials(self._avd_spec.cfg)
275        build_api = android_build_client.AndroidBuildClient(credentials)
276        # Device images.
277        build_id = self._avd_spec.remote_image.get(constants.BUILD_ID)
278        build_target = self._avd_spec.remote_image.get(constants.BUILD_TARGET)
279        image_zip_name_format = (_EXTRA_IMAGE_ZIP_NAME_FORMAT if
280                                 self._ShouldMixDiskImage() else
281                                 _SDK_REPO_IMAGE_ZIP_NAME_FORMAT)
282        image_zip_path = self._RetrieveArtifact(
283            download_dir, build_api, build_target, build_id,
284            image_zip_name_format % {"build_id": build_id})
285
286        # Emulator tools.
287        emu_build_id = self._avd_spec.emulator_build_id
288        if not emu_build_id:
289            emu_build_id = self._RetrieveEmulatorBuildID(
290                download_dir, build_api, build_target, build_id)
291            if not emu_build_id:
292                raise errors.GetRemoteImageError(
293                    "No emulator build ID in command line or "
294                    "emulator-info.txt.")
295
296        emu_build_target = (self._avd_spec.emulator_build_target or
297                            self._avd_spec.cfg.emulator_build_target)
298        emu_zip_name = self._InferEmulatorZipName(emu_build_target,
299                                                  emu_build_id)
300        emu_zip_path = self._RetrieveArtifact(download_dir, build_api,
301                                              emu_build_target, emu_build_id,
302                                              emu_zip_name)
303
304        system_image_zip_path = self._RetrieveSystemImageZip(
305            download_dir, build_api)
306        boot_image_path = self._RetrieveBootImage(download_dir, build_api)
307        # Retrieve OTA tools from the goldfish build which contains
308        # mk_combined_img.
309        ota_tools_zip_path = (
310            self._RetrieveArtifact(download_dir, build_api, build_target,
311                                   build_id, _OTA_TOOLS_ZIP_NAME)
312            if system_image_zip_path or boot_image_path else None)
313
314        return ArtifactPaths(image_zip_path, emu_zip_path,
315                             ota_tools_zip_path, system_image_zip_path,
316                             boot_image_path)
317
318    def _RetrieveSystemImageZip(self, download_dir, build_api):
319        """Retrieve system image zip if system build info is not empty.
320
321        Args:
322            download_dir: The download cache directory.
323            build_api: An AndroidBuildClient object.
324
325        Returns:
326            The path to the system image zip in download_dir.
327            None if the system build info is empty.
328        """
329        build_id = self._avd_spec.system_build_info.get(constants.BUILD_ID)
330        build_target = self._avd_spec.system_build_info.get(
331            constants.BUILD_TARGET)
332        if build_id and build_target:
333            image_zip_name = _IMAGE_ZIP_NAME_FORMAT % {
334                "build_target": build_target.split("-", 1)[0],
335                "build_id": build_id}
336            return self._RetrieveArtifact(
337                download_dir, build_api, build_target, build_id,
338                image_zip_name)
339        return None
340
341    def _RetrieveBootImage(self, download_dir, build_api):
342        """Retrieve boot image if kernel build info is not empty.
343
344        Args:
345            download_dir: The download cache directory.
346            build_api: An AndroidBuildClient object.
347
348        Returns:
349            The path to the boot image in download_dir.
350            None if the kernel build info is empty.
351        """
352        build_id = self._avd_spec.kernel_build_info.get(constants.BUILD_ID)
353        build_target = self._avd_spec.kernel_build_info.get(
354            constants.BUILD_TARGET)
355        image_name = self._avd_spec.kernel_build_info.get(
356            constants.BUILD_ARTIFACT)
357        if build_id and build_target and image_name:
358            return self._RetrieveArtifact(
359                download_dir, build_api, build_target, build_id, image_name)
360        return None
361
362    @staticmethod
363    def _GetSubdirNameInZip(zip_path):
364        """Get the name of the only subdirectory in a zip.
365
366        In an SDK repository zip, the images and the binaries are located in a
367        subdirectory. This class needs to find out the subdirectory name in
368        order to construct the remote commands.
369
370        For example, in sdk-repo-*-emulator-*.zip, all files are in
371        "emulator/". The zip entries are:
372
373        emulator/NOTICE.txt
374        emulator/emulator
375        emulator/lib64/libc++.so
376        ...
377
378        This method scans the entries and returns the common subdirectory name.
379        """
380        sep = "/"
381        with zipfile.ZipFile(zip_path, 'r') as zip_obj:
382            entries = zip_obj.namelist()
383            if len(entries) > 0 and sep in entries[0]:
384                subdir = entries[0].split(sep, 1)[0]
385                if all(e.startswith(subdir + sep) for e in entries):
386                    return subdir
387            logger.warning("Expect one subdirectory in %s. Actual entries: %s",
388                           zip_path, " ".join(entries))
389            return ""
390
391    def _UploadArtifacts(self, artifacts_paths):
392        """Process and upload all images and tools to the remote host.
393
394        Args:
395            artifact_paths: An object of ArtifactPaths.
396
397        Returns:
398            An object of RemotePaths.
399        """
400        remote_emulator_dir, remote_image_dir = self._UploadDeviceImages(
401            artifacts_paths.emulator_zip, artifacts_paths.image_zip)
402
403        remote_kernel_path = None
404        remote_ramdisk_path = None
405
406        if artifacts_paths.boot_image or artifacts_paths.system_image_zip:
407            with tempfile.TemporaryDirectory("host_gf") as temp_dir:
408                ota_tools_dir = os.path.join(temp_dir, "ota_tools")
409                logger.debug("Unzip %s.", artifacts_paths.ota_tools_zip)
410                with zipfile.ZipFile(artifacts_paths.ota_tools_zip,
411                                     "r") as zip_file:
412                    zip_file.extractall(ota_tools_dir)
413                ota = ota_tools.OtaTools(ota_tools_dir)
414
415                image_dir = os.path.join(temp_dir, "images")
416                logger.debug("Unzip %s.", artifacts_paths.image_zip)
417                with zipfile.ZipFile(artifacts_paths.image_zip,
418                                     "r") as zip_file:
419                    zip_file.extractall(image_dir)
420                image_dir = os.path.join(
421                    image_dir,
422                    self._GetSubdirNameInZip(artifacts_paths.image_zip))
423
424                if artifacts_paths.system_image_zip:
425                    self._MixAndUploadDiskImage(
426                        remote_image_dir, image_dir,
427                        artifacts_paths.system_image_zip, ota)
428
429                if artifacts_paths.boot_image:
430                    remote_kernel_path, remote_ramdisk_path = (
431                        self._MixAndUploadKernelImages(
432                            image_dir, artifacts_paths.boot_image, ota))
433
434        return RemotePaths(remote_image_dir, remote_emulator_dir,
435                           remote_kernel_path, remote_ramdisk_path)
436
437    def _ShouldMixDiskImage(self):
438        """Determines whether a mixed disk image is required.
439
440        This method checks whether the user requires to replace an image that
441        is part of the disk image. Acloud supports replacing system and kernel
442        images. Only the system is installed on the disk.
443
444        Returns:
445            Boolean, whether a mixed disk image is required.
446        """
447        return (self._avd_spec.system_build_info.get(constants.BUILD_ID) and
448                self._avd_spec.system_build_info.get(constants.BUILD_TARGET))
449
450    @utils.TimeExecute(
451        function_description="Processing and uploading tools and images")
452    def _UploadDeviceImages(self, emulator_zip_path, image_zip_path):
453        """Upload artifacts to remote host and extract them.
454
455        Args:
456            emulator_zip_path: The local path to the emulator zip.
457            image_zip_path: The local path to the image zip.
458
459        Returns:
460            The remote paths to the extracted emulator tools and images.
461        """
462        self._ssh.Run("mkdir -p " +
463                      " ".join([_REMOTE_INSTANCE_DIR, _REMOTE_ARTIFACT_DIR,
464                                _REMOTE_EMULATOR_DIR, _REMOTE_IMAGE_DIR]))
465        self._ssh.ScpPushFile(emulator_zip_path, _REMOTE_ARTIFACT_DIR)
466        self._ssh.ScpPushFile(image_zip_path, _REMOTE_ARTIFACT_DIR)
467
468        self._ssh.Run("unzip -d %s %s" % (
469            _REMOTE_EMULATOR_DIR,
470            remote_path.join(_REMOTE_ARTIFACT_DIR,
471                             os.path.basename(emulator_zip_path))))
472        self._ssh.Run("unzip -d %s %s" % (
473            _REMOTE_IMAGE_DIR,
474            remote_path.join(_REMOTE_ARTIFACT_DIR,
475                             os.path.basename(image_zip_path))))
476        remote_emulator_subdir = remote_path.join(
477            _REMOTE_EMULATOR_DIR, self._GetSubdirNameInZip(emulator_zip_path))
478        remote_image_subdir = remote_path.join(
479            _REMOTE_IMAGE_DIR, self._GetSubdirNameInZip(image_zip_path))
480        # TODO(b/141898893): In Android build environment, emulator gets build
481        # information from $ANDROID_PRODUCT_OUT/system/build.prop.
482        # If image_dir is an extacted SDK repository, the file is at
483        # image_dir/build.prop. Acloud copies it to
484        # image_dir/system/build.prop.
485        src_path = remote_path.join(remote_image_subdir, "build.prop")
486        dst_path = remote_path.join(remote_image_subdir, "system",
487                                    "build.prop")
488        self._ssh.Run("'test -f %(dst)s || "
489                      "{ mkdir -p %(dst_dir)s && cp %(src)s %(dst)s ; }'" %
490                      {"src": src_path,
491                       "dst": dst_path,
492                       "dst_dir": remote_path.dirname(dst_path)})
493        return remote_emulator_subdir, remote_image_subdir
494
495    def _MixAndUploadDiskImage(self, remote_image_dir, image_dir,
496                               system_image_zip_path, ota):
497        """Mix emulator images with a system image and upload them.
498
499        Args:
500            remote_image_dir: The remote directory where the mixed disk image
501                              is uploaded.
502            image_dir: The directory containing emulator images.
503            system_image_zip_path: The path to the zip containing the system
504                                   image.
505            ota: An instance of ota_tools.OtaTools.
506
507        Returns:
508            The remote path to the mixed disk image.
509        """
510        with tempfile.TemporaryDirectory("host_gf_disk") as temp_dir:
511            logger.debug("Unzip %s.", system_image_zip_path)
512            with zipfile.ZipFile(system_image_zip_path, "r") as zip_file:
513                zip_file.extract(_SYSTEM_IMAGE_NAME, temp_dir)
514
515            mixed_image = goldfish_utils.MixWithSystemImage(
516                os.path.join(temp_dir, "mix_disk"),
517                image_dir,
518                os.path.join(temp_dir, _SYSTEM_IMAGE_NAME),
519                ota)
520
521            # TODO(b/142228085): Use -system instead of overwriting the file.
522            remote_disk_image_path = os.path.join(
523                remote_image_dir, goldfish_utils.SYSTEM_QEMU_IMAGE_NAME)
524            self._ssh.ScpPushFile(mixed_image, remote_disk_image_path)
525
526        return remote_disk_image_path
527
528    def _MixAndUploadKernelImages(self, image_dir, boot_image_path, ota):
529        """Mix emulator kernel images with a boot image and upload them.
530
531        Args:
532            image_dir: The directory containing emulator images.
533            boot_image_path: The path to the boot image.
534            ota: An instance of ota_tools.OtaTools.
535
536        Returns:
537            The remote paths to the kernel image and the ramdisk image.
538        """
539        with tempfile.TemporaryDirectory("host_gf_kernel") as temp_dir:
540            kernel_path, ramdisk_path = goldfish_utils.MixWithBootImage(
541                temp_dir, image_dir, boot_image_path, ota)
542
543            self._ssh.ScpPushFile(kernel_path, _REMOTE_KERNEL_PATH)
544            self._ssh.ScpPushFile(ramdisk_path, _REMOTE_RAMDISK_PATH)
545
546        return _REMOTE_KERNEL_PATH, _REMOTE_RAMDISK_PATH
547
548    @utils.TimeExecute(function_description="Start emulator")
549    def _StartEmulator(self, remote_paths):
550        """Start emulator command as a remote background process.
551
552        Args:
553            remote_emulator_dir: The emulator tool directory on remote host.
554            remote_image_dir: The image directory on remote host.
555        """
556        remote_emulator_bin_path = remote_path.join(
557            remote_paths.emulator_dir, _EMULATOR_BIN_NAME)
558        remote_bin_paths = [
559            remote_path.join(remote_paths.emulator_dir, name) for
560            name in _EMULATOR_BIN_DIR_NAMES]
561        remote_bin_paths.append(remote_emulator_bin_path)
562        self._ssh.Run("chmod -R +x %s" % " ".join(remote_bin_paths))
563
564        env = {constants.ENV_ANDROID_PRODUCT_OUT: remote_paths.image_dir,
565               constants.ENV_ANDROID_TMP: _REMOTE_INSTANCE_DIR,
566               constants.ENV_ANDROID_BUILD_TOP: _REMOTE_INSTANCE_DIR}
567        adb_port = _EMULATOR_DEFAULT_CONSOLE_PORT + 1
568        cmd = ["nohup", remote_emulator_bin_path, "-verbose", "-show-kernel",
569               "-read-only", "-ports",
570               str(_EMULATOR_DEFAULT_CONSOLE_PORT) + "," + str(adb_port),
571               "-no-window",
572               "-logcat-output", _REMOTE_LOGCAT_PATH,
573               "-stdouterr-file", _REMOTE_STDOUTERR_PATH]
574
575        if remote_paths.kernel:
576            cmd.extend(("-kernel", remote_paths.kernel))
577
578        if remote_paths.ramdisk:
579            cmd.extend(("-ramdisk", remote_paths.ramdisk))
580
581        cmd.extend(goldfish_utils.ConvertAvdSpecToArgs(self._avd_spec))
582
583        # Unlock the device so that the disabled vbmeta takes effect.
584        # These arguments must be at the end of the command line.
585        if self._ShouldMixDiskImage():
586            cmd.extend(("-qemu", "-append",
587                        "androidboot.verifiedbootstate=orange"))
588
589        # Emulator doesn't create -stdouterr-file automatically.
590        self._ssh.Run(
591            "'export {env} ; touch {stdouterr} ; {cmd} &'".format(
592                env=" ".join(k + "=~/" + v for k, v in env.items()),
593                stdouterr=_REMOTE_STDOUTERR_PATH,
594                cmd=" ".join(cmd)))
595
596    @utils.TimeExecute(function_description="Wait for emulator")
597    def _WaitForEmulator(self):
598        """Wait for remote emulator console to be active.
599
600        Raises:
601            errors.DeviceBootError if connection fails.
602            errors.DeviceBootTimeoutError if boot times out.
603        """
604        ip_addr = self._avd_spec.remote_host
605        console_port = _EMULATOR_DEFAULT_CONSOLE_PORT
606        poll_timeout_secs = (self._avd_spec.boot_timeout_secs or
607                             _DEFAULT_BOOT_TIMEOUT_SECS)
608        try:
609            with emulator_console.RemoteEmulatorConsole(
610                    ip_addr,
611                    console_port,
612                    self._ssh_user,
613                    self._ssh_private_key_path,
614                    self._ssh_extra_args) as console:
615                utils.PollAndWait(
616                    func=lambda: (True if console.Ping() else
617                                  console.Reconnect()),
618                    expected_return=True,
619                    timeout_exception=errors.DeviceBootTimeoutError,
620                    timeout_secs=poll_timeout_secs,
621                    sleep_interval_secs=5)
622        except errors.DeviceConnectionError as e:
623            raise errors.DeviceBootError("Fail to connect to %s:%d." %
624                                         (ip_addr, console_port)) from e
625
626    def GetBuildInfoDict(self):
627        """Get build info dictionary.
628
629        Returns:
630            A build info dictionary.
631        """
632        build_info_dict = {key: val for key, val in
633                           self._avd_spec.remote_image.items() if val}
634        return build_info_dict
635
636    def GetFailures(self):
637        """Get Failures from all devices.
638
639        Returns:
640            A dictionary the contains all the failures.
641            The key is the name of the instance that fails to boot,
642            and the value is an errors.DeviceBootError object.
643        """
644        return self._failures
645
646    def GetLogs(self):
647        """Get log files of created instances.
648
649        Returns:
650            A dictionary that maps instance names to lists of report.LogFile.
651        """
652        return self._logs
653