817 lines
30 KiB
Python
817 lines
30 KiB
Python
|
# Copyright 2018 - The Android Open Source Project
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
r"""Instance class.
|
||
|
|
||
|
Define the instance class used to hold details about an AVD instance.
|
||
|
|
||
|
The instance class will hold details about AVD instances (remote/local) used to
|
||
|
enable users to understand what instances they've created. This will be leveraged
|
||
|
for the list, delete, and reconnect commands.
|
||
|
|
||
|
The details include:
|
||
|
- instance name (for remote instances)
|
||
|
- creation date/instance duration
|
||
|
- instance image details (branch/target/build id)
|
||
|
- and more!
|
||
|
"""
|
||
|
|
||
|
import collections
|
||
|
import datetime
|
||
|
import logging
|
||
|
import os
|
||
|
import re
|
||
|
import subprocess
|
||
|
import tempfile
|
||
|
|
||
|
# pylint: disable=import-error
|
||
|
import dateutil.parser
|
||
|
import dateutil.tz
|
||
|
|
||
|
from acloud.create import local_image_local_instance
|
||
|
from acloud.internal import constants
|
||
|
from acloud.internal.lib import cvd_runtime_config
|
||
|
from acloud.internal.lib import utils
|
||
|
from acloud.internal.lib.adb_tools import AdbTools
|
||
|
from acloud.internal.lib.local_instance_lock import LocalInstanceLock
|
||
|
|
||
|
|
||
|
logger = logging.getLogger(__name__)
|
||
|
|
||
|
_ACLOUD_CVD_TEMP = os.path.join(tempfile.gettempdir(), "acloud_cvd_temp")
|
||
|
_CVD_RUNTIME_FOLDER_NAME = "cuttlefish_runtime"
|
||
|
_CVD_STATUS_BIN = "cvd_status"
|
||
|
_LOCAL_INSTANCE_NAME_FORMAT = "local-instance-%(id)d"
|
||
|
_LOCAL_INSTANCE_NAME_PATTERN = re.compile(r"^local-instance-(?P<id>\d+)$")
|
||
|
_ACLOUDWEB_INSTANCE_START_STRING = "cf-"
|
||
|
_MSG_UNABLE_TO_CALCULATE = "Unable to calculate"
|
||
|
_NO_ANDROID_ENV = "android source not available"
|
||
|
_RE_GROUP_ADB = "local_adb_port"
|
||
|
_RE_GROUP_VNC = "local_vnc_port"
|
||
|
_RE_SSH_TUNNEL_PATTERN = (r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)"
|
||
|
r"((.*\s*-L\s)(?P<%s>\d+):127.0.0.1:%s)"
|
||
|
r"(.+%s)")
|
||
|
_RE_TIMEZONE = re.compile(r"^(?P<time>[0-9\-\.:T]*)(?P<timezone>[+-]\d+:\d+)$")
|
||
|
|
||
|
_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"]
|
||
|
_RE_RUN_CVD = re.compile(r"(?P<date_str>^[^/]+)(.*run_cvd)")
|
||
|
_DISPLAY_STRING = "%(x_res)sx%(y_res)s (%(dpi)s)"
|
||
|
_RE_ZONE = re.compile(r".+/zones/(?P<zone>.+)$")
|
||
|
_LOCAL_ZONE = "local"
|
||
|
_FULL_NAME_STRING = ("device serial: %(device_serial)s (%(instance_name)s) "
|
||
|
"elapsed time: %(elapsed_time)s")
|
||
|
_INDENT = " " * 3
|
||
|
LocalPorts = collections.namedtuple("LocalPorts", [constants.VNC_PORT,
|
||
|
constants.ADB_PORT])
|
||
|
|
||
|
|
||
|
def GetDefaultCuttlefishConfig():
|
||
|
"""Get the path of default cuttlefish instance config.
|
||
|
|
||
|
Return:
|
||
|
String, path of cf runtime config.
|
||
|
"""
|
||
|
cfg_path = os.path.join(os.path.expanduser("~"), _CVD_RUNTIME_FOLDER_NAME,
|
||
|
constants.CUTTLEFISH_CONFIG_FILE)
|
||
|
if os.path.isfile(cfg_path):
|
||
|
return cfg_path
|
||
|
return None
|
||
|
|
||
|
|
||
|
def GetLocalInstanceName(local_instance_id):
|
||
|
"""Get local cuttlefish instance name by instance id.
|
||
|
|
||
|
Args:
|
||
|
local_instance_id: Integer of instance id.
|
||
|
|
||
|
Return:
|
||
|
String, the instance name.
|
||
|
"""
|
||
|
return _LOCAL_INSTANCE_NAME_FORMAT % {"id": local_instance_id}
|
||
|
|
||
|
|
||
|
def GetLocalInstanceIdByName(name):
|
||
|
"""Get local cuttlefish instance id by name.
|
||
|
|
||
|
Args:
|
||
|
name: String of instance name.
|
||
|
|
||
|
Return:
|
||
|
The instance id as an integer if the name is in valid format.
|
||
|
None if the name does not represent a local cuttlefish instance.
|
||
|
"""
|
||
|
match = _LOCAL_INSTANCE_NAME_PATTERN.match(name)
|
||
|
if match:
|
||
|
return int(match.group("id"))
|
||
|
return None
|
||
|
|
||
|
|
||
|
def GetLocalInstanceConfig(local_instance_id):
|
||
|
"""Get the path of instance config.
|
||
|
|
||
|
Args:
|
||
|
local_instance_id: Integer of instance id.
|
||
|
|
||
|
Return:
|
||
|
String, path of cf runtime config.
|
||
|
"""
|
||
|
cfg_path = os.path.join(GetLocalInstanceRuntimeDir(local_instance_id),
|
||
|
constants.CUTTLEFISH_CONFIG_FILE)
|
||
|
if os.path.isfile(cfg_path):
|
||
|
return cfg_path
|
||
|
return None
|
||
|
|
||
|
|
||
|
def GetAllLocalInstanceConfigs():
|
||
|
"""Get all cuttlefish runtime configs from the known locations.
|
||
|
|
||
|
Return:
|
||
|
List of tuples. Each tuple consists of an instance id and a config
|
||
|
path.
|
||
|
"""
|
||
|
id_cfg_pairs = []
|
||
|
# Check if any instance config is under home folder.
|
||
|
cfg_path = GetDefaultCuttlefishConfig()
|
||
|
if cfg_path:
|
||
|
id_cfg_pairs.append((1, cfg_path))
|
||
|
|
||
|
# Check if any instance config is under acloud cvd temp folder.
|
||
|
if os.path.exists(_ACLOUD_CVD_TEMP):
|
||
|
for ins_name in os.listdir(_ACLOUD_CVD_TEMP):
|
||
|
ins_id = GetLocalInstanceIdByName(ins_name)
|
||
|
if ins_id is not None:
|
||
|
cfg_path = GetLocalInstanceConfig(ins_id)
|
||
|
if cfg_path:
|
||
|
id_cfg_pairs.append((ins_id, cfg_path))
|
||
|
return id_cfg_pairs
|
||
|
|
||
|
|
||
|
def GetLocalInstanceHomeDir(local_instance_id):
|
||
|
"""Get local instance home dir according to instance id.
|
||
|
|
||
|
Args:
|
||
|
local_instance_id: Integer of instance id.
|
||
|
|
||
|
Return:
|
||
|
String, path of instance home dir.
|
||
|
"""
|
||
|
return os.path.join(_ACLOUD_CVD_TEMP,
|
||
|
GetLocalInstanceName(local_instance_id))
|
||
|
|
||
|
|
||
|
def GetLocalInstanceLock(local_instance_id):
|
||
|
"""Get local instance lock.
|
||
|
|
||
|
Args:
|
||
|
local_instance_id: Integer of instance id.
|
||
|
|
||
|
Returns:
|
||
|
LocalInstanceLock object.
|
||
|
"""
|
||
|
file_path = os.path.join(_ACLOUD_CVD_TEMP,
|
||
|
GetLocalInstanceName(local_instance_id) + ".lock")
|
||
|
return LocalInstanceLock(file_path)
|
||
|
|
||
|
|
||
|
def GetLocalInstanceRuntimeDir(local_instance_id):
|
||
|
"""Get instance runtime dir
|
||
|
|
||
|
Args:
|
||
|
local_instance_id: Integer of instance id.
|
||
|
|
||
|
Return:
|
||
|
String, path of instance runtime dir.
|
||
|
"""
|
||
|
return os.path.join(GetLocalInstanceHomeDir(local_instance_id),
|
||
|
_CVD_RUNTIME_FOLDER_NAME)
|
||
|
|
||
|
|
||
|
def _GetCurrentLocalTime():
|
||
|
"""Return a datetime object for current time in local time zone."""
|
||
|
return datetime.datetime.now(dateutil.tz.tzlocal())
|
||
|
|
||
|
|
||
|
def _GetElapsedTime(start_time):
|
||
|
"""Calculate the elapsed time from start_time till now.
|
||
|
|
||
|
Args:
|
||
|
start_time: String of instance created time.
|
||
|
|
||
|
Returns:
|
||
|
datetime.timedelta of elapsed time, _MSG_UNABLE_TO_CALCULATE for
|
||
|
datetime can't parse cases.
|
||
|
"""
|
||
|
match = _RE_TIMEZONE.match(start_time)
|
||
|
try:
|
||
|
# Check start_time has timezone or not. If timezone can't be found,
|
||
|
# use local timezone to get elapsed time.
|
||
|
if match:
|
||
|
return _GetCurrentLocalTime() - dateutil.parser.parse(start_time)
|
||
|
|
||
|
return _GetCurrentLocalTime() - dateutil.parser.parse(
|
||
|
start_time).replace(tzinfo=dateutil.tz.tzlocal())
|
||
|
except ValueError:
|
||
|
logger.debug(("Can't parse datetime string(%s)."), start_time)
|
||
|
return _MSG_UNABLE_TO_CALCULATE
|
||
|
|
||
|
|
||
|
# pylint: disable=useless-object-inheritance
|
||
|
class Instance(object):
|
||
|
"""Class to store data of instance."""
|
||
|
|
||
|
# pylint: disable=too-many-locals
|
||
|
def __init__(self, name, fullname, display, ip, status=None, adb_port=None,
|
||
|
vnc_port=None, ssh_tunnel_is_connected=None, createtime=None,
|
||
|
elapsed_time=None, avd_type=None, avd_flavor=None,
|
||
|
is_local=False, device_information=None, zone=None,
|
||
|
webrtc_port=None):
|
||
|
self._name = name
|
||
|
self._fullname = fullname
|
||
|
self._status = status
|
||
|
self._display = display # Resolution and dpi
|
||
|
self._ip = ip
|
||
|
self._adb_port = adb_port # adb port which is forwarding to remote
|
||
|
self._vnc_port = vnc_port # vnc port which is forwarding to remote
|
||
|
self._webrtc_port = webrtc_port
|
||
|
# True if ssh tunnel is still connected
|
||
|
self._ssh_tunnel_is_connected = ssh_tunnel_is_connected
|
||
|
self._createtime = createtime
|
||
|
self._elapsed_time = elapsed_time
|
||
|
self._avd_type = avd_type
|
||
|
self._avd_flavor = avd_flavor
|
||
|
self._is_local = is_local # True if this is a local instance
|
||
|
self._device_information = device_information
|
||
|
self._zone = zone
|
||
|
|
||
|
def __repr__(self):
|
||
|
"""Return full name property for print."""
|
||
|
return self._fullname
|
||
|
|
||
|
def Summary(self):
|
||
|
"""Let's make it easy to see what this class is holding."""
|
||
|
representation = []
|
||
|
representation.append(" name: %s" % self._name)
|
||
|
representation.append("%s IP: %s" % (_INDENT, self._ip))
|
||
|
representation.append("%s create time: %s" % (_INDENT, self._createtime))
|
||
|
representation.append("%s elapse time: %s" % (_INDENT, self._elapsed_time))
|
||
|
representation.append("%s status: %s" % (_INDENT, self._status))
|
||
|
representation.append("%s avd type: %s" % (_INDENT, self._avd_type))
|
||
|
representation.append("%s display: %s" % (_INDENT, self._display))
|
||
|
representation.append("%s vnc: 127.0.0.1:%s" % (_INDENT, self._vnc_port))
|
||
|
representation.append("%s zone: %s" % (_INDENT, self._zone))
|
||
|
representation.append("%s webrtc port: %s" % (_INDENT, self._webrtc_port))
|
||
|
|
||
|
if self._adb_port and self._device_information:
|
||
|
representation.append("%s adb serial: 127.0.0.1:%s" %
|
||
|
(_INDENT, self._adb_port))
|
||
|
representation.append("%s product: %s" % (
|
||
|
_INDENT, self._device_information["product"]))
|
||
|
representation.append("%s model: %s" % (
|
||
|
_INDENT, self._device_information["model"]))
|
||
|
representation.append("%s device: %s" % (
|
||
|
_INDENT, self._device_information["device"]))
|
||
|
representation.append("%s transport_id: %s" % (
|
||
|
_INDENT, self._device_information["transport_id"]))
|
||
|
else:
|
||
|
representation.append("%s adb serial: disconnected" % _INDENT)
|
||
|
|
||
|
return "\n".join(representation)
|
||
|
|
||
|
def AdbConnected(self):
|
||
|
"""Check AVD adb connected.
|
||
|
|
||
|
Returns:
|
||
|
Boolean, True when adb status of AVD is connected.
|
||
|
"""
|
||
|
if self._adb_port and self._device_information:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
"""Return the instance name."""
|
||
|
return self._name
|
||
|
|
||
|
@property
|
||
|
def fullname(self):
|
||
|
"""Return the instance full name."""
|
||
|
return self._fullname
|
||
|
|
||
|
@property
|
||
|
def ip(self):
|
||
|
"""Return the ip."""
|
||
|
return self._ip
|
||
|
|
||
|
@property
|
||
|
def status(self):
|
||
|
"""Return status."""
|
||
|
return self._status
|
||
|
|
||
|
@property
|
||
|
def display(self):
|
||
|
"""Return display."""
|
||
|
return self._display
|
||
|
|
||
|
@property
|
||
|
def ssh_tunnel_is_connected(self):
|
||
|
"""Return the connect status."""
|
||
|
return self._ssh_tunnel_is_connected
|
||
|
|
||
|
@property
|
||
|
def createtime(self):
|
||
|
"""Return create time."""
|
||
|
return self._createtime
|
||
|
|
||
|
@property
|
||
|
def avd_type(self):
|
||
|
"""Return avd_type."""
|
||
|
return self._avd_type
|
||
|
|
||
|
@property
|
||
|
def avd_flavor(self):
|
||
|
"""Return avd_flavor."""
|
||
|
return self._avd_flavor
|
||
|
|
||
|
@property
|
||
|
def islocal(self):
|
||
|
"""Return if it is a local instance."""
|
||
|
return self._is_local
|
||
|
|
||
|
@property
|
||
|
def adb_port(self):
|
||
|
"""Return adb_port."""
|
||
|
return self._adb_port
|
||
|
|
||
|
@property
|
||
|
def vnc_port(self):
|
||
|
"""Return vnc_port."""
|
||
|
return self._vnc_port
|
||
|
|
||
|
@property
|
||
|
def webrtc_port(self):
|
||
|
"""Return webrtc_port."""
|
||
|
return self._webrtc_port
|
||
|
|
||
|
@property
|
||
|
def zone(self):
|
||
|
"""Return zone."""
|
||
|
return self._zone
|
||
|
|
||
|
|
||
|
class LocalInstance(Instance):
|
||
|
"""Class to store data of local cuttlefish instance."""
|
||
|
def __init__(self, cf_config_path):
|
||
|
"""Initialize a localInstance object.
|
||
|
|
||
|
Args:
|
||
|
cf_config_path: String, path to the cf runtime config.
|
||
|
"""
|
||
|
self._cf_runtime_cfg = cvd_runtime_config.CvdRuntimeConfig(cf_config_path)
|
||
|
self._instance_dir = self._cf_runtime_cfg.instance_dir
|
||
|
self._virtual_disk_paths = self._cf_runtime_cfg.virtual_disk_paths
|
||
|
self._local_instance_id = int(self._cf_runtime_cfg.instance_id)
|
||
|
|
||
|
display = _DISPLAY_STRING % {"x_res": self._cf_runtime_cfg.x_res,
|
||
|
"y_res": self._cf_runtime_cfg.y_res,
|
||
|
"dpi": self._cf_runtime_cfg.dpi}
|
||
|
# TODO(143063678), there's no createtime info in
|
||
|
# cuttlefish_config.json so far.
|
||
|
name = GetLocalInstanceName(self._local_instance_id)
|
||
|
fullname = (_FULL_NAME_STRING %
|
||
|
{"device_serial": "0.0.0.0:%s" % self._cf_runtime_cfg.adb_port,
|
||
|
"instance_name": name,
|
||
|
"elapsed_time": None})
|
||
|
adb_device = AdbTools(self._cf_runtime_cfg.adb_port)
|
||
|
webrtc_port = local_image_local_instance.LocalImageLocalInstance.GetWebrtcSigServerPort(
|
||
|
self._local_instance_id)
|
||
|
device_information = None
|
||
|
if adb_device.IsAdbConnected():
|
||
|
device_information = adb_device.device_information
|
||
|
|
||
|
super().__init__(
|
||
|
name=name, fullname=fullname, display=display, ip="0.0.0.0",
|
||
|
status=constants.INS_STATUS_RUNNING,
|
||
|
adb_port=self._cf_runtime_cfg.adb_port,
|
||
|
vnc_port=self._cf_runtime_cfg.vnc_port,
|
||
|
createtime=None, elapsed_time=None, avd_type=constants.TYPE_CF,
|
||
|
is_local=True, device_information=device_information,
|
||
|
zone=_LOCAL_ZONE, webrtc_port=webrtc_port)
|
||
|
|
||
|
def Summary(self):
|
||
|
"""Return the string that this class is holding."""
|
||
|
instance_home = "%s instance home: %s" % (_INDENT, self._instance_dir)
|
||
|
return "%s\n%s" % (super().Summary(), instance_home)
|
||
|
|
||
|
def CvdStatus(self):
|
||
|
"""check if local instance is active.
|
||
|
|
||
|
Execute cvd_status cmd to check if it exit without error.
|
||
|
|
||
|
Returns
|
||
|
True if instance is active.
|
||
|
"""
|
||
|
if not self._cf_runtime_cfg.cvd_tools_path:
|
||
|
logger.debug("No cvd tools path found from config:%s",
|
||
|
self._cf_runtime_cfg.config_path)
|
||
|
return False
|
||
|
cvd_env = os.environ.copy()
|
||
|
cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path
|
||
|
cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir(self._local_instance_id)
|
||
|
cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id)
|
||
|
try:
|
||
|
cvd_status_cmd = os.path.join(self._cf_runtime_cfg.cvd_tools_path,
|
||
|
_CVD_STATUS_BIN)
|
||
|
# TODO(b/150575261): Change the cvd home and cvd artifact path to
|
||
|
# another place instead of /tmp to prevent from the file not
|
||
|
# found exception.
|
||
|
if not os.path.exists(cvd_status_cmd):
|
||
|
logger.warning("Cvd tools path doesn't exist:%s", cvd_status_cmd)
|
||
|
for env_host_out in [constants.ENV_ANDROID_SOONG_HOST_OUT,
|
||
|
constants.ENV_ANDROID_HOST_OUT]:
|
||
|
if os.environ.get(env_host_out, _NO_ANDROID_ENV) in cvd_status_cmd:
|
||
|
logger.warning(
|
||
|
"Can't find the cvd_status tool (Try lunching a "
|
||
|
"cuttlefish target like aosp_cf_x86_phone-userdebug "
|
||
|
"and running 'make hosttar' before list/delete local "
|
||
|
"instances)")
|
||
|
return False
|
||
|
logger.debug("Running cmd[%s] to check cvd status.", cvd_status_cmd)
|
||
|
process = subprocess.Popen(cvd_status_cmd,
|
||
|
stdin=None,
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.STDOUT,
|
||
|
env=cvd_env)
|
||
|
stdout, _ = process.communicate()
|
||
|
if process.returncode != 0:
|
||
|
if stdout:
|
||
|
logger.debug("Local instance[%s] is not active: %s",
|
||
|
self.name, stdout.strip())
|
||
|
return False
|
||
|
return True
|
||
|
except subprocess.CalledProcessError as cpe:
|
||
|
logger.error("Failed to run cvd_status: %s", cpe.output)
|
||
|
return False
|
||
|
|
||
|
def Delete(self):
|
||
|
"""Execute stop_cvd to stop local cuttlefish instance.
|
||
|
|
||
|
- We should get the same host tool used to launch cvd to delete instance
|
||
|
, So get stop_cvd bin from the cvd runtime config.
|
||
|
- Add CUTTLEFISH_CONFIG_FILE env variable to tell stop_cvd which cvd
|
||
|
need to be deleted.
|
||
|
- Stop adb since local instance use the fixed adb port and could be
|
||
|
reused again soon.
|
||
|
"""
|
||
|
stop_cvd_cmd = os.path.join(self.cf_runtime_cfg.cvd_tools_path,
|
||
|
constants.CMD_STOP_CVD)
|
||
|
logger.debug("Running cmd[%s] to delete local cvd", stop_cvd_cmd)
|
||
|
cvd_env = os.environ.copy()
|
||
|
if self.instance_dir:
|
||
|
cvd_env[constants.ENV_CUTTLEFISH_CONFIG_FILE] = self._cf_runtime_cfg.config_path
|
||
|
cvd_env[constants.ENV_CVD_HOME] = GetLocalInstanceHomeDir(
|
||
|
self._local_instance_id)
|
||
|
cvd_env[constants.ENV_CUTTLEFISH_INSTANCE] = str(self._local_instance_id)
|
||
|
else:
|
||
|
logger.error("instance_dir is null!! instance[%d] might not be"
|
||
|
" deleted", self._local_instance_id)
|
||
|
subprocess.check_call(
|
||
|
utils.AddUserGroupsToCmd(stop_cvd_cmd,
|
||
|
constants.LIST_CF_USER_GROUPS),
|
||
|
stderr=subprocess.STDOUT, shell=True, env=cvd_env)
|
||
|
|
||
|
adb_cmd = AdbTools(self.adb_port)
|
||
|
# When relaunch a local instance, we need to pass in retry=True to make
|
||
|
# sure adb device is completely gone since it will use the same adb port
|
||
|
adb_cmd.DisconnectAdb(retry=True)
|
||
|
|
||
|
def GetLock(self):
|
||
|
"""Return the LocalInstanceLock for this object."""
|
||
|
return GetLocalInstanceLock(self._local_instance_id)
|
||
|
|
||
|
@property
|
||
|
def instance_dir(self):
|
||
|
"""Return _instance_dir."""
|
||
|
return self._instance_dir
|
||
|
|
||
|
@property
|
||
|
def instance_id(self):
|
||
|
"""Return _local_instance_id."""
|
||
|
return self._local_instance_id
|
||
|
|
||
|
@property
|
||
|
def virtual_disk_paths(self):
|
||
|
"""Return virtual_disk_paths"""
|
||
|
return self._virtual_disk_paths
|
||
|
|
||
|
@property
|
||
|
def cf_runtime_cfg(self):
|
||
|
"""Return _cf_runtime_cfg"""
|
||
|
return self._cf_runtime_cfg
|
||
|
|
||
|
|
||
|
class LocalGoldfishInstance(Instance):
|
||
|
"""Class to store data of local goldfish instance.
|
||
|
|
||
|
A goldfish instance binds to a console port and an adb port. The console
|
||
|
port is for `adb emu` to send emulator-specific commands. The adb port is
|
||
|
for `adb connect` to start a TCP connection. By convention, the console
|
||
|
port is an even number, and the adb port is the console port + 1. The first
|
||
|
instance uses port 5554 and 5555, the second instance uses 5556 and 5557,
|
||
|
and so on.
|
||
|
"""
|
||
|
|
||
|
_INSTANCE_NAME_PATTERN = re.compile(
|
||
|
r"^local-goldfish-instance-(?P<id>\d+)$")
|
||
|
_INSTANCE_NAME_FORMAT = "local-goldfish-instance-%(id)s"
|
||
|
_EMULATOR_DEFAULT_CONSOLE_PORT = 5554
|
||
|
_DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT = 5585
|
||
|
_DEVICE_SERIAL_FORMAT = "emulator-%(console_port)s"
|
||
|
_DEVICE_SERIAL_PATTERN = re.compile(r"^emulator-(?P<console_port>\d+)$")
|
||
|
|
||
|
def __init__(self, local_instance_id, avd_flavor=None, create_time=None,
|
||
|
x_res=None, y_res=None, dpi=None):
|
||
|
"""Initialize a LocalGoldfishInstance object.
|
||
|
|
||
|
Args:
|
||
|
local_instance_id: Integer of instance id.
|
||
|
avd_flavor: String, the flavor of the virtual device.
|
||
|
create_time: String, the creation date and time.
|
||
|
x_res: Integer of x dimension.
|
||
|
y_res: Integer of y dimension.
|
||
|
dpi: Integer of dpi.
|
||
|
"""
|
||
|
self._id = local_instance_id
|
||
|
adb_port = self.console_port + 1
|
||
|
self._adb = AdbTools(adb_port=adb_port,
|
||
|
device_serial=self.device_serial)
|
||
|
|
||
|
name = self._INSTANCE_NAME_FORMAT % {"id": local_instance_id}
|
||
|
|
||
|
elapsed_time = _GetElapsedTime(create_time) if create_time else None
|
||
|
|
||
|
fullname = _FULL_NAME_STRING % {"device_serial": self.device_serial,
|
||
|
"instance_name": name,
|
||
|
"elapsed_time": elapsed_time}
|
||
|
|
||
|
if x_res and y_res and dpi:
|
||
|
display = _DISPLAY_STRING % {"x_res": x_res, "y_res": y_res,
|
||
|
"dpi": dpi}
|
||
|
else:
|
||
|
display = "unknown"
|
||
|
|
||
|
device_information = (self._adb.device_information if
|
||
|
self._adb.device_information else None)
|
||
|
|
||
|
super().__init__(
|
||
|
name=name, fullname=fullname, display=display, ip="127.0.0.1",
|
||
|
status=None, adb_port=adb_port, avd_type=constants.TYPE_GF,
|
||
|
createtime=create_time, elapsed_time=elapsed_time,
|
||
|
avd_flavor=avd_flavor, is_local=True,
|
||
|
device_information=device_information)
|
||
|
|
||
|
@staticmethod
|
||
|
def _GetInstanceDirRoot():
|
||
|
"""Return the root directory of all instance directories."""
|
||
|
return os.path.join(tempfile.gettempdir(), "acloud_gf_temp")
|
||
|
|
||
|
@property
|
||
|
def adb(self):
|
||
|
"""Return the AdbTools to send emulator commands to this instance."""
|
||
|
return self._adb
|
||
|
|
||
|
@property
|
||
|
def console_port(self):
|
||
|
"""Return the console port as an integer."""
|
||
|
# Emulator requires the console port to be an even number.
|
||
|
return self._EMULATOR_DEFAULT_CONSOLE_PORT + (self._id - 1) * 2
|
||
|
|
||
|
@property
|
||
|
def device_serial(self):
|
||
|
"""Return the serial number that contains the console port."""
|
||
|
return self._DEVICE_SERIAL_FORMAT % {"console_port": self.console_port}
|
||
|
|
||
|
@property
|
||
|
def instance_dir(self):
|
||
|
"""Return the path to instance directory."""
|
||
|
return os.path.join(self._GetInstanceDirRoot(),
|
||
|
self._INSTANCE_NAME_FORMAT % {"id": self._id})
|
||
|
|
||
|
@classmethod
|
||
|
def GetIdByName(cls, name):
|
||
|
"""Get id by name.
|
||
|
|
||
|
Args:
|
||
|
name: String of instance name.
|
||
|
|
||
|
Return:
|
||
|
The instance id as an integer if the name is in valid format.
|
||
|
None if the name does not represent a local goldfish instance.
|
||
|
"""
|
||
|
match = cls._INSTANCE_NAME_PATTERN.match(name)
|
||
|
if match:
|
||
|
return int(match.group("id"))
|
||
|
return None
|
||
|
|
||
|
@classmethod
|
||
|
def GetLockById(cls, instance_id):
|
||
|
"""Get LocalInstanceLock by id."""
|
||
|
lock_path = os.path.join(
|
||
|
cls._GetInstanceDirRoot(),
|
||
|
(cls._INSTANCE_NAME_FORMAT % {"id": instance_id}) + ".lock")
|
||
|
return LocalInstanceLock(lock_path)
|
||
|
|
||
|
def GetLock(self):
|
||
|
"""Return the LocalInstanceLock for this object."""
|
||
|
return self.GetLockById(self._id)
|
||
|
|
||
|
@classmethod
|
||
|
def GetExistingInstances(cls):
|
||
|
"""Get the list of instances that adb can send emu commands to."""
|
||
|
instances = []
|
||
|
for serial in AdbTools.GetDeviceSerials():
|
||
|
match = cls._DEVICE_SERIAL_PATTERN.match(serial)
|
||
|
if not match:
|
||
|
continue
|
||
|
port = int(match.group("console_port"))
|
||
|
instance_id = (port - cls._EMULATOR_DEFAULT_CONSOLE_PORT) // 2 + 1
|
||
|
instances.append(LocalGoldfishInstance(instance_id))
|
||
|
return instances
|
||
|
|
||
|
@classmethod
|
||
|
def GetMaxNumberOfInstances(cls):
|
||
|
"""Get number of emulators that adb can detect."""
|
||
|
max_port = os.environ.get("ADB_LOCAL_TRANSPORT_MAX_PORT",
|
||
|
cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT)
|
||
|
try:
|
||
|
max_port = int(max_port)
|
||
|
except ValueError:
|
||
|
max_port = cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT
|
||
|
if (max_port < cls._EMULATOR_DEFAULT_CONSOLE_PORT or
|
||
|
max_port > constants.MAX_PORT):
|
||
|
max_port = cls._DEFAULT_ADB_LOCAL_TRANSPORT_MAX_PORT
|
||
|
return (max_port + 1 - cls._EMULATOR_DEFAULT_CONSOLE_PORT) // 2
|
||
|
|
||
|
|
||
|
class RemoteInstance(Instance):
|
||
|
"""Class to store data of remote instance."""
|
||
|
|
||
|
# pylint: disable=too-many-locals
|
||
|
def __init__(self, gce_instance):
|
||
|
"""Process the args into class vars.
|
||
|
|
||
|
RemoteInstace initialized by gce dict object. We parse the required data
|
||
|
from gce_instance to local variables.
|
||
|
Reference:
|
||
|
https://cloud.google.com/compute/docs/reference/rest/v1/instances/get
|
||
|
|
||
|
We also gather more details on client side including the forwarding adb
|
||
|
port and vnc port which will be used to determine the status of ssh
|
||
|
tunnel connection.
|
||
|
|
||
|
The status of gce instance will be displayed in _fullname property:
|
||
|
- Connected: If gce instance and ssh tunnel and adb connection are all
|
||
|
active.
|
||
|
- No connected: If ssh tunnel or adb connection is not found.
|
||
|
- Terminated: If we can't retrieve the public ip from gce instance.
|
||
|
|
||
|
Args:
|
||
|
gce_instance: dict object queried from gce.
|
||
|
"""
|
||
|
name = gce_instance.get(constants.INS_KEY_NAME)
|
||
|
|
||
|
create_time = gce_instance.get(constants.INS_KEY_CREATETIME)
|
||
|
elapsed_time = _GetElapsedTime(create_time)
|
||
|
status = gce_instance.get(constants.INS_KEY_STATUS)
|
||
|
zone = self._GetZoneName(gce_instance.get(constants.INS_KEY_ZONE))
|
||
|
|
||
|
ip = None
|
||
|
for network_interface in gce_instance.get("networkInterfaces"):
|
||
|
for access_config in network_interface.get("accessConfigs"):
|
||
|
ip = access_config.get("natIP")
|
||
|
|
||
|
# Get metadata
|
||
|
display = None
|
||
|
avd_type = None
|
||
|
avd_flavor = None
|
||
|
for metadata in gce_instance.get("metadata", {}).get("items", []):
|
||
|
key = metadata["key"]
|
||
|
value = metadata["value"]
|
||
|
if key == constants.INS_KEY_DISPLAY:
|
||
|
display = value
|
||
|
elif key == constants.INS_KEY_AVD_TYPE:
|
||
|
avd_type = value
|
||
|
elif key == constants.INS_KEY_AVD_FLAVOR:
|
||
|
avd_flavor = value
|
||
|
# TODO(176884236): Insert avd information into metadata of instance.
|
||
|
if not avd_type and name.startswith(_ACLOUDWEB_INSTANCE_START_STRING):
|
||
|
avd_type = constants.TYPE_CF
|
||
|
|
||
|
# Find ssl tunnel info.
|
||
|
adb_port = None
|
||
|
vnc_port = None
|
||
|
device_information = None
|
||
|
if ip:
|
||
|
forwarded_ports = self.GetAdbVncPortFromSSHTunnel(ip, avd_type)
|
||
|
adb_port = forwarded_ports.adb_port
|
||
|
vnc_port = forwarded_ports.vnc_port
|
||
|
ssh_tunnel_is_connected = adb_port is not None
|
||
|
|
||
|
adb_device = AdbTools(adb_port)
|
||
|
if adb_device.IsAdbConnected():
|
||
|
device_information = adb_device.device_information
|
||
|
fullname = (_FULL_NAME_STRING %
|
||
|
{"device_serial": "127.0.0.1:%d" % adb_port,
|
||
|
"instance_name": name,
|
||
|
"elapsed_time": elapsed_time})
|
||
|
else:
|
||
|
fullname = (_FULL_NAME_STRING %
|
||
|
{"device_serial": "not connected",
|
||
|
"instance_name": name,
|
||
|
"elapsed_time": elapsed_time})
|
||
|
# If instance is terminated, its ip is None.
|
||
|
else:
|
||
|
ssh_tunnel_is_connected = False
|
||
|
fullname = (_FULL_NAME_STRING %
|
||
|
{"device_serial": "terminated",
|
||
|
"instance_name": name,
|
||
|
"elapsed_time": elapsed_time})
|
||
|
|
||
|
super().__init__(
|
||
|
name=name, fullname=fullname, display=display, ip=ip, status=status,
|
||
|
adb_port=adb_port, vnc_port=vnc_port,
|
||
|
ssh_tunnel_is_connected=ssh_tunnel_is_connected,
|
||
|
createtime=create_time, elapsed_time=elapsed_time, avd_type=avd_type,
|
||
|
avd_flavor=avd_flavor, is_local=False,
|
||
|
device_information=device_information,
|
||
|
zone=zone,
|
||
|
webrtc_port=constants.WEBRTC_LOCAL_PORT)
|
||
|
|
||
|
@staticmethod
|
||
|
def _GetZoneName(zone_info):
|
||
|
"""Get the zone name from the zone information of gce instance.
|
||
|
|
||
|
Zone information is like:
|
||
|
"https://www.googleapis.com/compute/v1/projects/project/zones/us-central1-c"
|
||
|
We want to get "us-central1-c" as zone name.
|
||
|
|
||
|
Args:
|
||
|
zone_info: String, zone information of gce instance.
|
||
|
|
||
|
Returns:
|
||
|
Zone name of gce instance. None if zone name can't find.
|
||
|
"""
|
||
|
zone_match = _RE_ZONE.match(zone_info)
|
||
|
if zone_match:
|
||
|
return zone_match.group("zone")
|
||
|
|
||
|
logger.debug("Can't get zone name from %s.", zone_info)
|
||
|
return None
|
||
|
|
||
|
@staticmethod
|
||
|
def GetAdbVncPortFromSSHTunnel(ip, avd_type):
|
||
|
"""Get forwarding adb and vnc port from ssh tunnel.
|
||
|
|
||
|
Args:
|
||
|
ip: String, ip address.
|
||
|
avd_type: String, the AVD type.
|
||
|
|
||
|
Returns:
|
||
|
NamedTuple ForwardedPorts(vnc_port, adb_port) holding the ports
|
||
|
used in the ssh forwarded call. Both fields are integers.
|
||
|
"""
|
||
|
if avd_type not in utils.AVD_PORT_DICT:
|
||
|
return utils.ForwardedPorts(vnc_port=None, adb_port=None)
|
||
|
|
||
|
default_vnc_port = utils.AVD_PORT_DICT[avd_type].vnc_port
|
||
|
default_adb_port = utils.AVD_PORT_DICT[avd_type].adb_port
|
||
|
# TODO(165888525): Align the SSH tunnel for the order of adb port and
|
||
|
# vnc port.
|
||
|
re_pattern = re.compile(_RE_SSH_TUNNEL_PATTERN %
|
||
|
(_RE_GROUP_ADB, default_adb_port,
|
||
|
_RE_GROUP_VNC, default_vnc_port, ip))
|
||
|
adb_port = None
|
||
|
vnc_port = None
|
||
|
process_output = utils.CheckOutput(constants.COMMAND_PS)
|
||
|
for line in process_output.splitlines():
|
||
|
match = re_pattern.match(line)
|
||
|
if match:
|
||
|
adb_port = int(match.group(_RE_GROUP_ADB))
|
||
|
vnc_port = int(match.group(_RE_GROUP_VNC))
|
||
|
break
|
||
|
|
||
|
logger.debug(("grathering detail for ssh tunnel. "
|
||
|
"IP:%s, forwarding (adb:%d, vnc:%d)"), ip, adb_port,
|
||
|
vnc_port)
|
||
|
|
||
|
return utils.ForwardedPorts(vnc_port=vnc_port, adb_port=adb_port)
|