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