1# Copyright 2019 - 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.
14r"""GoldfishLocalImageLocalInstance class.
15
16Create class that is responsible for creating a local goldfish instance with
17local images.
18
19The emulator binary supports two types of environments, Android build system
20and SDK. This class runs the emulator in build environment.
21- This class uses the prebuilt emulator in ANDROID_EMULATOR_PREBUILTS.
22- If the instance requires mixed images, this class uses the OTA tools in
23  ANDROID_HOST_OUT.
24
25To run this program outside of a build environment, the following setup is
26required.
27- One of the local tool directories is an unzipped SDK emulator repository,
28  i.e., sdk-repo-<os>-emulator-<build>.zip.
29- If the instance doesn't require mixed images, the local image directory
30  should be an unzipped SDK image repository, i.e.,
31  sdk-repo-<os>-system-images-<build>.zip.
32- If the instance requires mixed images, the local image directory should
33  contain both the unzipped update package and the unzipped extra image
34  package, i.e., <target>-img-<build>.zip and
35  emu-extra-<os>-system-images-<build>.zip.
36- If the instance requires mixed images, one of the local tool directories
37  should be an unzipped OTA tools package, i.e., otatools.zip.
38"""
39
40import logging
41import os
42import shutil
43import subprocess
44import sys
45
46from acloud import errors
47from acloud.create import base_avd_create
48from acloud.internal import constants
49from acloud.internal.lib import adb_tools
50from acloud.internal.lib import ota_tools
51from acloud.internal.lib import utils
52from acloud.list import instance
53from acloud.public import report
54
55
56logger = logging.getLogger(__name__)
57
58# Input and output file names
59_EMULATOR_BIN_NAME = "emulator"
60_SDK_REPO_EMULATOR_DIR_NAME = "emulator"
61_SYSTEM_IMAGE_NAME = "system.img"
62_SYSTEM_QEMU_IMAGE_NAME = "system-qemu.img"
63_NON_MIXED_BACKUP_IMAGE_EXT = ".bak-non-mixed"
64_BUILD_PROP_FILE_NAME = "build.prop"
65_MISC_INFO_FILE_NAME = "misc_info.txt"
66_SYSTEM_QEMU_CONFIG_FILE_NAME = "system-qemu-config.txt"
67
68# Partition names
69_SYSTEM_PARTITION_NAME = "system"
70_SUPER_PARTITION_NAME = "super"
71_VBMETA_PARTITION_NAME = "vbmeta"
72
73# Timeout
74_DEFAULT_EMULATOR_TIMEOUT_SECS = 150
75_EMULATOR_TIMEOUT_ERROR = "Emulator did not boot within %(timeout)d secs."
76_EMU_KILL_TIMEOUT_SECS = 20
77_EMU_KILL_TIMEOUT_ERROR = "Emulator did not stop within %(timeout)d secs."
78
79_CONFIRM_RELAUNCH = ("\nGoldfish AVD is already running. \n"
80                     "Enter 'y' to terminate current instance and launch a "
81                     "new instance, enter anything else to exit out[y/N]: ")
82
83_MISSING_EMULATOR_MSG = ("Emulator binary is not found. Check "
84                         "ANDROID_EMULATOR_PREBUILTS in build environment, "
85                         "or set --local-tool to an unzipped SDK emulator "
86                         "repository.")
87
88
89def _GetImageForLogicalPartition(partition_name, system_image_path, image_dir):
90    """Map a logical partition name to an image path.
91
92    Args:
93        partition_name: String. On emulator, the logical partitions include
94                        "system", "vendor", and "product".
95        system_image_path: String. The path to system image.
96        image_dir: String. The directory containing the other images.
97
98    Returns:
99        system_image_path if the partition is "system".
100        Otherwise, this method returns the path under image_dir.
101
102    Raises
103        errors.GetLocalImageError if the image does not exist.
104    """
105    if partition_name == _SYSTEM_PARTITION_NAME:
106        image_path = system_image_path
107    else:
108        image_path = os.path.join(image_dir, partition_name + ".img")
109    if not os.path.isfile(image_path):
110        raise errors.GetLocalImageError(
111            "Cannot find image for logical partition %s" % partition_name)
112    return image_path
113
114
115def _GetImageForPhysicalPartition(partition_name, super_image_path,
116                                  vbmeta_image_path, image_dir):
117    """Map a physical partition name to an image path.
118
119    Args:
120        partition_name: String. On emulator, the physical partitions include
121                        "super" and "vbmeta".
122        super_image_path: String. The path to super image.
123        vbmeta_image_path: String. The path to vbmeta image.
124        image_dir: String. The directory containing the other images.
125
126    Returns:
127        super_image_path if the partition is "super".
128        vbmeta_image_path if the partition is "vbmeta".
129        Otherwise, this method returns the path under image_dir.
130
131    Raises:
132        errors.GetLocalImageError if the image does not exist.
133    """
134    if partition_name == _SUPER_PARTITION_NAME:
135        image_path = super_image_path
136    elif partition_name == _VBMETA_PARTITION_NAME:
137        image_path = vbmeta_image_path
138    else:
139        image_path = os.path.join(image_dir, partition_name + ".img")
140    if not os.path.isfile(image_path):
141        raise errors.GetLocalImageError(
142            "Unexpected physical partition: %s" % partition_name)
143    return image_path
144
145
146class GoldfishLocalImageLocalInstance(base_avd_create.BaseAVDCreate):
147    """Create class for a local image local instance emulator."""
148
149    def _CreateAVD(self, avd_spec, no_prompts):
150        """Create the AVD.
151
152        Args:
153            avd_spec: AVDSpec object that provides the local image directory.
154            no_prompts: Boolean, True to skip all prompts.
155
156        Returns:
157            A Report instance.
158
159        Raises:
160            errors.GetSdkRepoPackageError if emulator binary is not found.
161            errors.GetLocalImageError if the local image directory does not
162            contain required files.
163            errors.CreateError if an instance exists and cannot be deleted.
164            errors.CheckPathError if OTA tools are not found.
165        """
166        if not utils.IsSupportedPlatform(print_warning=True):
167            result_report = report.Report(command="create")
168            result_report.SetStatus(report.Status.FAIL)
169            return result_report
170
171        emulator_path = self._FindEmulatorBinary(avd_spec.local_tool_dirs)
172        emulator_path = os.path.abspath(emulator_path)
173
174        image_dir = os.path.abspath(avd_spec.local_image_dir)
175
176        if not (os.path.isfile(os.path.join(image_dir, _SYSTEM_IMAGE_NAME)) or
177                os.path.isfile(os.path.join(image_dir,
178                                            _SYSTEM_QEMU_IMAGE_NAME))):
179            raise errors.GetLocalImageError("No system image in %s." %
180                                            image_dir)
181
182        # TODO(b/141898893): In Android build environment, emulator gets build
183        # information from $ANDROID_PRODUCT_OUT/system/build.prop.
184        # If image_dir is an extacted SDK repository, the file is at
185        # image_dir/build.prop. Acloud copies it to
186        # image_dir/system/build.prop.
187        self._CopyBuildProp(image_dir)
188
189        instance_id = avd_spec.local_instance_id
190        inst = instance.LocalGoldfishInstance(instance_id,
191                                              avd_flavor=avd_spec.flavor)
192        adb = adb_tools.AdbTools(adb_port=inst.adb_port,
193                                 device_serial=inst.device_serial)
194
195        self._CheckRunningEmulator(adb, no_prompts)
196
197        instance_dir = inst.instance_dir
198        shutil.rmtree(instance_dir, ignore_errors=True)
199        os.makedirs(instance_dir)
200
201        extra_args = self._ConvertAvdSpecToArgs(avd_spec, instance_dir)
202
203        logger.info("Instance directory: %s", instance_dir)
204        proc = self._StartEmulatorProcess(emulator_path, instance_dir,
205                                          image_dir, inst.console_port,
206                                          inst.adb_port, extra_args)
207
208        boot_timeout_secs = (avd_spec.boot_timeout_secs or
209                             _DEFAULT_EMULATOR_TIMEOUT_SECS)
210        result_report = report.Report(command="create")
211        try:
212            self._WaitForEmulatorToStart(adb, proc, boot_timeout_secs)
213        except (errors.DeviceBootTimeoutError, errors.SubprocessFail) as e:
214            result_report.SetStatus(report.Status.BOOT_FAIL)
215            result_report.AddDeviceBootFailure(inst.name, inst.ip,
216                                               inst.adb_port, vnc_port=None,
217                                               error=str(e))
218        else:
219            result_report.SetStatus(report.Status.SUCCESS)
220            result_report.AddDevice(inst.name, inst.ip, inst.adb_port,
221                                    vnc_port=None)
222
223        if proc.poll() is None:
224            inst.WriteCreationTimestamp()
225
226        return result_report
227
228    @staticmethod
229    def _MixImages(output_dir, image_dir, system_image_dir, ota):
230        """Mix emulator images and a system image into a disk image.
231
232        Args:
233            output_dir: The path to the output directory.
234            image_dir: The input directory that provides images except
235                       system.img.
236            system_image_dir: The input directory that provides system.img.
237            ota: An instance of ota_tools.OtaTools.
238
239        Returns:
240            The path to the mixed disk image in output_dir.
241        """
242        # Create the super image.
243        mixed_super_image_path = os.path.join(output_dir, "mixed_super.img")
244        system_image_path = os.path.join(system_image_dir, _SYSTEM_IMAGE_NAME)
245        ota.BuildSuperImage(mixed_super_image_path,
246                            os.path.join(image_dir, _MISC_INFO_FILE_NAME),
247                            lambda partition: _GetImageForLogicalPartition(
248                                partition, system_image_path, image_dir))
249
250        # Create the vbmeta image.
251        disabled_vbmeta_image_path = os.path.join(output_dir,
252                                                  "disabled_vbmeta.img")
253        ota.MakeDisabledVbmetaImage(disabled_vbmeta_image_path)
254
255        # Create the disk image.
256        combined_image = os.path.join(output_dir, "combined.img")
257        ota.MkCombinedImg(combined_image,
258                          os.path.join(image_dir,
259                                       _SYSTEM_QEMU_CONFIG_FILE_NAME),
260                          lambda partition: _GetImageForPhysicalPartition(
261                              partition, mixed_super_image_path,
262                              disabled_vbmeta_image_path, image_dir))
263        return combined_image
264
265    @staticmethod
266    def _FindEmulatorBinary(search_paths):
267        """Return the path to the emulator binary."""
268        # Find in unzipped sdk-repo-*.zip.
269        for search_path in search_paths:
270            path = os.path.join(search_path, _EMULATOR_BIN_NAME)
271            if os.path.isfile(path):
272                return path
273
274            path = os.path.join(search_path, _SDK_REPO_EMULATOR_DIR_NAME,
275                                _EMULATOR_BIN_NAME)
276            if os.path.isfile(path):
277                return path
278
279        # Find in build environment.
280        prebuilt_emulator_dir = os.environ.get(
281            constants.ENV_ANDROID_EMULATOR_PREBUILTS)
282        if prebuilt_emulator_dir:
283            path = os.path.join(prebuilt_emulator_dir, _EMULATOR_BIN_NAME)
284            if os.path.isfile(path):
285                return path
286
287        raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG)
288
289    @staticmethod
290    def _IsEmulatorRunning(adb):
291        """Check existence of an emulator by sending an empty command.
292
293        Args:
294            adb: adb_tools.AdbTools initialized with the emulator's serial.
295
296        Returns:
297            Boolean, whether the emulator is running.
298        """
299        return adb.EmuCommand() == 0
300
301    def _CheckRunningEmulator(self, adb, no_prompts):
302        """Attempt to delete a running emulator.
303
304        Args:
305            adb: adb_tools.AdbTools initialized with the emulator's serial.
306            no_prompts: Boolean, True to skip all prompts.
307
308        Raises:
309            errors.CreateError if the emulator isn't deleted.
310        """
311        if not self._IsEmulatorRunning(adb):
312            return
313        logger.info("Goldfish AVD is already running.")
314        if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH):
315            if adb.EmuCommand("kill") != 0:
316                raise errors.CreateError("Cannot kill emulator.")
317            self._WaitForEmulatorToStop(adb)
318        else:
319            sys.exit(constants.EXIT_BY_USER)
320
321    @staticmethod
322    def _CopyBuildProp(image_dir):
323        """Copy build.prop to system/build.prop if it doesn't exist.
324
325        Args:
326            image_dir: The directory to find build.prop in.
327
328        Raises:
329            errors.GetLocalImageError if build.prop does not exist.
330        """
331        build_prop_path = os.path.join(image_dir, "system",
332                                       _BUILD_PROP_FILE_NAME)
333        if os.path.exists(build_prop_path):
334            return
335        build_prop_src_path = os.path.join(image_dir, _BUILD_PROP_FILE_NAME)
336        if not os.path.isfile(build_prop_src_path):
337            raise errors.GetLocalImageError("No %s in %s." %
338                                            _BUILD_PROP_FILE_NAME, image_dir)
339        build_prop_dir = os.path.dirname(build_prop_path)
340        logger.info("Copy %s to %s", _BUILD_PROP_FILE_NAME, build_prop_path)
341        if not os.path.exists(build_prop_dir):
342            os.makedirs(build_prop_dir)
343        shutil.copyfile(build_prop_src_path, build_prop_path)
344
345    @staticmethod
346    def _ReplaceSystemQemuImg(new_image, image_dir):
347        """Replace system-qemu.img in the directory.
348
349        Args:
350            new_image: The path to the new image.
351            image_dir: The directory containing system-qemu.img.
352        """
353        system_qemu_img = os.path.join(image_dir, _SYSTEM_QEMU_IMAGE_NAME)
354        if os.path.exists(system_qemu_img):
355            system_qemu_img_bak = system_qemu_img + _NON_MIXED_BACKUP_IMAGE_EXT
356            if not os.path.exists(system_qemu_img_bak):
357                # If system-qemu.img.bak-non-mixed does not exist, the
358                # system-qemu.img was not created by acloud and should be
359                # preserved. The user can restore it by renaming the backup to
360                # system-qemu.img.
361                logger.info("Rename %s to %s%s.",
362                            system_qemu_img, _SYSTEM_QEMU_IMAGE_NAME,
363                            _NON_MIXED_BACKUP_IMAGE_EXT)
364                os.rename(system_qemu_img, system_qemu_img_bak)
365            else:
366                # The existing system-qemu.img.bak-non-mixed was renamed by
367                # the previous invocation on acloud. The existing
368                # system-qemu.img is a mixed image. Acloud removes the mixed
369                # image because it is large and not reused.
370                os.remove(system_qemu_img)
371        try:
372            logger.info("Link %s to %s.", system_qemu_img, new_image)
373            os.link(new_image, system_qemu_img)
374        except OSError:
375            logger.info("Fail to link. Copy %s to %s",
376                        system_qemu_img, new_image)
377            shutil.copyfile(new_image, system_qemu_img)
378
379    def _ConvertAvdSpecToArgs(self, avd_spec, instance_dir):
380        """Convert AVD spec to emulator arguments.
381
382        Args:
383            avd_spec: AVDSpec object.
384            instance_dir: The instance directory for mixed images.
385
386        Returns:
387            List of strings, the arguments for emulator command.
388        """
389        args = []
390
391        if avd_spec.gpu:
392            args.extend(("-gpu", avd_spec.gpu))
393
394        if not avd_spec.autoconnect:
395            args.append("-no-window")
396
397        if avd_spec.local_system_image_dir:
398            mixed_image_dir = os.path.join(instance_dir, "mixed_images")
399            os.mkdir(mixed_image_dir)
400
401            image_dir = os.path.abspath(avd_spec.local_image_dir)
402
403            ota_tools_dir = ota_tools.FindOtaTools(avd_spec.local_tool_dirs)
404            ota_tools_dir = os.path.abspath(ota_tools_dir)
405
406            mixed_image = self._MixImages(
407                mixed_image_dir, image_dir,
408                os.path.abspath(avd_spec.local_system_image_dir),
409                ota_tools.OtaTools(ota_tools_dir))
410
411            # TODO(b/142228085): Use -system instead of modifying image_dir.
412            self._ReplaceSystemQemuImg(mixed_image, image_dir)
413
414            # Unlock the device so that the disabled vbmeta takes effect.
415            args.extend(("-qemu", "-append",
416                         "androidboot.verifiedbootstate=orange"))
417
418        return args
419
420    @staticmethod
421    def _StartEmulatorProcess(emulator_path, working_dir, image_dir,
422                              console_port, adb_port, extra_args):
423        """Start an emulator process.
424
425        Args:
426            emulator_path: The path to emulator binary.
427            working_dir: The working directory for the emulator process.
428                         The emulator command creates files in the directory.
429            image_dir: The directory containing the required images.
430                       e.g., composite system.img or system-qemu.img.
431            console_port: The console port of the emulator.
432            adb_port: The ADB port of the emulator.
433            extra_args: List of strings, the extra arguments.
434
435        Returns:
436            A Popen object, the emulator process.
437        """
438        emulator_env = os.environ.copy()
439        emulator_env[constants.ENV_ANDROID_PRODUCT_OUT] = image_dir
440        # Set ANDROID_TMP for emulator to create AVD info files in.
441        emulator_env[constants.ENV_ANDROID_TMP] = working_dir
442        # Set ANDROID_BUILD_TOP so that the emulator considers itself to be in
443        # build environment.
444        if constants.ENV_ANDROID_BUILD_TOP not in emulator_env:
445            emulator_env[constants.ENV_ANDROID_BUILD_TOP] = image_dir
446
447        logcat_path = os.path.join(working_dir, "logcat.txt")
448        stdouterr_path = os.path.join(working_dir, "stdouterr.txt")
449        # The command doesn't create -stdouterr-file automatically.
450        with open(stdouterr_path, "w") as _:
451            pass
452
453        emulator_cmd = [
454            os.path.abspath(emulator_path),
455            "-verbose", "-show-kernel", "-read-only",
456            "-ports", str(console_port) + "," + str(adb_port),
457            "-logcat-output", logcat_path,
458            "-stdouterr-file", stdouterr_path
459        ]
460        emulator_cmd.extend(extra_args)
461        logger.debug("Execute %s", emulator_cmd)
462
463        with open(os.devnull, "rb+") as devnull:
464            return subprocess.Popen(
465                emulator_cmd, shell=False, cwd=working_dir, env=emulator_env,
466                stdin=devnull, stdout=devnull, stderr=devnull)
467
468    def _WaitForEmulatorToStop(self, adb):
469        """Wait for an emulator to be unavailable on the console port.
470
471        Args:
472            adb: adb_tools.AdbTools initialized with the emulator's serial.
473
474        Raises:
475            errors.CreateError if the emulator does not stop within timeout.
476        """
477        create_error = errors.CreateError(_EMU_KILL_TIMEOUT_ERROR %
478                                          {"timeout": _EMU_KILL_TIMEOUT_SECS})
479        utils.PollAndWait(func=lambda: self._IsEmulatorRunning(adb),
480                          expected_return=False,
481                          timeout_exception=create_error,
482                          timeout_secs=_EMU_KILL_TIMEOUT_SECS,
483                          sleep_interval_secs=1)
484
485    def _WaitForEmulatorToStart(self, adb, proc, timeout):
486        """Wait for an emulator to be available on the console port.
487
488        Args:
489            adb: adb_tools.AdbTools initialized with the emulator's serial.
490            proc: Popen object, the running emulator process.
491            timeout: Integer, timeout in seconds.
492
493        Raises:
494            errors.DeviceBootTimeoutError if the emulator does not boot within
495            timeout.
496            errors.SubprocessFail if the process terminates.
497        """
498        timeout_error = errors.DeviceBootTimeoutError(_EMULATOR_TIMEOUT_ERROR %
499                                                      {"timeout": timeout})
500        utils.PollAndWait(func=lambda: (proc.poll() is None and
501                                        self._IsEmulatorRunning(adb)),
502                          expected_return=True,
503                          timeout_exception=timeout_error,
504                          timeout_secs=timeout,
505                          sleep_interval_secs=5)
506        if proc.poll() is not None:
507            raise errors.SubprocessFail("Emulator process returned %d." %
508                                        proc.returncode)
509