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