1#!/usr/bin/env python 2# 3# Copyright 2018 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16r"""LocalImageLocalInstance class. 17 18Create class that is responsible for creating a local instance AVD with a 19local image. For launching multiple local instances under the same user, 20The cuttlefish tool requires 3 variables: 21- ANDROID_HOST_OUT: To locate the launch_cvd tool. 22- HOME: To specify the temporary folder of launch_cvd. 23- CUTTLEFISH_INSTANCE: To specify the instance id. 24Acloud user must either set ANDROID_HOST_OUT or run acloud with --local-tool. 25Acloud sets the other 2 variables for each local instance. 26 27The adb port and vnc port of local instance will be decided according to 28instance id. The rule of adb port will be '6520 + [instance id] - 1' and the vnc 29port will be '6444 + [instance id] - 1'. 30e.g: 31If instance id = 3 the adb port will be 6522 and vnc port will be 6446. 32 33To delete the local instance, we will call stop_cvd with the environment variable 34[CUTTLEFISH_CONFIG_FILE] which is pointing to the runtime cuttlefish json. 35""" 36 37import logging 38import os 39import shutil 40import subprocess 41import threading 42import sys 43 44from acloud import errors 45from acloud.create import base_avd_create 46from acloud.internal import constants 47from acloud.internal.lib import utils 48from acloud.internal.lib.adb_tools import AdbTools 49from acloud.list import list as list_instance 50from acloud.list import instance 51from acloud.public import report 52 53 54logger = logging.getLogger(__name__) 55 56_CMD_LAUNCH_CVD_ARGS = (" -daemon -cpus %s -x_res %s -y_res %s -dpi %s " 57 "-memory_mb %s -run_adb_connector=%s " 58 "-system_image_dir %s -instance_dir %s") 59_CMD_LAUNCH_CVD_DISK_ARGS = (" -blank_data_image_mb %s " 60 "-data_policy always_create") 61_CONFIRM_RELAUNCH = ("\nCuttlefish AVD[id:%d] is already running. \n" 62 "Enter 'y' to terminate current instance and launch a new " 63 "instance, enter anything else to exit out[y/N]: ") 64_LAUNCH_CVD_TIMEOUT_SECS = 120 # default timeout as 120 seconds 65_LAUNCH_CVD_TIMEOUT_ERROR = ("Cuttlefish AVD launch timeout, did not complete " 66 "within %d secs.") 67_VIRTUAL_DISK_PATHS = "virtual_disk_paths" 68 69 70class LocalImageLocalInstance(base_avd_create.BaseAVDCreate): 71 """Create class for a local image local instance AVD.""" 72 73 @utils.TimeExecute(function_description="Total time: ", 74 print_before_call=False, print_status=False) 75 def _CreateAVD(self, avd_spec, no_prompts): 76 """Create the AVD. 77 78 Args: 79 avd_spec: AVDSpec object that tells us what we're going to create. 80 no_prompts: Boolean, True to skip all prompts. 81 82 Raises: 83 errors.LaunchCVDFail: Launch AVD failed. 84 85 Returns: 86 A Report instance. 87 """ 88 # Running instances on local is not supported on all OS. 89 if not utils.IsSupportedPlatform(print_warning=True): 90 result_report = report.Report(command="create") 91 result_report.SetStatus(report.Status.FAIL) 92 return result_report 93 94 self.PrintDisclaimer() 95 local_image_path, host_bins_path = self.GetImageArtifactsPath(avd_spec) 96 97 launch_cvd_path = os.path.join(host_bins_path, "bin", 98 constants.CMD_LAUNCH_CVD) 99 cmd = self.PrepareLaunchCVDCmd(launch_cvd_path, 100 avd_spec.hw_property, 101 avd_spec.connect_adb, 102 local_image_path, 103 avd_spec.local_instance_id) 104 105 result_report = report.Report(command="create") 106 instance_name = instance.GetLocalInstanceName( 107 avd_spec.local_instance_id) 108 try: 109 self.CheckLaunchCVD( 110 cmd, host_bins_path, avd_spec.local_instance_id, local_image_path, 111 no_prompts, avd_spec.boot_timeout_secs or _LAUNCH_CVD_TIMEOUT_SECS) 112 except errors.LaunchCVDFail as launch_error: 113 result_report.SetStatus(report.Status.BOOT_FAIL) 114 result_report.AddDeviceBootFailure( 115 instance_name, constants.LOCALHOST, None, None, 116 error=str(launch_error)) 117 return result_report 118 119 active_ins = list_instance.GetActiveCVD(avd_spec.local_instance_id) 120 if active_ins: 121 result_report.SetStatus(report.Status.SUCCESS) 122 result_report.AddDevice(instance_name, constants.LOCALHOST, 123 active_ins.adb_port, active_ins.vnc_port) 124 # Launch vnc client if we're auto-connecting. 125 if avd_spec.connect_vnc: 126 utils.LaunchVNCFromReport(result_report, avd_spec, no_prompts) 127 if avd_spec.unlock_screen: 128 AdbTools(active_ins.adb_port).AutoUnlockScreen() 129 else: 130 err_msg = "cvd_status return non-zero after launch_cvd" 131 logger.error(err_msg) 132 result_report.SetStatus(report.Status.BOOT_FAIL) 133 result_report.AddDeviceBootFailure( 134 instance_name, constants.LOCALHOST, None, None, error=err_msg) 135 136 return result_report 137 138 @staticmethod 139 def _FindCvdHostBinaries(search_paths): 140 """Return the directory that contains CVD host binaries.""" 141 for search_path in search_paths: 142 if os.path.isfile(os.path.join(search_path, "bin", 143 constants.CMD_LAUNCH_CVD)): 144 return search_path 145 146 host_out_dir = os.environ.get(constants.ENV_ANDROID_HOST_OUT) 147 if (host_out_dir and 148 os.path.isfile(os.path.join(host_out_dir, "bin", 149 constants.CMD_LAUNCH_CVD))): 150 return host_out_dir 151 152 raise errors.GetCvdLocalHostPackageError( 153 "CVD host binaries are not found. Please run `make hosttar`, or " 154 "set --local-tool to an extracted CVD host package.") 155 156 def GetImageArtifactsPath(self, avd_spec): 157 """Get image artifacts path. 158 159 This method will check if launch_cvd is exist and return the tuple path 160 (image path and host bins path) where they are located respectively. 161 For remote image, RemoteImageLocalInstance will override this method and 162 return the artifacts path which is extracted and downloaded from remote. 163 164 Args: 165 avd_spec: AVDSpec object that tells us what we're going to create. 166 167 Returns: 168 Tuple of (local image file, host bins package) paths. 169 """ 170 return (avd_spec.local_image_dir, 171 self._FindCvdHostBinaries(avd_spec.local_tool_dirs)) 172 173 @staticmethod 174 def PrepareLaunchCVDCmd(launch_cvd_path, hw_property, connect_adb, 175 system_image_dir, local_instance_id): 176 """Prepare launch_cvd command. 177 178 Create the launch_cvd commands with all the required args and add 179 in the user groups to it if necessary. 180 181 Args: 182 launch_cvd_path: String of launch_cvd path. 183 hw_property: dict object of hw property. 184 system_image_dir: String of local images path. 185 connect_adb: Boolean flag that enables adb_connector. 186 local_instance_id: Integer of instance id. 187 188 Returns: 189 String, launch_cvd cmd. 190 """ 191 instance_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id) 192 launch_cvd_w_args = launch_cvd_path + _CMD_LAUNCH_CVD_ARGS % ( 193 hw_property["cpu"], hw_property["x_res"], hw_property["y_res"], 194 hw_property["dpi"], hw_property["memory"], 195 ("true" if connect_adb else "false"), system_image_dir, 196 instance_dir) 197 if constants.HW_ALIAS_DISK in hw_property: 198 launch_cvd_w_args = (launch_cvd_w_args + _CMD_LAUNCH_CVD_DISK_ARGS % 199 hw_property[constants.HW_ALIAS_DISK]) 200 201 launch_cmd = utils.AddUserGroupsToCmd(launch_cvd_w_args, 202 constants.LIST_CF_USER_GROUPS) 203 logger.debug("launch_cvd cmd:\n %s", launch_cmd) 204 return launch_cmd 205 206 def CheckLaunchCVD(self, cmd, host_bins_path, local_instance_id, 207 local_image_path, no_prompts=False, 208 timeout_secs=_LAUNCH_CVD_TIMEOUT_SECS): 209 """Execute launch_cvd command and wait for boot up completed. 210 211 1. Check if the provided image files are in use by any launch_cvd process. 212 2. Check if launch_cvd with the same instance id is running. 213 3. Launch local AVD. 214 215 Args: 216 cmd: String, launch_cvd command. 217 host_bins_path: String of host package directory. 218 local_instance_id: Integer of instance id. 219 local_image_path: String of local image directory. 220 no_prompts: Boolean, True to skip all prompts. 221 timeout_secs: Integer, the number of seconds to wait for the AVD to boot up. 222 """ 223 # launch_cvd assumes host bins are in $ANDROID_HOST_OUT, let's overwrite 224 # it to wherever we're running launch_cvd since they could be in a 225 # different dir (e.g. downloaded image). 226 os.environ[constants.ENV_ANDROID_HOST_OUT] = host_bins_path 227 # Check if the instance with same id is running. 228 existing_ins = list_instance.GetActiveCVD(local_instance_id) 229 if existing_ins: 230 if no_prompts or utils.GetUserAnswerYes(_CONFIRM_RELAUNCH % 231 local_instance_id): 232 existing_ins.Delete() 233 else: 234 sys.exit(constants.EXIT_BY_USER) 235 else: 236 # Image files can't be shared among instances, so check if any running 237 # launch_cvd process is using this path. 238 occupied_ins_id = self.IsLocalImageOccupied(local_image_path) 239 if occupied_ins_id: 240 utils.PrintColorString( 241 "The image path[%s] is already used by current running AVD" 242 "[id:%d]\nPlease choose another path to launch local " 243 "instance." % (local_image_path, occupied_ins_id), 244 utils.TextColors.FAIL) 245 sys.exit(constants.EXIT_BY_USER) 246 247 self._LaunchCvd(cmd, local_instance_id, timeout=timeout_secs) 248 249 @staticmethod 250 @utils.TimeExecute(function_description="Waiting for AVD(s) to boot up") 251 def _LaunchCvd(cmd, local_instance_id, timeout=None): 252 """Execute Launch CVD. 253 254 Kick off the launch_cvd command and log the output. 255 256 Args: 257 cmd: String, launch_cvd command. 258 local_instance_id: Integer of instance id. 259 timeout: Integer, the number of seconds to wait for the AVD to boot up. 260 261 Raises: 262 errors.LaunchCVDFail when any CalledProcessError. 263 """ 264 # Delete the cvd home/runtime temp if exist. The runtime folder is 265 # under the cvd home dir, so we only delete them from home dir. 266 cvd_home_dir = instance.GetLocalInstanceHomeDir(local_instance_id) 267 cvd_runtime_dir = instance.GetLocalInstanceRuntimeDir(local_instance_id) 268 shutil.rmtree(cvd_home_dir, ignore_errors=True) 269 os.makedirs(cvd_runtime_dir) 270 271 cvd_env = os.environ.copy() 272 cvd_env[constants.ENV_CVD_HOME] = cvd_home_dir 273 cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(local_instance_id) 274 # Check the result of launch_cvd command. 275 # An exit code of 0 is equivalent to VIRTUAL_DEVICE_BOOT_COMPLETED 276 process = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT, 277 env=cvd_env) 278 if timeout: 279 timer = threading.Timer(timeout, process.kill) 280 timer.start() 281 process.wait() 282 if timeout: 283 timer.cancel() 284 if process.returncode == 0: 285 return 286 raise errors.LaunchCVDFail( 287 "Can't launch cuttlefish AVD. Return code:%s. \nFor more detail: " 288 "%s/launcher.log" % (str(process.returncode), cvd_runtime_dir)) 289 290 @staticmethod 291 def PrintDisclaimer(): 292 """Print Disclaimer.""" 293 utils.PrintColorString( 294 "(Disclaimer: Local cuttlefish instance is not a fully supported\n" 295 "runtime configuration, fixing breakages is on a best effort SLO.)\n", 296 utils.TextColors.WARNING) 297 298 @staticmethod 299 def IsLocalImageOccupied(local_image_dir): 300 """Check if the given image path is being used by a running CVD process. 301 302 Args: 303 local_image_dir: String, path of local image. 304 305 Return: 306 Integer of instance id which using the same image path. 307 """ 308 # TODO(149602560): Remove occupied image checking after after cf disk 309 # overlay is stable 310 for cf_runtime_config_path in instance.GetAllLocalInstanceConfigs(): 311 ins = instance.LocalInstance(cf_runtime_config_path) 312 if ins.CvdStatus(): 313 for disk_path in ins.virtual_disk_paths: 314 if local_image_dir in disk_path: 315 return ins.instance_id 316 return None 317