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