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