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 contain both the unzipped update package and the unzipped extra image 34 package, i.e., <target>-img-<build>.zip and 35 emu-extra-<os>-system-images-<build>.zip. 36- If the instance requires mixed images, one of the local tool directories 37 should be an unzipped OTA tools package, i.e., otatools.zip. 38""" 39 40import logging 41import os 42import shutil 43import subprocess 44import sys 45 46from acloud import errors 47from acloud.create import base_avd_create 48from acloud.internal import constants 49from acloud.internal.lib import adb_tools 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_SDK_REPO_EMULATOR_DIR_NAME = "emulator" 61_SYSTEM_IMAGE_NAME = "system.img" 62_SYSTEM_QEMU_IMAGE_NAME = "system-qemu.img" 63_NON_MIXED_BACKUP_IMAGE_EXT = ".bak-non-mixed" 64_BUILD_PROP_FILE_NAME = "build.prop" 65_MISC_INFO_FILE_NAME = "misc_info.txt" 66_SYSTEM_QEMU_CONFIG_FILE_NAME = "system-qemu-config.txt" 67 68# Partition names 69_SYSTEM_PARTITION_NAME = "system" 70_SUPER_PARTITION_NAME = "super" 71_VBMETA_PARTITION_NAME = "vbmeta" 72 73# Timeout 74_DEFAULT_EMULATOR_TIMEOUT_SECS = 150 75_EMULATOR_TIMEOUT_ERROR = "Emulator did not boot within %(timeout)d secs." 76_EMU_KILL_TIMEOUT_SECS = 20 77_EMU_KILL_TIMEOUT_ERROR = "Emulator did not stop within %(timeout)d secs." 78 79_CONFIRM_RELAUNCH = ("\nGoldfish AVD is already running. \n" 80 "Enter 'y' to terminate current instance and launch a " 81 "new instance, enter anything else to exit out[y/N]: ") 82 83_MISSING_EMULATOR_MSG = ("Emulator binary is not found. Check " 84 "ANDROID_EMULATOR_PREBUILTS in build environment, " 85 "or set --local-tool to an unzipped SDK emulator " 86 "repository.") 87 88 89def _GetImageForLogicalPartition(partition_name, system_image_path, image_dir): 90 """Map a logical partition name to an image path. 91 92 Args: 93 partition_name: String. On emulator, the logical partitions include 94 "system", "vendor", and "product". 95 system_image_path: String. The path to system image. 96 image_dir: String. The directory containing the other images. 97 98 Returns: 99 system_image_path if the partition is "system". 100 Otherwise, this method returns the path under image_dir. 101 102 Raises 103 errors.GetLocalImageError if the image does not exist. 104 """ 105 if partition_name == _SYSTEM_PARTITION_NAME: 106 image_path = system_image_path 107 else: 108 image_path = os.path.join(image_dir, partition_name + ".img") 109 if not os.path.isfile(image_path): 110 raise errors.GetLocalImageError( 111 "Cannot find image for logical partition %s" % partition_name) 112 return image_path 113 114 115def _GetImageForPhysicalPartition(partition_name, super_image_path, 116 vbmeta_image_path, image_dir): 117 """Map a physical partition name to an image path. 118 119 Args: 120 partition_name: String. On emulator, the physical partitions include 121 "super" and "vbmeta". 122 super_image_path: String. The path to super image. 123 vbmeta_image_path: String. The path to vbmeta image. 124 image_dir: String. The directory containing the other images. 125 126 Returns: 127 super_image_path if the partition is "super". 128 vbmeta_image_path if the partition is "vbmeta". 129 Otherwise, this method returns the path under image_dir. 130 131 Raises: 132 errors.GetLocalImageError if the image does not exist. 133 """ 134 if partition_name == _SUPER_PARTITION_NAME: 135 image_path = super_image_path 136 elif partition_name == _VBMETA_PARTITION_NAME: 137 image_path = vbmeta_image_path 138 else: 139 image_path = os.path.join(image_dir, partition_name + ".img") 140 if not os.path.isfile(image_path): 141 raise errors.GetLocalImageError( 142 "Unexpected physical partition: %s" % partition_name) 143 return image_path 144 145 146class GoldfishLocalImageLocalInstance(base_avd_create.BaseAVDCreate): 147 """Create class for a local image local instance emulator.""" 148 149 def _CreateAVD(self, avd_spec, no_prompts): 150 """Create the AVD. 151 152 Args: 153 avd_spec: AVDSpec object that provides the local image directory. 154 no_prompts: Boolean, True to skip all prompts. 155 156 Returns: 157 A Report instance. 158 159 Raises: 160 errors.GetSdkRepoPackageError if emulator binary is not found. 161 errors.GetLocalImageError if the local image directory does not 162 contain required files. 163 errors.CreateError if an instance exists and cannot be deleted. 164 errors.CheckPathError if OTA tools are not found. 165 """ 166 if not utils.IsSupportedPlatform(print_warning=True): 167 result_report = report.Report(command="create") 168 result_report.SetStatus(report.Status.FAIL) 169 return result_report 170 171 emulator_path = self._FindEmulatorBinary(avd_spec.local_tool_dirs) 172 emulator_path = os.path.abspath(emulator_path) 173 174 image_dir = os.path.abspath(avd_spec.local_image_dir) 175 176 if not (os.path.isfile(os.path.join(image_dir, _SYSTEM_IMAGE_NAME)) or 177 os.path.isfile(os.path.join(image_dir, 178 _SYSTEM_QEMU_IMAGE_NAME))): 179 raise errors.GetLocalImageError("No system image in %s." % 180 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_id = avd_spec.local_instance_id 190 inst = instance.LocalGoldfishInstance(instance_id, 191 avd_flavor=avd_spec.flavor) 192 adb = adb_tools.AdbTools(adb_port=inst.adb_port, 193 device_serial=inst.device_serial) 194 195 self._CheckRunningEmulator(adb, no_prompts) 196 197 instance_dir = inst.instance_dir 198 shutil.rmtree(instance_dir, ignore_errors=True) 199 os.makedirs(instance_dir) 200 201 extra_args = self._ConvertAvdSpecToArgs(avd_spec, instance_dir) 202 203 logger.info("Instance directory: %s", instance_dir) 204 proc = self._StartEmulatorProcess(emulator_path, instance_dir, 205 image_dir, inst.console_port, 206 inst.adb_port, 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(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(inst.name, inst.ip, 216 inst.adb_port, vnc_port=None, 217 error=str(e)) 218 else: 219 result_report.SetStatus(report.Status.SUCCESS) 220 result_report.AddDevice(inst.name, inst.ip, inst.adb_port, 221 vnc_port=None) 222 223 if proc.poll() is None: 224 inst.WriteCreationTimestamp() 225 226 return result_report 227 228 @staticmethod 229 def _MixImages(output_dir, image_dir, system_image_dir, ota): 230 """Mix emulator images and a system image into a disk image. 231 232 Args: 233 output_dir: The path to the output directory. 234 image_dir: The input directory that provides images except 235 system.img. 236 system_image_dir: The input directory that provides system.img. 237 ota: An instance of ota_tools.OtaTools. 238 239 Returns: 240 The path to the mixed disk image in output_dir. 241 """ 242 # Create the super image. 243 mixed_super_image_path = os.path.join(output_dir, "mixed_super.img") 244 system_image_path = os.path.join(system_image_dir, _SYSTEM_IMAGE_NAME) 245 ota.BuildSuperImage(mixed_super_image_path, 246 os.path.join(image_dir, _MISC_INFO_FILE_NAME), 247 lambda partition: _GetImageForLogicalPartition( 248 partition, system_image_path, image_dir)) 249 250 # Create the vbmeta image. 251 disabled_vbmeta_image_path = os.path.join(output_dir, 252 "disabled_vbmeta.img") 253 ota.MakeDisabledVbmetaImage(disabled_vbmeta_image_path) 254 255 # Create the disk image. 256 combined_image = os.path.join(output_dir, "combined.img") 257 ota.MkCombinedImg(combined_image, 258 os.path.join(image_dir, 259 _SYSTEM_QEMU_CONFIG_FILE_NAME), 260 lambda partition: _GetImageForPhysicalPartition( 261 partition, mixed_super_image_path, 262 disabled_vbmeta_image_path, image_dir)) 263 return combined_image 264 265 @staticmethod 266 def _FindEmulatorBinary(search_paths): 267 """Return the path to the emulator binary.""" 268 # Find in unzipped sdk-repo-*.zip. 269 for search_path in search_paths: 270 path = os.path.join(search_path, _EMULATOR_BIN_NAME) 271 if os.path.isfile(path): 272 return path 273 274 path = os.path.join(search_path, _SDK_REPO_EMULATOR_DIR_NAME, 275 _EMULATOR_BIN_NAME) 276 if os.path.isfile(path): 277 return path 278 279 # Find in build environment. 280 prebuilt_emulator_dir = os.environ.get( 281 constants.ENV_ANDROID_EMULATOR_PREBUILTS) 282 if prebuilt_emulator_dir: 283 path = os.path.join(prebuilt_emulator_dir, _EMULATOR_BIN_NAME) 284 if os.path.isfile(path): 285 return path 286 287 raise errors.GetSdkRepoPackageError(_MISSING_EMULATOR_MSG) 288 289 @staticmethod 290 def _IsEmulatorRunning(adb): 291 """Check existence of an emulator by sending an empty command. 292 293 Args: 294 adb: adb_tools.AdbTools initialized with the emulator's serial. 295 296 Returns: 297 Boolean, whether the emulator is running. 298 """ 299 return adb.EmuCommand() == 0 300 301 def _CheckRunningEmulator(self, adb, no_prompts): 302 """Attempt to delete a running emulator. 303 304 Args: 305 adb: adb_tools.AdbTools initialized with the emulator's serial. 306 no_prompts: Boolean, True to skip all prompts. 307 308 Raises: 309 errors.CreateError if the emulator isn't deleted. 310 """ 311 if not self._IsEmulatorRunning(adb): 312 return 313 logger.info("Goldfish AVD is already running.") 314 if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH): 315 if adb.EmuCommand("kill") != 0: 316 raise errors.CreateError("Cannot kill emulator.") 317 self._WaitForEmulatorToStop(adb) 318 else: 319 sys.exit(constants.EXIT_BY_USER) 320 321 @staticmethod 322 def _CopyBuildProp(image_dir): 323 """Copy build.prop to system/build.prop if it doesn't exist. 324 325 Args: 326 image_dir: The directory to find build.prop in. 327 328 Raises: 329 errors.GetLocalImageError if build.prop does not exist. 330 """ 331 build_prop_path = os.path.join(image_dir, "system", 332 _BUILD_PROP_FILE_NAME) 333 if os.path.exists(build_prop_path): 334 return 335 build_prop_src_path = os.path.join(image_dir, _BUILD_PROP_FILE_NAME) 336 if not os.path.isfile(build_prop_src_path): 337 raise errors.GetLocalImageError("No %s in %s." % 338 _BUILD_PROP_FILE_NAME, image_dir) 339 build_prop_dir = os.path.dirname(build_prop_path) 340 logger.info("Copy %s to %s", _BUILD_PROP_FILE_NAME, build_prop_path) 341 if not os.path.exists(build_prop_dir): 342 os.makedirs(build_prop_dir) 343 shutil.copyfile(build_prop_src_path, build_prop_path) 344 345 @staticmethod 346 def _ReplaceSystemQemuImg(new_image, image_dir): 347 """Replace system-qemu.img in the directory. 348 349 Args: 350 new_image: The path to the new image. 351 image_dir: The directory containing system-qemu.img. 352 """ 353 system_qemu_img = os.path.join(image_dir, _SYSTEM_QEMU_IMAGE_NAME) 354 if os.path.exists(system_qemu_img): 355 system_qemu_img_bak = system_qemu_img + _NON_MIXED_BACKUP_IMAGE_EXT 356 if not os.path.exists(system_qemu_img_bak): 357 # If system-qemu.img.bak-non-mixed does not exist, the 358 # system-qemu.img was not created by acloud and should be 359 # preserved. The user can restore it by renaming the backup to 360 # system-qemu.img. 361 logger.info("Rename %s to %s%s.", 362 system_qemu_img, _SYSTEM_QEMU_IMAGE_NAME, 363 _NON_MIXED_BACKUP_IMAGE_EXT) 364 os.rename(system_qemu_img, system_qemu_img_bak) 365 else: 366 # The existing system-qemu.img.bak-non-mixed was renamed by 367 # the previous invocation on acloud. The existing 368 # system-qemu.img is a mixed image. Acloud removes the mixed 369 # image because it is large and not reused. 370 os.remove(system_qemu_img) 371 try: 372 logger.info("Link %s to %s.", system_qemu_img, new_image) 373 os.link(new_image, system_qemu_img) 374 except OSError: 375 logger.info("Fail to link. Copy %s to %s", 376 system_qemu_img, new_image) 377 shutil.copyfile(new_image, system_qemu_img) 378 379 def _ConvertAvdSpecToArgs(self, avd_spec, instance_dir): 380 """Convert AVD spec to emulator arguments. 381 382 Args: 383 avd_spec: AVDSpec object. 384 instance_dir: The instance directory for mixed images. 385 386 Returns: 387 List of strings, the arguments for emulator command. 388 """ 389 args = [] 390 391 if avd_spec.gpu: 392 args.extend(("-gpu", avd_spec.gpu)) 393 394 if not avd_spec.autoconnect: 395 args.append("-no-window") 396 397 if avd_spec.local_system_image_dir: 398 mixed_image_dir = os.path.join(instance_dir, "mixed_images") 399 os.mkdir(mixed_image_dir) 400 401 image_dir = os.path.abspath(avd_spec.local_image_dir) 402 403 ota_tools_dir = ota_tools.FindOtaTools(avd_spec.local_tool_dirs) 404 ota_tools_dir = os.path.abspath(ota_tools_dir) 405 406 mixed_image = self._MixImages( 407 mixed_image_dir, image_dir, 408 os.path.abspath(avd_spec.local_system_image_dir), 409 ota_tools.OtaTools(ota_tools_dir)) 410 411 # TODO(b/142228085): Use -system instead of modifying image_dir. 412 self._ReplaceSystemQemuImg(mixed_image, image_dir) 413 414 # Unlock the device so that the disabled vbmeta takes effect. 415 args.extend(("-qemu", "-append", 416 "androidboot.verifiedbootstate=orange")) 417 418 return args 419 420 @staticmethod 421 def _StartEmulatorProcess(emulator_path, working_dir, image_dir, 422 console_port, adb_port, extra_args): 423 """Start an emulator process. 424 425 Args: 426 emulator_path: The path to emulator binary. 427 working_dir: The working directory for the emulator process. 428 The emulator command creates files in the directory. 429 image_dir: The directory containing the required images. 430 e.g., composite system.img or system-qemu.img. 431 console_port: The console port of the emulator. 432 adb_port: The ADB port of the emulator. 433 extra_args: List of strings, the extra arguments. 434 435 Returns: 436 A Popen object, the emulator process. 437 """ 438 emulator_env = os.environ.copy() 439 emulator_env[constants.ENV_ANDROID_PRODUCT_OUT] = image_dir 440 # Set ANDROID_TMP for emulator to create AVD info files in. 441 emulator_env[constants.ENV_ANDROID_TMP] = working_dir 442 # Set ANDROID_BUILD_TOP so that the emulator considers itself to be in 443 # build environment. 444 if constants.ENV_ANDROID_BUILD_TOP not in emulator_env: 445 emulator_env[constants.ENV_ANDROID_BUILD_TOP] = image_dir 446 447 logcat_path = os.path.join(working_dir, "logcat.txt") 448 stdouterr_path = os.path.join(working_dir, "stdouterr.txt") 449 # The command doesn't create -stdouterr-file automatically. 450 with open(stdouterr_path, "w") as _: 451 pass 452 453 emulator_cmd = [ 454 os.path.abspath(emulator_path), 455 "-verbose", "-show-kernel", "-read-only", 456 "-ports", str(console_port) + "," + str(adb_port), 457 "-logcat-output", logcat_path, 458 "-stdouterr-file", stdouterr_path 459 ] 460 emulator_cmd.extend(extra_args) 461 logger.debug("Execute %s", emulator_cmd) 462 463 with open(os.devnull, "rb+") as devnull: 464 return subprocess.Popen( 465 emulator_cmd, shell=False, cwd=working_dir, env=emulator_env, 466 stdin=devnull, stdout=devnull, stderr=devnull) 467 468 def _WaitForEmulatorToStop(self, adb): 469 """Wait for an emulator to be unavailable on the console port. 470 471 Args: 472 adb: adb_tools.AdbTools initialized with the emulator's serial. 473 474 Raises: 475 errors.CreateError if the emulator does not stop within timeout. 476 """ 477 create_error = errors.CreateError(_EMU_KILL_TIMEOUT_ERROR % 478 {"timeout": _EMU_KILL_TIMEOUT_SECS}) 479 utils.PollAndWait(func=lambda: self._IsEmulatorRunning(adb), 480 expected_return=False, 481 timeout_exception=create_error, 482 timeout_secs=_EMU_KILL_TIMEOUT_SECS, 483 sleep_interval_secs=1) 484 485 def _WaitForEmulatorToStart(self, adb, proc, timeout): 486 """Wait for an emulator to be available on the console port. 487 488 Args: 489 adb: adb_tools.AdbTools initialized with the emulator's serial. 490 proc: Popen object, the running emulator process. 491 timeout: Integer, timeout in seconds. 492 493 Raises: 494 errors.DeviceBootTimeoutError if the emulator does not boot within 495 timeout. 496 errors.SubprocessFail if the process terminates. 497 """ 498 timeout_error = errors.DeviceBootTimeoutError(_EMULATOR_TIMEOUT_ERROR % 499 {"timeout": timeout}) 500 utils.PollAndWait(func=lambda: (proc.poll() is None and 501 self._IsEmulatorRunning(adb)), 502 expected_return=True, 503 timeout_exception=timeout_error, 504 timeout_secs=timeout, 505 sleep_interval_secs=5) 506 if proc.poll() is not None: 507 raise errors.SubprocessFail("Emulator process returned %d." % 508 proc.returncode) 509