• 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_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