• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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