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