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