1# Copyright 2018 - 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"""Instance class. 15 16Define the instance class used to hold details about an AVD instance. 17 18The instance class will hold details about AVD instances (remote/local) used to 19enable users to understand what instances they've created. This will be leveraged 20for the list, delete, and reconnect commands. 21 22The details include: 23- instance name (for remote instances) 24- creation date/instance duration 25- instance image details (branch/target/build id) 26- and more! 27""" 28 29import collections 30import datetime 31import json 32import logging 33import os 34import re 35import subprocess 36import tempfile 37 38# pylint: disable=import-error 39import dateutil.parser 40import dateutil.tz 41 42from acloud.create import local_image_local_instance 43from acloud.internal import constants 44from acloud.internal.lib import cvd_runtime_config 45from acloud.internal.lib import utils 46from acloud.internal.lib.adb_tools import AdbTools 47from acloud.internal.lib.local_instance_lock import LocalInstanceLock 48from acloud.internal.lib.gcompute_client import GetInstanceIP 49 50 51logger = logging.getLogger(__name__) 52 53_ACLOUD_CVD_TEMP = os.path.join(tempfile.gettempdir(), "acloud_cvd_temp") 54_CVD_CONFIG_FOLDER = "%(cvd_runtime)s/instances/cvd-%(id)d" 55_CVD_LOG_FOLDER = _CVD_CONFIG_FOLDER + "/logs" 56_CVD_RUNTIME_FOLDER_NAME = "cuttlefish_runtime" 57_CVD_BIN = "cvd" 58_CVD_BIN_FOLDER = "host_bins/bin" 59_CVD_STATUS_BIN = "cvd_status" 60_CVD_SERVER = "cvd_server" 61_CVD_STOP_ERROR_KEYWORDS = "cvd_internal_stop E" 62# Default timeout 30 secs for cvd commands. 63_CVD_TIMEOUT = 30 64_INSTANCE_ASSEMBLY_DIR = "cuttlefish_assembly" 65_LOCAL_INSTANCE_NAME_FORMAT = "local-instance-%(id)d" 66_LOCAL_INSTANCE_NAME_PATTERN = re.compile(r"^local-instance-(?P<id>\d+)$") 67_ACLOUDWEB_INSTANCE_START_STRING = "cf-" 68_MSG_UNABLE_TO_CALCULATE = "Unable to calculate" 69_NO_ANDROID_ENV = "android source not available" 70_RE_GROUP_ADB = "local_adb_port" 71_RE_GROUP_VNC = "local_vnc_port" 72_RE_SSH_TUNNEL_PATTERN = (r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)" 73 r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)" 74 r"(.+%s)") 75_RE_TIMEZONE = re.compile(r"^(?P<time>[0-9\-\.:T]*)(?P<timezone>[+-]\d+:\d+)$") 76_RE_DEVICE_INFO = re.compile(r"(?s).*(?P<device_info>[{][\s\w\W]+})") 77 78_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"] 79_RE_RUN_CVD = re.compile(r"(?P<date_str>^[^/]+)(.*run_cvd)") 80_DISPLAY_STRING = "%(x_res)sx%(y_res)s (%(dpi)s)" 81_RE_ZONE = re.compile(r".+/zones/(?P<zone>.+)$") 82_LOCAL_ZONE = "local" 83_FULL_NAME_STRING = ("device serial: %(device_serial)s (%(instance_name)s) " 84 "elapsed time: %(elapsed_time)s") 85_INDENT = " " * 3 86LocalPorts = collections.namedtuple("LocalPorts", [constants.VNC_PORT, 87 constants.ADB_PORT]) 88 89 90def GetDefaultCuttlefishConfig(): 91 """Get the path of default cuttlefish instance config. 92 93 Return: 94 String, path of cf runtime config. 95 """ 96 cfg_path = os.path.join(os.path.expanduser("~"), _CVD_RUNTIME_FOLDER_NAME, 97 constants.CUTTLEFISH_CONFIG_FILE) 98 if os.path.isfile(cfg_path): 99 return cfg_path 100 return None 101 102 103def GetLocalInstanceName(local_instance_id): 104 """Get local cuttlefish instance name by instance id. 105 106 Args: 107 local_instance_id: Integer of instance id. 108 109 Return: 110 String, the instance name. 111 """ 112 return _LOCAL_INSTANCE_NAME_FORMAT % {"id": local_instance_id} 113 114 115def GetLocalInstanceIdByName(name): 116 """Get local cuttlefish instance id by name. 117 118 Args: 119 name: String of instance name. 120 121 Return: 122 The instance id as an integer if the name is in valid format. 123 None if the name does not represent a local cuttlefish instance. 124 """ 125 match = _LOCAL_INSTANCE_NAME_PATTERN.match(name) 126 if match: 127 return int(match.group("id")) 128 return None 129 130 131def GetLocalInstanceConfigPath(local_instance_id): 132 """Get the path of instance config. 133 134 Args: 135 local_instance_id: Integer of instance id. 136 137 Return: 138 String, path of cf runtime config. 139 """ 140 ins_assembly_dir = os.path.join(GetLocalInstanceHomeDir(local_instance_id), 141 _INSTANCE_ASSEMBLY_DIR) 142 return os.path.join(ins_assembly_dir, constants.CUTTLEFISH_CONFIG_FILE) 143 144 145def GetLocalInstanceConfig(local_instance_id): 146 """Get the path of existed config from local instance. 147 148 Args: 149 local_instance_id: Integer of instance id. 150 151 Return: 152 String, path of cf runtime config. None for config not exist. 153 """ 154 cfg_path = GetLocalInstanceConfigPath(local_instance_id) 155 if os.path.isfile(cfg_path): 156 return cfg_path 157 return None 158 159 160def GetAllLocalInstanceConfigs(): 161 """Get all cuttlefish runtime configs from the known locations. 162 163 Return: 164 List of tuples. Each tuple consists of an instance id and a config 165 path. 166 """ 167 id_cfg_pairs = [] 168 # Check if any instance config is under home folder. 169 cfg_path = GetDefaultCuttlefishConfig() 170 if cfg_path: 171 id_cfg_pairs.append((1, cfg_path)) 172 173 # Check if any instance config is under acloud cvd temp folder. 174 if os.path.exists(_ACLOUD_CVD_TEMP): 175 for ins_name in os.listdir(_ACLOUD_CVD_TEMP): 176 ins_id = GetLocalInstanceIdByName(ins_name) 177 if ins_id is not None: 178 cfg_path = GetLocalInstanceConfig(ins_id) 179 if cfg_path: 180 id_cfg_pairs.append((ins_id, cfg_path)) 181 return id_cfg_pairs 182 183 184def GetLocalInstanceHomeDir(local_instance_id): 185 """Get local instance home dir according to instance id. 186 187 Args: 188 local_instance_id: Integer of instance id. 189 190 Return: 191 String, path of instance home dir. 192 """ 193 return os.path.join(_ACLOUD_CVD_TEMP, 194 GetLocalInstanceName(local_instance_id)) 195 196 197def GetLocalInstanceLock(local_instance_id): 198 """Get local instance lock. 199 200 Args: 201 local_instance_id: Integer of instance id. 202 203 Returns: 204 LocalInstanceLock object. 205 """ 206 file_path = os.path.join(_ACLOUD_CVD_TEMP, 207 GetLocalInstanceName(local_instance_id) + ".lock") 208 return LocalInstanceLock(file_path) 209 210 211def GetLocalInstanceRuntimeDir(local_instance_id): 212 """Get instance runtime dir 213 214 Args: 215 local_instance_id: Integer of instance id. 216 217 Return: 218 String, path of instance runtime dir. 219 """ 220 return os.path.join(GetLocalInstanceHomeDir(local_instance_id), 221 _CVD_RUNTIME_FOLDER_NAME) 222 223 224def GetLocalInstanceLogDir(local_instance_id): 225 """Get local instance log directory. 226 227 Cuttlefish log directories are different between versions: 228 229 In Android 10, the logs are in `<runtime_dir>`. 230 231 In Android 11, the logs are in `<runtime_dir>.<id>`. 232 `<runtime_dir>` is a symbolic link to `<runtime_dir>.<id>`. 233 234 In the latest version, the logs are in 235 `<runtime_dir>/instances/cvd-<id>/logs`. 236 `<runtime_dir>_runtime` and `<runtime_dir>.<id>` are symbolic links to 237 `<runtime_dir>/instances/cvd-<id>`. 238 239 This method looks for `<runtime_dir>/instances/cvd-<id>/logs` which is the 240 latest known location. If it doesn't exist, this method returns 241 `<runtime_dir>` which is compatible with the old versions. 242 243 Args: 244 local_instance_id: Integer of instance id. 245 246 Returns: 247 The path to the log directory. 248 """ 249 runtime_dir = GetLocalInstanceRuntimeDir(local_instance_id) 250 log_dir = _CVD_LOG_FOLDER % {"cvd_runtime": runtime_dir, 251 "id": local_instance_id} 252 return log_dir if os.path.isdir(log_dir) else runtime_dir 253 254 255def _GetCurrentLocalTime(): 256 """Return a datetime object for current time in local time zone.""" 257 return datetime.datetime.now(dateutil.tz.tzlocal()) 258 259 260def _GetElapsedTime(start_time): 261 """Calculate the elapsed time from start_time till now. 262 263 Args: 264 start_time: String of instance created time. 265 266 Returns: 267 datetime.timedelta of elapsed time, _MSG_UNABLE_TO_CALCULATE for 268 datetime can't parse cases. 269 """ 270 match = _RE_TIMEZONE.match(start_time) 271 try: 272 # Check start_time has timezone or not. If timezone can't be found, 273 # use local timezone to get elapsed time. 274 if match: 275 return _GetCurrentLocalTime() - dateutil.parser.parse(start_time) 276 277 return _GetCurrentLocalTime() - dateutil.parser.parse( 278 start_time).replace(tzinfo=dateutil.tz.tzlocal()) 279 except ValueError: 280 logger.debug(("Can't parse datetime string(%s)."), start_time) 281 return _MSG_UNABLE_TO_CALCULATE 282 283def _IsProcessRunning(process): 284 """Check if this process is running. 285 286 Returns: 287 Boolean, True for this process is running. 288 """ 289 match_pattern = re.compile(f"(.+)({process} )(.+)") 290 process_output = utils.CheckOutput(constants.COMMAND_PS) 291 for line in process_output.splitlines(): 292 process_match = match_pattern.match(line) 293 if process_match: 294 return True 295 return False 296 297 298# pylint: disable=useless-object-inheritance 299class Instance(object): 300 """Class to store data of instance.""" 301 302 # pylint: disable=too-many-locals 303 def __init__(self, name, fullname, display, ip, status=None, adb_port=None, 304 vnc_port=None, ssh_tunnel_is_connected=None, createtime=None, 305 elapsed_time=None, avd_type=None, avd_flavor=None, 306 is_local=False, device_information=None, zone=None, 307 webrtc_port=None, webrtc_forward_port=None): 308 self._name = name 309 self._fullname = fullname 310 self._status = status 311 self._display = display # Resolution and dpi 312 self._ip = ip 313 self._adb_port = adb_port # adb port which is forwarding to remote 314 self._vnc_port = vnc_port # vnc port which is forwarding to remote 315 self._webrtc_port = webrtc_port 316 self._webrtc_forward_port = webrtc_forward_port 317 # True if ssh tunnel is still connected 318 self._ssh_tunnel_is_connected = ssh_tunnel_is_connected 319 self._createtime = createtime 320 self._elapsed_time = elapsed_time 321 self._avd_type = avd_type 322 self._avd_flavor = avd_flavor 323 self._is_local = is_local # True if this is a local instance 324 self._device_information = device_information 325 self._zone = zone 326 self._autoconnect = self._GetAutoConnect() 327 328 def __repr__(self): 329 """Return full name property for print.""" 330 return self._fullname 331 332 def Summary(self): 333 """Let's make it easy to see what this class is holding.""" 334 representation = [] 335 representation.append(" name: %s" % self._name) 336 representation.append("%s IP: %s" % (_INDENT, self._ip)) 337 representation.append("%s create time: %s" % (_INDENT, self._createtime)) 338 representation.append("%s elapse time: %s" % (_INDENT, self._elapsed_time)) 339 representation.append("%s status: %s" % (_INDENT, self._status)) 340 representation.append("%s avd type: %s" % (_INDENT, self._avd_type)) 341 representation.append("%s display: %s" % (_INDENT, self._display)) 342 representation.append("%s vnc: 127.0.0.1:%s" % (_INDENT, self._vnc_port)) 343 representation.append("%s zone: %s" % (_INDENT, self._zone)) 344 representation.append("%s autoconnect: %s" % (_INDENT, self._autoconnect)) 345 representation.append("%s webrtc port: %s" % (_INDENT, self._webrtc_port)) 346 representation.append("%s webrtc forward port: %s" % 347 (_INDENT, self._webrtc_forward_port)) 348 349 if self._adb_port and self._device_information: 350 serial_ip = self._ip if self._ip == "0.0.0.0" else "127.0.0.1" 351 representation.append("%s adb serial: %s:%s" % 352 (_INDENT, serial_ip, self._adb_port)) 353 representation.append("%s product: %s" % ( 354 _INDENT, self._device_information["product"])) 355 representation.append("%s model: %s" % ( 356 _INDENT, self._device_information["model"])) 357 representation.append("%s device: %s" % ( 358 _INDENT, self._device_information["device"])) 359 representation.append("%s transport_id: %s" % ( 360 _INDENT, self._device_information["transport_id"])) 361 else: 362 representation.append("%s adb serial: disconnected" % _INDENT) 363 364 return "\n".join(representation) 365 366 def AdbConnected(self): 367 """Check AVD adb connected. 368 369 Returns: 370 Boolean, True when adb status of AVD is connected. 371 """ 372 if self._adb_port and self._device_information: 373 return True 374 return False 375 376 def _GetAutoConnect(self): 377 """Get the autoconnect of instance. 378 379 Returns: 380 String of autoconnect type. None for no autoconnect. 381 """ 382 if self._webrtc_port or self._webrtc_forward_port: 383 return constants.INS_KEY_WEBRTC 384 if self._vnc_port: 385 return constants.INS_KEY_VNC 386 if self._adb_port: 387 return constants.INS_KEY_ADB 388 return None 389 390 @property 391 def name(self): 392 """Return the instance name.""" 393 return self._name 394 395 @property 396 def fullname(self): 397 """Return the instance full name.""" 398 return self._fullname 399 400 @property 401 def ip(self): 402 """Return the ip.""" 403 return self._ip 404 405 @property 406 def status(self): 407 """Return status.""" 408 return self._status 409 410 @property 411 def display(self): 412 """Return display.""" 413 return self._display 414 415 @property 416 def ssh_tunnel_is_connected(self): 417 """Return the connect status.""" 418 return self._ssh_tunnel_is_connected 419 420 @property 421 def createtime(self): 422 """Return create time.""" 423 return self._createtime 424 425 @property 426 def avd_type(self): 427 """Return avd_type.""" 428 return self._avd_type 429 430 @property 431 def avd_flavor(self): 432 """Return avd_flavor.""" 433 return self._avd_flavor 434 435 @property 436 def islocal(self): 437 """Return if it is a local instance.""" 438 return self._is_local 439 440 @property 441 def adb_port(self): 442 """Return adb_port.""" 443 return self._adb_port 444 445 @property 446 def vnc_port(self): 447 """Return vnc_port.""" 448 return self._vnc_port 449 450 @property 451 def webrtc_port(self): 452 """Return webrtc_port.""" 453 return self._webrtc_port 454 455 @property 456 def webrtc_forward_port(self): 457 """Return webrtc_forward_port.""" 458 return self._webrtc_forward_port 459 460 @property 461 def zone(self): 462 """Return zone.""" 463 return self._zone 464 465 @property 466 def autoconnect(self): 467 """Return autoconnect.""" 468 return self._autoconnect 469 470 471class LocalInstance(Instance): 472 """Class to store data of local cuttlefish instance.""" 473 def __init__(self, cf_config_path): 474 """Initialize a localInstance object. 475 476 Args: 477 cf_config_path: String, path to the cf runtime config. 478 """ 479 self._cf_runtime_cfg = cvd_runtime_config.CvdRuntimeConfig(cf_config_path) 480 self._instance_dir = self._cf_runtime_cfg.instance_dir 481 self._virtual_disk_paths = self._cf_runtime_cfg.virtual_disk_paths 482 self._local_instance_id = int(self._cf_runtime_cfg.instance_id) 483 display = _DISPLAY_STRING % {"x_res": self._cf_runtime_cfg.x_res, 484 "y_res": self._cf_runtime_cfg.y_res, 485 "dpi": self._cf_runtime_cfg.dpi} 486 # TODO(143063678), there's no createtime info in 487 # cuttlefish_config.json so far. 488 name = GetLocalInstanceName(self._local_instance_id) 489 fullname = (_FULL_NAME_STRING % 490 {"device_serial": "0.0.0.0:%s" % self._cf_runtime_cfg.adb_port, 491 "instance_name": name, 492 "elapsed_time": None}) 493 adb_device = AdbTools(device_serial="0.0.0.0:%s" % self._cf_runtime_cfg.adb_port) 494 webrtc_port = local_image_local_instance.LocalImageLocalInstance.GetWebrtcSigServerPort( 495 self._local_instance_id) 496 cvd_fleet_info = self.GetDevidInfoFromCvdFleet() 497 if cvd_fleet_info: 498 display = cvd_fleet_info.get("displays") 499 500 device_information = None 501 if adb_device.IsAdbConnected(): 502 device_information = adb_device.device_information 503 504 super().__init__( 505 name=name, fullname=fullname, display=display, ip="0.0.0.0", 506 status=constants.INS_STATUS_RUNNING, 507 adb_port=self._cf_runtime_cfg.adb_port, 508 vnc_port=self._cf_runtime_cfg.vnc_port, 509 createtime=None, elapsed_time=None, avd_type=constants.TYPE_CF, 510 is_local=True, device_information=device_information, 511 zone=_LOCAL_ZONE, webrtc_port=webrtc_port) 512 513 def Summary(self): 514 """Return the string that this class is holding.""" 515 instance_home = "%s instance home: %s" % (_INDENT, self._instance_dir) 516 return "%s\n%s" % (super().Summary(), instance_home) 517 518 def _GetCvdEnv(self): 519 """Get the environment to run cvd commands. 520 521 Returns: 522 os.environ with cuttlefish variables updated. 523 """ 524 cvd_env = os.environ.copy() 525 cvd_env[constants.ENV_ANDROID_SOONG_HOST_OUT] = os.path.dirname( 526 self._cf_runtime_cfg.cvd_tools_path) 527 cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path 528 cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir(self._local_instance_id) 529 cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id) 530 return cvd_env 531 532 def GetDevidInfoFromCvdFleet(self): 533 """Get device information from 'cvd fleet'. 534 535 Execute 'cvd fleet' cmd to get device information. 536 537 Returns 538 Output of 'cvd fleet'. None for fail to run 'cvd fleet'. 539 """ 540 ins_home_dir = GetLocalInstanceHomeDir(self._local_instance_id) 541 try: 542 cvd_tool = os.path.join(ins_home_dir, _CVD_BIN_FOLDER, _CVD_BIN) 543 cvd_fleet_cmd = f"{cvd_tool} fleet" 544 if not os.path.exists(cvd_tool): 545 logger.warning("Cvd tools path doesn't exist:%s", cvd_tool) 546 return None 547 if not _IsProcessRunning(_CVD_SERVER): 548 logger.warning("The %s is not active.", _CVD_SERVER) 549 return None 550 logger.debug("Running cmd [%s] to get device info.", cvd_fleet_cmd) 551 process = subprocess.Popen(cvd_fleet_cmd, shell=True, text=True, 552 env=self._GetCvdEnv(), 553 stdout=subprocess.PIPE, 554 stderr=subprocess.PIPE) 555 stdout, _ = process.communicate(timeout=_CVD_TIMEOUT) 556 logger.debug("Output of cvd fleet: %s", stdout) 557 return json.loads(self._ParsingCvdFleetOutput(stdout)) 558 except (subprocess.CalledProcessError, subprocess.TimeoutExpired, 559 json.JSONDecodeError) as error: 560 logger.error("Failed to run 'cvd fleet': %s", str(error)) 561 return None 562 563 @staticmethod 564 def _ParsingCvdFleetOutput(output): 565 """Parsing the output of cvd fleet. 566 567 The output example: 568 WARNING: cvd_server client version (8245608) does not match. 569 { 570 "adb_serial" : "0.0.0.0:6520", 571 "assembly_dir" : "/home/cuttlefish_runtime/assembly", 572 "displays" : ["720 x 1280 ( 320 )"], 573 "instance_dir" : "/home/cuttlefish_runtime/instances/cvd-1", 574 "instance_name" : "cvd-1", 575 "status" : "Running", 576 "web_access" : "https://0.0.0.0:8443/client.html?deviceId=cvd-1", 577 "webrtc_port" : "8443" 578 } 579 580 Returns: 581 Parsed output filtered warning message. 582 """ 583 device_match = _RE_DEVICE_INFO.match(output) 584 if device_match: 585 return device_match.group("device_info") 586 return "" 587 588 def CvdStatus(self): 589 """check if local instance is active. 590 591 Execute cvd_status cmd to check if it exit without error. 592 593 Returns 594 True if instance is active. 595 """ 596 if not self._cf_runtime_cfg.cvd_tools_path: 597 logger.debug("No cvd tools path found from config:%s", 598 self._cf_runtime_cfg.config_path) 599 return False 600 try: 601 cvd_status_cmd = os.path.join(self._cf_runtime_cfg.cvd_tools_path, 602 _CVD_STATUS_BIN) 603 # TODO(b/150575261): Change the cvd home and cvd artifact path to 604 # another place instead of /tmp to prevent from the file not 605 # found exception. 606 if not os.path.exists(cvd_status_cmd): 607 logger.warning("Cvd tools path doesn't exist:%s", cvd_status_cmd) 608 for env_host_out in [constants.ENV_ANDROID_SOONG_HOST_OUT, 609 constants.ENV_ANDROID_HOST_OUT]: 610 if os.environ.get(env_host_out, _NO_ANDROID_ENV) in cvd_status_cmd: 611 logger.warning( 612 "Can't find the cvd_status tool (Try lunching a " 613 "cuttlefish target like aosp_cf_x86_64_phone-userdebug " 614 "and running 'make hosttar' before list/delete local " 615 "instances)") 616 return False 617 logger.debug("Running cmd[%s] to check cvd status.", cvd_status_cmd) 618 process = subprocess.Popen(cvd_status_cmd, 619 stdin=None, 620 stdout=subprocess.PIPE, 621 stderr=subprocess.STDOUT, 622 env=self._GetCvdEnv()) 623 stdout, _ = process.communicate() 624 if process.returncode != 0: 625 if stdout: 626 logger.debug("Local instance[%s] is not active: %s", 627 self.name, stdout.strip()) 628 return False 629 return True 630 except subprocess.CalledProcessError as cpe: 631 logger.error("Failed to run cvd_status: %s", cpe.output) 632 return False 633 634 def Delete(self): 635 """Execute "cvd stop" to stop local cuttlefish instance. 636 637 - We should get the same host tool used to delete instance. 638 - Add CUTTLEFISH_CONFIG_FILE env variable to tell cvd which cvd need to 639 be deleted. 640 - Stop adb since local instance use the fixed adb port and could be 641 reused again soon. 642 """ 643 ins_home_dir = GetLocalInstanceHomeDir(self._local_instance_id) 644 cvd_tool = os.path.join(ins_home_dir, _CVD_BIN_FOLDER, _CVD_BIN) 645 stop_cvd_cmd = f"{cvd_tool} stop" 646 logger.debug("Running cmd[%s] to delete local cvd", stop_cvd_cmd) 647 if not self.instance_dir: 648 logger.error("instance_dir is null!! instance[%d] might not be" 649 " deleted", self._local_instance_id) 650 try: 651 output = subprocess.check_output( 652 utils.AddUserGroupsToCmd(stop_cvd_cmd, 653 constants.LIST_CF_USER_GROUPS), 654 stderr=subprocess.STDOUT, shell=True, env=self._GetCvdEnv(), 655 text=True, timeout=_CVD_TIMEOUT) 656 # TODO: Remove workaround of stop_cvd when 'cvd stop' is stable. 657 if _CVD_STOP_ERROR_KEYWORDS in output: 658 logger.debug("Fail to stop cvd: %s", output) 659 self._ExecuteStopCvd(os.path.join(ins_home_dir, _CVD_BIN_FOLDER)) 660 except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e: 661 logger.debug("'cvd stop' error: %s", str(e)) 662 self._ExecuteStopCvd(os.path.join(ins_home_dir, _CVD_BIN_FOLDER)) 663 664 adb_cmd = AdbTools(self.adb_port) 665 # When relaunch a local instance, we need to pass in retry=True to make 666 # sure adb device is completely gone since it will use the same adb port 667 adb_cmd.DisconnectAdb(retry=True) 668 669 def _ExecuteStopCvd(self, dir_path): 670 """Execute "stop_cvd" to stop local cuttlefish instance. 671 672 Args: 673 bin_dir: String, directory path of "stop_cvd". 674 """ 675 stop_cvd_cmd = os.path.join(dir_path, constants.CMD_STOP_CVD) 676 subprocess.check_call( 677 utils.AddUserGroupsToCmd( 678 stop_cvd_cmd, constants.LIST_CF_USER_GROUPS), 679 stderr=subprocess.STDOUT, shell=True, env=self._GetCvdEnv()) 680 681 def GetLock(self): 682 """Return the LocalInstanceLock for this object.""" 683 return GetLocalInstanceLock(self._local_instance_id) 684 685 @property 686 def instance_dir(self): 687 """Return _instance_dir.""" 688 return self._instance_dir 689 690 @property 691 def instance_id(self): 692 """Return _local_instance_id.""" 693 return self._local_instance_id 694 695 @property 696 def virtual_disk_paths(self): 697 """Return virtual_disk_paths""" 698 return self._virtual_disk_paths 699 700 @property 701 def cf_runtime_cfg(self): 702 """Return _cf_runtime_cfg""" 703 return self._cf_runtime_cfg 704 705 706class LocalGoldfishInstance(Instance): 707 """Class to store data of local goldfish instance. 708 709 A goldfish instance binds to a console port and an adb port. The console 710 port is for `adb emu` to send emulator-specific commands. The adb port is 711 for `adb connect` to start a TCP connection. By convention, the console 712 port is an even number, and the adb port is the console port + 1. The first 713 instance uses port 5554 and 5555, the second instance uses 5556 and 5557, 714 and so on. 715 """ 716 717 _INSTANCE_NAME_PATTERN = re.compile( 718 r"^local-goldfish-instance-(?P<id>\d+)$") 719 _INSTANCE_NAME_FORMAT = "local-goldfish-instance-%(id)s" 720 _EMULATOR_DEFAULT_CONSOLE_PORT = 5554 721 _DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT = 5585 722 _DEVICE_SERIAL_FORMAT = "emulator-%(console_port)s" 723 _DEVICE_SERIAL_PATTERN = re.compile(r"^emulator-(?P<console_port>\d+)$") 724 725 def __init__(self, local_instance_id, avd_flavor=None, create_time=None, 726 x_res=None, y_res=None, dpi=None): 727 """Initialize a LocalGoldfishInstance object. 728 729 Args: 730 local_instance_id: Integer of instance id. 731 avd_flavor: String, the flavor of the virtual device. 732 create_time: String, the creation date and time. 733 x_res: Integer of x dimension. 734 y_res: Integer of y dimension. 735 dpi: Integer of dpi. 736 """ 737 self._id = local_instance_id 738 adb_port = self.console_port + 1 739 self._adb = AdbTools(adb_port=adb_port, 740 device_serial=self.device_serial) 741 742 name = self._INSTANCE_NAME_FORMAT % {"id": local_instance_id} 743 744 elapsed_time = _GetElapsedTime(create_time) if create_time else None 745 746 fullname = _FULL_NAME_STRING % {"device_serial": self.device_serial, 747 "instance_name": name, 748 "elapsed_time": elapsed_time} 749 750 if x_res and y_res and dpi: 751 display = _DISPLAY_STRING % {"x_res": x_res, "y_res": y_res, 752 "dpi": dpi} 753 else: 754 display = "unknown" 755 756 device_information = (self._adb.device_information if 757 self._adb.device_information else None) 758 759 super().__init__( 760 name=name, fullname=fullname, display=display, ip="127.0.0.1", 761 status=None, adb_port=adb_port, avd_type=constants.TYPE_GF, 762 createtime=create_time, elapsed_time=elapsed_time, 763 avd_flavor=avd_flavor, is_local=True, 764 device_information=device_information) 765 766 @staticmethod 767 def _GetInstanceDirRoot(): 768 """Return the root directory of all instance directories.""" 769 return os.path.join(tempfile.gettempdir(), "acloud_gf_temp") 770 771 @property 772 def adb(self): 773 """Return the AdbTools to send emulator commands to this instance.""" 774 return self._adb 775 776 @property 777 def console_port(self): 778 """Return the console port as an integer.""" 779 # Emulator requires the console port to be an even number. 780 return self._EMULATOR_DEFAULT_CONSOLE_PORT + (self._id - 1) * 2 781 782 @property 783 def device_serial(self): 784 """Return the serial number that contains the console port.""" 785 return self._DEVICE_SERIAL_FORMAT % {"console_port": self.console_port} 786 787 @property 788 def instance_dir(self): 789 """Return the path to instance directory.""" 790 return os.path.join(self._GetInstanceDirRoot(), 791 self._INSTANCE_NAME_FORMAT % {"id": self._id}) 792 793 @classmethod 794 def GetIdByName(cls, name): 795 """Get id by name. 796 797 Args: 798 name: String of instance name. 799 800 Return: 801 The instance id as an integer if the name is in valid format. 802 None if the name does not represent a local goldfish instance. 803 """ 804 match = cls._INSTANCE_NAME_PATTERN.match(name) 805 if match: 806 return int(match.group("id")) 807 return None 808 809 @classmethod 810 def GetLockById(cls, instance_id): 811 """Get LocalInstanceLock by id.""" 812 lock_path = os.path.join( 813 cls._GetInstanceDirRoot(), 814 (cls._INSTANCE_NAME_FORMAT % {"id": instance_id}) + ".lock") 815 return LocalInstanceLock(lock_path) 816 817 def GetLock(self): 818 """Return the LocalInstanceLock for this object.""" 819 return self.GetLockById(self._id) 820 821 @classmethod 822 def GetExistingInstances(cls): 823 """Get the list of instances that adb can send emu commands to.""" 824 instances = [] 825 for serial in AdbTools.GetDeviceSerials(): 826 match = cls._DEVICE_SERIAL_PATTERN.match(serial) 827 if not match: 828 continue 829 port = int(match.group("console_port")) 830 instance_id = (port - cls._EMULATOR_DEFAULT_CONSOLE_PORT) // 2 + 1 831 instances.append(LocalGoldfishInstance(instance_id)) 832 return instances 833 834 @classmethod 835 def GetMaxNumberOfInstances(cls): 836 """Get number of emulators that adb can detect.""" 837 max_port = os.environ.get("ADB_LOCAL_TRANSPORT_MAX_PORT", 838 cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT) 839 try: 840 max_port = int(max_port) 841 except ValueError: 842 max_port = cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT 843 if (max_port < cls._EMULATOR_DEFAULT_CONSOLE_PORT or 844 max_port > constants.MAX_PORT): 845 max_port = cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT 846 return (max_port + 1 - cls._EMULATOR_DEFAULT_CONSOLE_PORT) // 2 847 848 849class RemoteInstance(Instance): 850 """Class to store data of remote instance.""" 851 852 # pylint: disable=too-many-locals 853 def __init__(self, gce_instance): 854 """Process the args into class vars. 855 856 RemoteInstace initialized by gce dict object. We parse the required data 857 from gce_instance to local variables. 858 Reference: 859 https://cloud.google.com/compute/docs/reference/rest/v1/instances/get 860 861 We also gather more details on client side including the forwarding adb 862 port and vnc port which will be used to determine the status of ssh 863 tunnel connection. 864 865 The status of gce instance will be displayed in _fullname property: 866 - Connected: If gce instance and ssh tunnel and adb connection are all 867 active. 868 - No connected: If ssh tunnel or adb connection is not found. 869 - Terminated: If we can't retrieve the public ip from gce instance. 870 871 Args: 872 gce_instance: dict object queried from gce. 873 """ 874 name = gce_instance.get(constants.INS_KEY_NAME) 875 876 create_time = gce_instance.get(constants.INS_KEY_CREATETIME) 877 elapsed_time = _GetElapsedTime(create_time) 878 status = gce_instance.get(constants.INS_KEY_STATUS) 879 zone = self._GetZoneName(gce_instance.get(constants.INS_KEY_ZONE)) 880 881 instance_ip = GetInstanceIP(gce_instance) 882 ip = instance_ip.external or instance_ip.internal 883 884 # Get metadata, webrtc_port will be removed if "cvd fleet" show it. 885 display = None 886 avd_type = None 887 avd_flavor = None 888 webrtc_port = None 889 for metadata in gce_instance.get("metadata", {}).get("items", []): 890 key = metadata["key"] 891 value = metadata["value"] 892 if key == constants.INS_KEY_DISPLAY: 893 display = value 894 elif key == constants.INS_KEY_AVD_TYPE: 895 avd_type = value 896 elif key == constants.INS_KEY_AVD_FLAVOR: 897 avd_flavor = value 898 elif key == constants.INS_KEY_WEBRTC_PORT: 899 webrtc_port = value 900 # TODO(176884236): Insert avd information into metadata of instance. 901 if not avd_type and name.startswith(_ACLOUDWEB_INSTANCE_START_STRING): 902 avd_type = constants.TYPE_CF 903 904 # Find ssl tunnel info. 905 adb_port = None 906 vnc_port = None 907 webrtc_forward_port = None 908 device_information = None 909 if ip: 910 forwarded_ports = self.GetAdbVncPortFromSSHTunnel(ip, avd_type) 911 adb_port = forwarded_ports.adb_port 912 vnc_port = forwarded_ports.vnc_port 913 ssh_tunnel_is_connected = adb_port is not None 914 webrtc_forward_port = utils.GetWebrtcPortFromSSHTunnel(ip) 915 916 adb_device = AdbTools(adb_port) 917 if adb_device.IsAdbConnected(): 918 device_information = adb_device.device_information 919 fullname = (_FULL_NAME_STRING % 920 {"device_serial": "127.0.0.1:%d" % adb_port, 921 "instance_name": name, 922 "elapsed_time": elapsed_time}) 923 else: 924 fullname = (_FULL_NAME_STRING % 925 {"device_serial": "not connected", 926 "instance_name": name, 927 "elapsed_time": elapsed_time}) 928 # If instance is terminated, its ip is None. 929 else: 930 ssh_tunnel_is_connected = False 931 fullname = (_FULL_NAME_STRING % 932 {"device_serial": "terminated", 933 "instance_name": name, 934 "elapsed_time": elapsed_time}) 935 936 super().__init__( 937 name=name, fullname=fullname, display=display, ip=ip, status=status, 938 adb_port=adb_port, vnc_port=vnc_port, 939 ssh_tunnel_is_connected=ssh_tunnel_is_connected, 940 createtime=create_time, elapsed_time=elapsed_time, avd_type=avd_type, 941 avd_flavor=avd_flavor, is_local=False, 942 device_information=device_information, 943 zone=zone, webrtc_port=webrtc_port, 944 webrtc_forward_port=webrtc_forward_port) 945 946 @staticmethod 947 def _GetZoneName(zone_info): 948 """Get the zone name from the zone information of gce instance. 949 950 Zone information is like: 951 "https://www.googleapis.com/compute/v1/projects/project/zones/us-central1-c" 952 We want to get "us-central1-c" as zone name. 953 954 Args: 955 zone_info: String, zone information of gce instance. 956 957 Returns: 958 Zone name of gce instance. None if zone name can't find. 959 """ 960 zone_match = _RE_ZONE.match(zone_info) 961 if zone_match: 962 return zone_match.group("zone") 963 964 logger.debug("Can't get zone name from %s.", zone_info) 965 return None 966 967 @staticmethod 968 def GetAdbVncPortFromSSHTunnel(ip, avd_type): 969 """Get forwarding adb and vnc port from ssh tunnel. 970 971 Args: 972 ip: String, ip address. 973 avd_type: String, the AVD type. 974 975 Returns: 976 NamedTuple ForwardedPorts(vnc_port, adb_port) holding the ports 977 used in the ssh forwarded call. Both fields are integers. 978 """ 979 if avd_type not in utils.AVD_PORT_DICT: 980 return utils.ForwardedPorts(vnc_port=None, adb_port=None) 981 982 default_vnc_port = utils.AVD_PORT_DICT[avd_type].vnc_port 983 default_adb_port = utils.AVD_PORT_DICT[avd_type].adb_port 984 # TODO(165888525): Align the SSH tunnel for the order of adb port and 985 # vnc port. 986 re_pattern = re.compile(_RE_SSH_TUNNEL_PATTERN % 987 (_RE_GROUP_ADB, default_adb_port, 988 _RE_GROUP_VNC, default_vnc_port, ip)) 989 adb_port = None 990 vnc_port = None 991 process_output = utils.CheckOutput(constants.COMMAND_PS) 992 for line in process_output.splitlines(): 993 match = re_pattern.match(line) 994 if match: 995 adb_port = int(match.group(_RE_GROUP_ADB)) 996 vnc_port = int(match.group(_RE_GROUP_VNC)) 997 break 998 999 logger.debug(("gathering detail for ssh tunnel. " 1000 "IP:%s, forwarding (adb:%s, vnc:%s)"), ip, adb_port, 1001 vnc_port) 1002 1003 return utils.ForwardedPorts(vnc_port=vnc_port, adb_port=adb_port) 1004