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