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