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