• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  be an unzipped extra image package, i.e.,
34  emu-extra-<os>-system-images-<build>.zip.
35- If the instance requires mixed images, one of the local tool directories
36  should be an unzipped OTA tools package, i.e., otatools.zip.
37"""
38
39import logging
40import os
41import shutil
42import subprocess
43import sys
44
45from acloud import errors
46from acloud.create import base_avd_create
47from acloud.create import create_common
48from acloud.internal import constants
49from acloud.internal.lib import ota_tools
50from acloud.internal.lib import utils
51from acloud.list import instance
52from acloud.public import report
53
54
55logger = logging.getLogger(__name__)
56
57# Input and output file names
58_EMULATOR_BIN_NAME = "emulator"
59_EMULATOR_BIN_DIR_NAMES = ("bin64", "qemu")
60_SDK_REPO_EMULATOR_DIR_NAME = "emulator"
61_SYSTEM_IMAGE_NAME = "system.img"
62_SYSTEM_IMAGE_NAME_PATTERN = r"system\.img"
63_SYSTEM_QEMU_IMAGE_NAME = "system-qemu.img"
64_NON_MIXED_BACKUP_IMAGE_EXT = ".bak-non-mixed"
65_BUILD_PROP_FILE_NAME = "build.prop"
66_MISC_INFO_FILE_NAME = "misc_info.txt"
67_SYSTEM_QEMU_CONFIG_FILE_NAME = "system-qemu-config.txt"
68
69# Timeout
70_DEFAULT_EMULATOR_TIMEOUT_SECS = 150
71_EMULATOR_TIMEOUT_ERROR = "Emulator did not boot within %(timeout)d secs."
72_EMU_KILL_TIMEOUT_SECS = 20
73_EMU_KILL_TIMEOUT_ERROR = "Emulator did not stop within %(timeout)d secs."
74
75_CONFIRM_RELAUNCH = ("\nGoldfish AVD is already running. \n"
76                     "Enter 'y' to terminate current instance and launch a "
77                     "new instance, enter anything else to exit out[y/N]: ")
78
79_MISSING_EMULATOR_MSG = ("Emulator binary is not found. Check "
80                         "ANDROID_EMULATOR_PREBUILTS in build environment, "
81                         "or set --local-tool to an unzipped SDK emulator "
82                         "repository.")
83
84_INSTANCES_IN_USE_MSG = ("All instances are in use. Try resetting an instance "
85                         "by specifying --local-instance and an id between 1 "
86                         "and %(max_id)d.")
87
88
89class GoldfishLocalImageLocalInstance(base_avd_create.BaseAVDCreate):
90    """Create class for a local image local instance emulator."""
91
92    def _CreateAVD(self, avd_spec, no_prompts):
93        """Create the AVD.
94
95        Args:
96            avd_spec: AVDSpec object that provides the local image directory.
97            no_prompts: Boolean, True to skip all prompts.
98
99        Returns:
100            A Report instance.
101        """
102        if not utils.IsSupportedPlatform(print_warning=True):
103            result_report = report.Report(command="create")
104            result_report.SetStatus(report.Status.FAIL)
105            return result_report
106
107        try:
108            ins_id, ins_lock = self._LockInstance(avd_spec)
109        except errors.CreateError as e:
110            result_report = report.Report(command="create")
111            result_report.AddError(str(e))
112            result_report.SetStatus(report.Status.FAIL)
113            return result_report
114
115        try:
116            ins = instance.LocalGoldfishInstance(ins_id,
117                                                 avd_flavor=avd_spec.flavor)
118            if not self._CheckRunningEmulator(ins.adb, no_prompts):
119                # Mark as in-use so that it won't be auto-selected again.
120                ins_lock.SetInUse(True)
121                sys.exit(constants.EXIT_BY_USER)
122
123            result_report = self._CreateAVDForInstance(ins, avd_spec)
124            # The infrastructure is able to delete the instance only if the
125            # instance name is reported. This method changes the state to
126            # in-use after creating the report.
127            ins_lock.SetInUse(True)
128            return result_report
129        finally:
130            ins_lock.Unlock()
131
132    @staticmethod
133    def _LockInstance(avd_spec):
134        """Select an id and lock the instance.
135
136        Args:
137            avd_spec: AVDSpec for the device.
138
139        Returns:
140            The instance id and the LocalInstanceLock that is locked by this
141            process.
142
143        Raises:
144            errors.CreateError if fails to select or lock the instance.
145        """
146        if avd_spec.local_instance_id:
147            ins_id = avd_spec.local_instance_id
148            ins_lock = instance.LocalGoldfishInstance.GetLockById(ins_id)
149            if ins_lock.Lock():
150                return ins_id, ins_lock
151            raise errors.CreateError("Instance %d is locked by another "
152                                     "process." % ins_id)
153
154        max_id = instance.LocalGoldfishInstance.GetMaxNumberOfInstances()
155        for ins_id in range(1, max_id + 1):
156            ins_lock = instance.LocalGoldfishInstance.GetLockById(ins_id)
157            if ins_lock.LockIfNotInUse(timeout_secs=0):
158                logger.info("Selected instance id: %d", ins_id)
159                return ins_id, ins_lock
160        raise errors.CreateError(_INSTANCES_IN_USE_MSG % {"max_id": max_id})
161
162    def _CreateAVDForInstance(self, ins, avd_spec):
163        """Create an emulator process for the goldfish instance.
164
165        Args:
166            ins: LocalGoldfishInstance to be initialized.
167            avd_spec: AVDSpec for the device.
168
169        Returns:
170            A Report instance.
171
172        Raises:
173            errors.GetSdkRepoPackageError if emulator binary is not found.
174            errors.GetLocalImageError if the local image directory does not
175            contain required files.
176            errors.CheckPathError if OTA tools are not found.
177        """
178        emulator_path = self._FindEmulatorBinary(avd_spec.local_tool_dirs)
179
180        image_dir = self._FindImageDir(avd_spec.local_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_dir = ins.instance_dir
190        create_common.PrepareLocalInstanceDir(instance_dir, avd_spec)
191
192        extra_args = self._ConvertAvdSpecToArgs(avd_spec, instance_dir)
193
194        logger.info("Instance directory: %s", instance_dir)
195        proc = self._StartEmulatorProcess(emulator_path, instance_dir,
196                                          image_dir, ins.console_port,
197                                          ins.adb_port, extra_args)
198
199        boot_timeout_secs = (avd_spec.boot_timeout_secs or
200                             _DEFAULT_EMULATOR_TIMEOUT_SECS)
201        result_report = report.Report(command="create")
202        try:
203            self._WaitForEmulatorToStart(ins.adb, proc, boot_timeout_secs)
204        except (errors.DeviceBootTimeoutError, errors.SubprocessFail) as e:
205            result_report.SetStatus(report.Status.BOOT_FAIL)
206            result_report.AddDeviceBootFailure(ins.name, ins.ip,
207                                               ins.adb_port, vnc_port=None,
208                                               error=str(e),
209                                               device_serial=ins.device_serial)
210        else:
211            result_report.SetStatus(report.Status.SUCCESS)
212            result_report.AddDevice(ins.name, ins.ip, ins.adb_port,
213                                    vnc_port=None,
214                                    device_serial=ins.device_serial)
215
216        return result_report
217
218    @staticmethod
219    def _MixImages(output_dir, image_dir, system_image_path, ota):
220        """Mix emulator images and a system image into a disk image.
221
222        Args:
223            output_dir: The path to the output directory.
224            image_dir: The input directory that provides images except
225                       system.img.
226            system_image_path: The path to the system image.
227            ota: An instance of ota_tools.OtaTools.
228
229        Returns:
230            The path to the mixed disk image in output_dir.
231        """
232        # Create the super image.
233        mixed_super_image_path = os.path.join(output_dir, "mixed_super.img")
234        ota.BuildSuperImage(
235            mixed_super_image_path,
236            os.path.join(image_dir, _MISC_INFO_FILE_NAME),
237            lambda partition: ota_tools.GetImageForPartition(
238                partition, image_dir, system=system_image_path))
239
240        # Create the vbmeta image.
241        disabled_vbmeta_image_path = os.path.join(output_dir,
242                                                  "disabled_vbmeta.img")
243        ota.MakeDisabledVbmetaImage(disabled_vbmeta_image_path)
244
245        # Create the disk image.
246        combined_image = os.path.join(output_dir, "combined.img")
247        ota.MkCombinedImg(
248            combined_image,
249            os.path.join(image_dir, _SYSTEM_QEMU_CONFIG_FILE_NAME),
250            lambda partition: ota_tools.GetImageForPartition(
251                partition, image_dir, super=mixed_super_image_path,
252                vbmeta=disabled_vbmeta_image_path))
253        return combined_image
254
255    @staticmethod
256    def _FindEmulatorBinary(search_paths):
257        """Find emulator binary in the directories.
258
259        The directories may be extracted from zip archives without preserving
260        file permissions. When this method finds the emulator binary and its
261        dependencies, it sets the files to be executable.
262
263        Args:
264            search_paths: Collection of strings, the directories to search for
265                          emulator binary.
266
267        Returns:
268            The path to the emulator binary.
269
270        Raises:
271            errors.GetSdkRepoPackageError if emulator binary is not found.
272        """
273        emulator_dir = None
274        # Find in unzipped sdk-repo-*.zip.
275        for search_path in search_paths:
276            if os.path.isfile(os.path.join(search_path, _EMULATOR_BIN_NAME)):
277                emulator_dir = search_path
278                break
279
280            sdk_repo_dir = os.path.join(search_path,
281                                        _SDK_REPO_EMULATOR_DIR_NAME)
282            if os.path.isfile(os.path.join(sdk_repo_dir, _EMULATOR_BIN_NAME)):
283                emulator_dir = sdk_repo_dir
284                break
285
286        # Find in build environment.
287        if not emulator_dir:
288            prebuilt_emulator_dir = os.environ.get(
289                constants.ENV_ANDROID_EMULATOR_PREBUILTS)
290            if (prebuilt_emulator_dir and os.path.isfile(
291                    os.path.join(prebuilt_emulator_dir, _EMULATOR_BIN_NAME))):
292                emulator_dir = prebuilt_emulator_dir
293
294        if not emulator_dir:
295            raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG)
296
297        emulator_dir = os.path.abspath(emulator_dir)
298        # Set the binaries to be executable.
299        for subdir_name in _EMULATOR_BIN_DIR_NAMES:
300            subdir_path = os.path.join(emulator_dir, subdir_name)
301            if os.path.isdir(subdir_path):
302                utils.SetDirectoryTreeExecutable(subdir_path)
303
304        emulator_path = os.path.join(emulator_dir, _EMULATOR_BIN_NAME)
305        utils.SetExecutable(emulator_path)
306        return emulator_path
307
308    @staticmethod
309    def _FindImageDir(image_dir):
310        """Find emulator images in the directory.
311
312        In build environment, the images are in $ANDROID_PRODUCT_OUT.
313        In an extracted SDK repository, the images are in the subdirectory
314        named after the CPU architecture.
315
316        Args:
317            image_dir: The path given by the environment variable or the user.
318
319        Returns:
320            The directory containing the emulator images.
321
322        Raises:
323            errors.GetLocalImageError if the images are not found.
324        """
325        image_dir = os.path.abspath(image_dir)
326        entries = os.listdir(image_dir)
327        if len(entries) == 1:
328            first_entry = os.path.join(image_dir, entries[0])
329            if os.path.isdir(first_entry):
330                image_dir = first_entry
331
332        if (os.path.isfile(os.path.join(image_dir, _SYSTEM_QEMU_IMAGE_NAME)) or
333                os.path.isfile(os.path.join(image_dir, _SYSTEM_IMAGE_NAME))):
334            return image_dir
335
336        raise errors.GetLocalImageError("No device image in %s." % image_dir)
337
338    @staticmethod
339    def _IsEmulatorRunning(adb):
340        """Check existence of an emulator by sending an empty command.
341
342        Args:
343            adb: adb_tools.AdbTools initialized with the emulator's serial.
344
345        Returns:
346            Boolean, whether the emulator is running.
347        """
348        return adb.EmuCommand() == 0
349
350    def _CheckRunningEmulator(self, adb, no_prompts):
351        """Attempt to delete a running emulator.
352
353        Args:
354            adb: adb_tools.AdbTools initialized with the emulator's serial.
355            no_prompts: Boolean, True to skip all prompts.
356
357        Returns:
358            Whether the user wants to continue.
359
360        Raises:
361            errors.CreateError if the emulator isn't deleted.
362        """
363        if not self._IsEmulatorRunning(adb):
364            return True
365        logger.info("Goldfish AVD is already running.")
366        if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH):
367            if adb.EmuCommand("kill") != 0:
368                raise errors.CreateError("Cannot kill emulator.")
369            self._WaitForEmulatorToStop(adb)
370            return True
371        return False
372
373    @staticmethod
374    def _CopyBuildProp(image_dir):
375        """Copy build.prop to system/build.prop if it doesn't exist.
376
377        Args:
378            image_dir: The directory to find build.prop in.
379
380        Raises:
381            errors.GetLocalImageError if build.prop does not exist.
382        """
383        build_prop_path = os.path.join(image_dir, "system",
384                                       _BUILD_PROP_FILE_NAME)
385        if os.path.exists(build_prop_path):
386            return
387        build_prop_src_path = os.path.join(image_dir, _BUILD_PROP_FILE_NAME)
388        if not os.path.isfile(build_prop_src_path):
389            raise errors.GetLocalImageError("No %s in %s." %
390                                            (_BUILD_PROP_FILE_NAME, image_dir))
391        build_prop_dir = os.path.dirname(build_prop_path)
392        logger.info("Copy %s to %s", _BUILD_PROP_FILE_NAME, build_prop_path)
393        if not os.path.exists(build_prop_dir):
394            os.makedirs(build_prop_dir)
395        shutil.copyfile(build_prop_src_path, build_prop_path)
396
397    @staticmethod
398    def _ReplaceSystemQemuImg(new_image, image_dir):
399        """Replace system-qemu.img in the directory.
400
401        Args:
402            new_image: The path to the new image.
403            image_dir: The directory containing system-qemu.img.
404        """
405        system_qemu_img = os.path.join(image_dir, _SYSTEM_QEMU_IMAGE_NAME)
406        if os.path.exists(system_qemu_img):
407            system_qemu_img_bak = system_qemu_img + _NON_MIXED_BACKUP_IMAGE_EXT
408            if not os.path.exists(system_qemu_img_bak):
409                # If system-qemu.img.bak-non-mixed does not exist, the
410                # system-qemu.img was not created by acloud and should be
411                # preserved. The user can restore it by renaming the backup to
412                # system-qemu.img.
413                logger.info("Rename %s to %s%s.",
414                            system_qemu_img, _SYSTEM_QEMU_IMAGE_NAME,
415                            _NON_MIXED_BACKUP_IMAGE_EXT)
416                os.rename(system_qemu_img, system_qemu_img_bak)
417            else:
418                # The existing system-qemu.img.bak-non-mixed was renamed by
419                # the previous invocation on acloud. The existing
420                # system-qemu.img is a mixed image. Acloud removes the mixed
421                # image because it is large and not reused.
422                os.remove(system_qemu_img)
423        try:
424            logger.info("Link %s to %s.", system_qemu_img, new_image)
425            os.link(new_image, system_qemu_img)
426        except OSError:
427            logger.info("Fail to link. Copy %s to %s",
428                        system_qemu_img, new_image)
429            shutil.copyfile(new_image, system_qemu_img)
430
431    def _ConvertAvdSpecToArgs(self, avd_spec, instance_dir):
432        """Convert AVD spec to emulator arguments.
433
434        Args:
435            avd_spec: AVDSpec object.
436            instance_dir: The instance directory for mixed images.
437
438        Returns:
439            List of strings, the arguments for emulator command.
440        """
441        args = []
442
443        if avd_spec.gpu:
444            args.extend(("-gpu", avd_spec.gpu))
445
446        if not avd_spec.autoconnect:
447            args.append("-no-window")
448
449        if avd_spec.local_system_image:
450            mixed_image_dir = os.path.join(instance_dir, "mixed_images")
451            if os.path.exists(mixed_image_dir):
452                shutil.rmtree(mixed_image_dir)
453            os.mkdir(mixed_image_dir)
454
455            image_dir = self._FindImageDir(avd_spec.local_image_dir)
456
457            system_image_path = create_common.FindLocalImage(
458                avd_spec.local_system_image, _SYSTEM_IMAGE_NAME_PATTERN)
459
460            ota_tools_dir = ota_tools.FindOtaTools(avd_spec.local_tool_dirs)
461            ota_tools_dir = os.path.abspath(ota_tools_dir)
462
463            mixed_image = self._MixImages(
464                mixed_image_dir, image_dir, system_image_path,
465                ota_tools.OtaTools(ota_tools_dir))
466
467            # TODO(b/142228085): Use -system instead of modifying image_dir.
468            self._ReplaceSystemQemuImg(mixed_image, image_dir)
469
470            # Unlock the device so that the disabled vbmeta takes effect.
471            args.extend(("-qemu", "-append",
472                         "androidboot.verifiedbootstate=orange"))
473
474        return args
475
476    @staticmethod
477    def _StartEmulatorProcess(emulator_path, working_dir, image_dir,
478                              console_port, adb_port, extra_args):
479        """Start an emulator process.
480
481        Args:
482            emulator_path: The path to emulator binary.
483            working_dir: The working directory for the emulator process.
484                         The emulator command creates files in the directory.
485            image_dir: The directory containing the required images.
486                       e.g., composite system.img or system-qemu.img.
487            console_port: The console port of the emulator.
488            adb_port: The ADB port of the emulator.
489            extra_args: List of strings, the extra arguments.
490
491        Returns:
492            A Popen object, the emulator process.
493        """
494        emulator_env = os.environ.copy()
495        emulator_env[constants.ENV_ANDROID_PRODUCT_OUT] = image_dir
496        # Set ANDROID_TMP for emulator to create AVD info files in.
497        emulator_env[constants.ENV_ANDROID_TMP] = working_dir
498        # Set ANDROID_BUILD_TOP so that the emulator considers itself to be in
499        # build environment.
500        if constants.ENV_ANDROID_BUILD_TOP not in emulator_env:
501            emulator_env[constants.ENV_ANDROID_BUILD_TOP] = image_dir
502
503        logcat_path = os.path.join(working_dir, "logcat.txt")
504        stdouterr_path = os.path.join(working_dir, "stdouterr.txt")
505        # The command doesn't create -stdouterr-file automatically.
506        with open(stdouterr_path, "w") as _:
507            pass
508
509        emulator_cmd = [
510            os.path.abspath(emulator_path),
511            "-verbose", "-show-kernel", "-read-only",
512            "-ports", str(console_port) + "," + str(adb_port),
513            "-logcat-output", logcat_path,
514            "-stdouterr-file", stdouterr_path
515        ]
516        emulator_cmd.extend(extra_args)
517        logger.debug("Execute %s", emulator_cmd)
518
519        with open(os.devnull, "rb+") as devnull:
520            return subprocess.Popen(
521                emulator_cmd, shell=False, cwd=working_dir, env=emulator_env,
522                stdin=devnull, stdout=devnull, stderr=devnull)
523
524    def _WaitForEmulatorToStop(self, adb):
525        """Wait for an emulator to be unavailable on the console port.
526
527        Args:
528            adb: adb_tools.AdbTools initialized with the emulator's serial.
529
530        Raises:
531            errors.CreateError if the emulator does not stop within timeout.
532        """
533        create_error = errors.CreateError(_EMU_KILL_TIMEOUT_ERROR %
534                                          {"timeout": _EMU_KILL_TIMEOUT_SECS})
535        utils.PollAndWait(func=lambda: self._IsEmulatorRunning(adb),
536                          expected_return=False,
537                          timeout_exception=create_error,
538                          timeout_secs=_EMU_KILL_TIMEOUT_SECS,
539                          sleep_interval_secs=1)
540
541    def _WaitForEmulatorToStart(self, adb, proc, timeout):
542        """Wait for an emulator to be available on the console port.
543
544        Args:
545            adb: adb_tools.AdbTools initialized with the emulator's serial.
546            proc: Popen object, the running emulator process.
547            timeout: Integer, timeout in seconds.
548
549        Raises:
550            errors.DeviceBootTimeoutError if the emulator does not boot within
551            timeout.
552            errors.SubprocessFail if the process terminates.
553        """
554        timeout_error = errors.DeviceBootTimeoutError(_EMULATOR_TIMEOUT_ERROR %
555                                                      {"timeout": timeout})
556        utils.PollAndWait(func=lambda: (proc.poll() is None and
557                                        self._IsEmulatorRunning(adb)),
558                          expected_return=True,
559                          timeout_exception=timeout_error,
560                          timeout_secs=timeout,
561                          sleep_interval_secs=5)
562        if proc.poll() is not None:
563            raise errors.SubprocessFail("Emulator process returned %d." %
564                                        proc.returncode)
565