1#!/usr/bin/env python
2#
3# Copyright 2018 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16r"""AVDSpec class.
17
18AVDSpec will take in args from the user and be the main data type that will
19get passed into the create classes. The inferring magic will happen within
20initialization of AVDSpec (like LKGB build id, image branch, etc).
21"""
22
23import glob
24import logging
25import os
26import re
27import subprocess
28import tempfile
29import threading
30
31from acloud import errors
32from acloud.create import create_common
33from acloud.internal import constants
34from acloud.internal.lib import android_build_client
35from acloud.internal.lib import auth
36from acloud.internal.lib import utils
37from acloud.list import list as list_instance
38from acloud.public import config
39
40
41logger = logging.getLogger(__name__)
42
43# Default values for build target.
44_BRANCH_RE = re.compile(r"^Manifest branch: (?P<branch>.+)")
45_COMMAND_REPO_INFO = "repo info platform/tools/acloud"
46_REPO_TIMEOUT = 3
47_CF_ZIP_PATTERN = "*img*.zip"
48_DEFAULT_BUILD_BITNESS = "x86"
49_DEFAULT_BUILD_TYPE = "userdebug"
50_ENV_ANDROID_PRODUCT_OUT = "ANDROID_PRODUCT_OUT"
51_ENV_ANDROID_BUILD_TOP = "ANDROID_BUILD_TOP"
52_GCE_LOCAL_IMAGE_CANDIDATES = ["avd-system.tar.gz",
53                               "android_system_disk_syslinux.img"]
54_LOCAL_ZIP_WARNING_MSG = "'adb sync' will take a long time if using images " \
55                         "built with `m dist`. Building with just `m` will " \
56                         "enable a faster 'adb sync' process."
57_RE_ANSI_ESCAPE = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]")
58_RE_FLAVOR = re.compile(r"^.+_(?P<flavor>.+)-img.+")
59_RE_MEMORY = re.compile(r"(?P<gb_size>\d+)g$|(?P<mb_size>\d+)m$",
60                        re.IGNORECASE)
61_RE_INT = re.compile(r"^\d+$")
62_RE_RES = re.compile(r"^(?P<x_res>\d+)x(?P<y_res>\d+)$")
63_X_RES = "x_res"
64_Y_RES = "y_res"
65_COMMAND_GIT_REMOTE = ["git", "remote"]
66
67# The branch prefix is necessary for the Android Build system to know what we're
68# talking about. For instance, on an aosp remote repo in the master branch,
69# Android Build will recognize it as aosp-master.
70_BRANCH_PREFIX = {"aosp": "aosp-"}
71_DEFAULT_BRANCH_PREFIX = "git_"
72_DEFAULT_BRANCH = "aosp-master"
73
74# The target prefix is needed to help concoct the lunch target name given a
75# the branch, avd type and device flavor:
76# aosp, cf and phone -> aosp_cf_x86_phone.
77_BRANCH_TARGET_PREFIX = {"aosp": "aosp_"}
78
79
80def EscapeAnsi(line):
81    """Remove ANSI control sequences (e.g. temrinal color codes...)
82
83    Args:
84        line: String, one line of command output.
85
86    Returns:
87        String without ANSI code.
88    """
89    return _RE_ANSI_ESCAPE.sub('', line)
90
91
92# pylint: disable=too-many-public-methods
93class AVDSpec():
94    """Class to store data on the type of AVD to create."""
95
96    def __init__(self, args):
97        """Process the args into class vars.
98
99        Args:
100            args: Namespace object from argparse.parse_args.
101        """
102        # Let's define the private class vars here and then process the user
103        # args afterwards.
104        self._client_adb_port = args.adb_port
105        self._autoconnect = None
106        self._instance_name_to_reuse = None
107        self._unlock_screen = None
108        self._report_internal_ip = None
109        self._avd_type = None
110        self._flavor = None
111        self._image_source = None
112        self._instance_type = None
113        self._local_image_dir = None
114        self._local_image_artifact = None
115        self._local_system_image_dir = None
116        self._local_tool_dirs = None
117        self._image_download_dir = None
118        self._num_of_instances = None
119        self._num_avds_per_instance = None
120        self._no_pull_log = None
121        self._remote_image = None
122        self._system_build_info = None
123        self._kernel_build_info = None
124        self._hw_property = None
125        self._remote_host = None
126        self._host_user = None
127        self._host_ssh_private_key_path = None
128        # Create config instance for android_build_client to query build api.
129        self._cfg = config.GetAcloudConfig(args)
130        # Reporting args.
131        self._serial_log_file = None
132        # gpu and emulator_build_id is only used for goldfish avd_type.
133        self._gpu = None
134        self._emulator_build_id = None
135
136        # Fields only used for cheeps type.
137        self._stable_cheeps_host_image_name = None
138        self._stable_cheeps_host_image_project = None
139        self._username = None
140        self._password = None
141
142        # The maximum time in seconds used to wait for the AVD to boot.
143        self._boot_timeout_secs = None
144        # The maximum time in seconds used to wait for the instance ready.
145        self._ins_timeout_secs = None
146
147        # The local instance id
148        self._local_instance_id = None
149
150        self._ProcessArgs(args)
151
152    def __repr__(self):
153        """Let's make it easy to see what this class is holding."""
154        # TODO: I'm pretty sure there's a better way to do this, but I'm not
155        # quite sure what that would be.
156        representation = []
157        representation.append("")
158        representation.append(" - instance_type: %s" % self._instance_type)
159        representation.append(" - avd type: %s" % self._avd_type)
160        representation.append(" - flavor: %s" % self._flavor)
161        representation.append(" - autoconnect: %s" % self._autoconnect)
162        representation.append(" - num of instances requested: %s" %
163                              self._num_of_instances)
164        representation.append(" - image source type: %s" %
165                              self._image_source)
166        image_summary = None
167        image_details = None
168        if self._image_source == constants.IMAGE_SRC_LOCAL:
169            image_summary = "local image dir"
170            image_details = self._local_image_dir
171            representation.append(" - instance id: %s" % self._local_instance_id)
172        elif self._image_source == constants.IMAGE_SRC_REMOTE:
173            image_summary = "remote image details"
174            image_details = self._remote_image
175        representation.append(" - %s: %s" % (image_summary, image_details))
176        representation.append(" - hw properties: %s" %
177                              self._hw_property)
178        return "\n".join(representation)
179
180    def _ProcessArgs(self, args):
181        """Main entry point to process args for the different type of args.
182
183        Split up the arg processing into related areas (image, instance type,
184        etc) so that we don't have one huge monolilthic method that does
185        everything. It makes it easier to review, write tests, and maintain.
186
187        Args:
188            args: Namespace object from argparse.parse_args.
189        """
190        self._ProcessMiscArgs(args)
191        self._ProcessImageArgs(args)
192        self._ProcessHWPropertyArgs(args)
193
194    def _ProcessImageArgs(self, args):
195        """ Process Image Args.
196
197        Args:
198            args: Namespace object from argparse.parse_args.
199        """
200        # If user didn't specify --local-image, infer remote image args
201        if args.local_image == "":
202            self._image_source = constants.IMAGE_SRC_REMOTE
203            if (self._avd_type == constants.TYPE_GF and
204                    self._instance_type != constants.INSTANCE_TYPE_REMOTE):
205                raise errors.UnsupportedInstanceImageType(
206                    "unsupported creation of avd type: %s, "
207                    "instance type: %s, image source: %s" %
208                    (self._avd_type, self._instance_type, self._image_source))
209            self._ProcessRemoteBuildArgs(args)
210        else:
211            self._image_source = constants.IMAGE_SRC_LOCAL
212            self._ProcessLocalImageArgs(args)
213
214        self.image_download_dir = (
215            args.image_download_dir if args.image_download_dir
216            else tempfile.gettempdir())
217
218    @staticmethod
219    def _ParseHWPropertyStr(hw_property_str):
220        """Parse string to dict.
221
222        Args:
223            hw_property_str: A hw properties string.
224
225        Returns:
226            Dict converted from a string.
227
228        Raises:
229            error.MalformedHWPropertyError: If hw_property_str is malformed.
230        """
231        hw_dict = create_common.ParseHWPropertyArgs(hw_property_str)
232        arg_hw_properties = {}
233        for key, value in hw_dict.items():
234            # Parsing HW properties int to avdspec.
235            if key == constants.HW_ALIAS_RESOLUTION:
236                match = _RE_RES.match(value)
237                if match:
238                    arg_hw_properties[_X_RES] = match.group("x_res")
239                    arg_hw_properties[_Y_RES] = match.group("y_res")
240                else:
241                    raise errors.InvalidHWPropertyError(
242                        "[%s] is an invalid resolution. Example:1280x800" % value)
243            elif key in [constants.HW_ALIAS_MEMORY, constants.HW_ALIAS_DISK]:
244                match = _RE_MEMORY.match(value)
245                if match and match.group("gb_size"):
246                    arg_hw_properties[key] = str(
247                        int(match.group("gb_size")) * 1024)
248                elif match and match.group("mb_size"):
249                    arg_hw_properties[key] = match.group("mb_size")
250                else:
251                    raise errors.InvalidHWPropertyError(
252                        "Expected gb size.[%s] is not allowed. Example:4g" % value)
253            elif key in [constants.HW_ALIAS_CPUS, constants.HW_ALIAS_DPI]:
254                if not _RE_INT.match(value):
255                    raise errors.InvalidHWPropertyError(
256                        "%s value [%s] is not an integer." % (key, value))
257                arg_hw_properties[key] = value
258
259        return arg_hw_properties
260
261    def _ProcessHWPropertyArgs(self, args):
262        """Get the HW properties from argparse.parse_args.
263
264        This method will initialize _hw_property in the following
265        manner:
266        1. Get default hw properties from config.
267        2. Override by hw_property args.
268
269        Args:
270            args: Namespace object from argparse.parse_args.
271        """
272        self._cfg.OverrideHwProperty(self._flavor, self._instance_type)
273        self._hw_property = {}
274        self._hw_property = self._ParseHWPropertyStr(self._cfg.hw_property)
275        logger.debug("Default hw property for [%s] flavor: %s", self._flavor,
276                     self._hw_property)
277
278        if args.hw_property:
279            arg_hw_property = self._ParseHWPropertyStr(args.hw_property)
280            logger.debug("Use custom hw property: %s", arg_hw_property)
281            self._hw_property.update(arg_hw_property)
282
283    def _ProcessMiscArgs(self, args):
284        """These args we can take as and don't belong to a group of args.
285
286        Args:
287            args: Namespace object from argparse.parse_args.
288        """
289        self._autoconnect = args.autoconnect
290        self._unlock_screen = args.unlock_screen
291        self._report_internal_ip = args.report_internal_ip
292        self._avd_type = args.avd_type
293        self._flavor = args.flavor or constants.FLAVOR_PHONE
294        if args.remote_host:
295            self._instance_type = constants.INSTANCE_TYPE_HOST
296        else:
297            self._instance_type = (constants.INSTANCE_TYPE_LOCAL
298                                   if args.local_instance else
299                                   constants.INSTANCE_TYPE_REMOTE)
300        self._remote_host = args.remote_host
301        self._host_user = args.host_user
302        self._host_ssh_private_key_path = args.host_ssh_private_key_path
303        self._local_instance_id = args.local_instance
304        self._local_tool_dirs = args.local_tool
305        self._num_of_instances = args.num
306        self._num_avds_per_instance = args.num_avds_per_instance
307        self._no_pull_log = args.no_pull_log
308        self._serial_log_file = args.serial_log_file
309        self._emulator_build_id = args.emulator_build_id
310        self._gpu = args.gpu
311
312        self._stable_cheeps_host_image_name = args.stable_cheeps_host_image_name
313        self._stable_cheeps_host_image_project = args.stable_cheeps_host_image_project
314        self._username = args.username
315        self._password = args.password
316
317        self._boot_timeout_secs = args.boot_timeout_secs
318        self._ins_timeout_secs = args.ins_timeout_secs
319
320        if args.reuse_gce:
321            if args.reuse_gce != constants.SELECT_ONE_GCE_INSTANCE:
322                if list_instance.GetInstancesFromInstanceNames(
323                        self._cfg, [args.reuse_gce]):
324                    self._instance_name_to_reuse = args.reuse_gce
325            if self._instance_name_to_reuse is None:
326                instance = list_instance.ChooseOneRemoteInstance(self._cfg)
327                self._instance_name_to_reuse = instance.name
328
329    @staticmethod
330    def _GetFlavorFromString(flavor_string):
331        """Get flavor name from flavor string.
332
333        Flavor string can come from the zipped image name or the lunch target.
334        e.g.
335        If flavor_string come from zipped name:aosp_cf_x86_phone-img-5455843.zip
336        , then "phone" is the flavor.
337        If flavor_string come from a lunch'd target:aosp_cf_x86_auto-userdebug,
338        then "auto" is the flavor.
339
340        Args:
341            flavor_string: String which contains flavor.It can be a
342                           build target or filename.
343
344        Returns:
345            String of flavor name. None if flavor can't be determined.
346        """
347        for flavor in constants.ALL_FLAVORS:
348            if re.match(r"(.*_)?%s" % flavor, flavor_string):
349                return flavor
350
351        logger.debug("Unable to determine flavor from build target: %s",
352                     flavor_string)
353        return None
354
355    def _ProcessLocalImageArgs(self, args):
356        """Get local image path.
357
358        Args:
359            args: Namespace object from argparse.parse_args.
360        """
361        if self._avd_type == constants.TYPE_CF:
362            self._ProcessCFLocalImageArgs(args.local_image, args.flavor)
363        elif self._avd_type == constants.TYPE_FVP:
364            self._ProcessFVPLocalImageArgs(args.local_image)
365        elif self._avd_type == constants.TYPE_GF:
366            self._local_image_dir = self._ProcessGFLocalImageArgs(
367                args.local_image)
368            if args.local_system_image != "":
369                self._local_system_image_dir = self._ProcessGFLocalImageArgs(
370                    args.local_system_image)
371        elif self._avd_type == constants.TYPE_GCE:
372            self._local_image_artifact = self._GetGceLocalImagePath(
373                args.local_image)
374        else:
375            raise errors.CreateError(
376                "Local image doesn't support the AVD type: %s" % self._avd_type
377            )
378
379    @staticmethod
380    def _GetGceLocalImagePath(local_image_dir):
381        """Get gce local image path.
382
383        Choose image file in local_image_dir over $ANDROID_PRODUCT_OUT.
384        There are various img files so we prioritize returning the one we find
385        first based in the specified order in _GCE_LOCAL_IMAGE_CANDIDATES.
386
387        Args:
388            local_image_dir: A string to specify local image dir.
389
390        Returns:
391            String, image file path if exists.
392
393        Raises:
394            errors.ImgDoesNotExist if image doesn't exist.
395        """
396        # IF the user specified a file, return it
397        if local_image_dir and os.path.isfile(local_image_dir):
398            return local_image_dir
399
400        # If the user didn't specify a dir, assume $ANDROID_PRODUCT_OUT
401        if not local_image_dir:
402            local_image_dir = utils.GetBuildEnvironmentVariable(
403                _ENV_ANDROID_PRODUCT_OUT)
404
405        for img_name in _GCE_LOCAL_IMAGE_CANDIDATES:
406            full_file_path = os.path.join(local_image_dir, img_name)
407            if os.path.exists(full_file_path):
408                return full_file_path
409
410        raise errors.ImgDoesNotExist("Could not find any GCE images (%s), you "
411                                     "can build them via \"m dist\"" %
412                                     ", ".join(_GCE_LOCAL_IMAGE_CANDIDATES))
413
414    @staticmethod
415    def _ProcessGFLocalImageArgs(local_image_arg):
416        """Get local built image path for goldfish.
417
418        Args:
419            local_image_arg: The path to the unzipped update package or SDK
420                             repository, i.e., <target>-img-<build>.zip or
421                             sdk-repo-<os>-system-images-<build>.zip.
422                             If the value is empty, this method returns
423                             ANDROID_PRODUCT_OUT in build environment.
424
425        Returns:
426            String, the path to the image directory.
427
428        Raises:
429            errors.GetLocalImageError if the directory is not found.
430        """
431        image_dir = (local_image_arg if local_image_arg else
432                     utils.GetBuildEnvironmentVariable(
433                         constants.ENV_ANDROID_PRODUCT_OUT))
434
435        if not os.path.isdir(image_dir):
436            raise errors.GetLocalImageError(
437                "%s is not a directory." % image_dir)
438
439        return image_dir
440
441    def _ProcessCFLocalImageArgs(self, local_image_arg, flavor_arg):
442        """Get local built image path for cuttlefish-type AVD.
443
444        Two scenarios of using --local-image:
445        - Without a following argument
446          Set flavor string if the required images are in $ANDROID_PRODUCT_OUT,
447        - With a following filename/dirname
448          Set flavor string from the specified image/dir name.
449
450        Args:
451            local_image_arg: String of local image args.
452            flavor_arg: String of flavor arg
453
454        """
455        flavor_from_build_string = None
456        if not local_image_arg:
457            self._CheckCFBuildTarget(self._instance_type)
458            local_image_path = utils.GetBuildEnvironmentVariable(
459                _ENV_ANDROID_PRODUCT_OUT)
460        else:
461            local_image_path = local_image_arg
462
463        if os.path.isfile(local_image_path):
464            self._local_image_artifact = local_image_arg
465            flavor_from_build_string = self._GetFlavorFromString(
466                self._local_image_artifact)
467            # Since file is provided and I assume it's a zip, so print the
468            # warning message.
469            utils.PrintColorString(_LOCAL_ZIP_WARNING_MSG,
470                                   utils.TextColors.WARNING)
471        else:
472            self._local_image_dir = local_image_path
473            # Since dir is provided, so checking that any images exist to ensure
474            # user didn't forget to 'make' before launch AVD.
475            image_list = glob.glob(os.path.join(self.local_image_dir, "*.img"))
476            if not image_list:
477                raise errors.GetLocalImageError(
478                    "No image found(Did you choose a lunch target and run `m`?)"
479                    ": %s.\n " % self.local_image_dir)
480
481            try:
482                flavor_from_build_string = self._GetFlavorFromString(
483                    utils.GetBuildEnvironmentVariable(constants.ENV_BUILD_TARGET))
484            except errors.GetAndroidBuildEnvVarError:
485                logger.debug("Unable to determine flavor from env variable: %s",
486                             constants.ENV_BUILD_TARGET)
487
488        if flavor_from_build_string and not flavor_arg:
489            self._flavor = flavor_from_build_string
490
491    def _ProcessFVPLocalImageArgs(self, local_image_arg):
492        """Get local built image path for FVP-type AVD.
493
494        Args:
495            local_image_arg: String of local image args.
496        """
497        build_target = utils.GetBuildEnvironmentVariable(
498            constants.ENV_BUILD_TARGET)
499        if build_target != "fvp":
500            utils.PrintColorString(
501                "%s is not an fvp target (Try lunching fvp-eng "
502                "and running 'm')" % build_target,
503                utils.TextColors.WARNING)
504        self._local_image_dir = utils.GetBuildEnvironmentVariable(
505            _ENV_ANDROID_PRODUCT_OUT)
506
507        # Since dir is provided, so checking that any images exist to ensure
508        # user didn't forget to 'make' before launch AVD.
509        image_list = glob.glob(os.path.join(self.local_image_dir, "*.img"))
510        if not image_list:
511            raise errors.GetLocalImageError(
512                "No image found(Did you choose a lunch target and run `m`?)"
513                ": %s.\n " % self._local_image_dir)
514
515    def _ProcessRemoteBuildArgs(self, args):
516        """Get the remote build args.
517
518        Some of the acloud magic happens here, we will infer some of these
519        values if the user hasn't specified them.
520
521        Args:
522            args: Namespace object from argparse.parse_args.
523        """
524        self._remote_image = {}
525        self._remote_image[constants.BUILD_BRANCH] = args.branch
526        if not self._remote_image[constants.BUILD_BRANCH]:
527            self._remote_image[constants.BUILD_BRANCH] = self._GetBuildBranch(
528                args.build_id, args.build_target)
529
530        self._remote_image[constants.BUILD_TARGET] = args.build_target
531        if not self._remote_image[constants.BUILD_TARGET]:
532            self._remote_image[constants.BUILD_TARGET] = self._GetBuildTarget(args)
533        else:
534            # If flavor isn't specified, try to infer it from build target,
535            # if we can't, just default to phone flavor.
536            self._flavor = args.flavor or self._GetFlavorFromString(
537                self._remote_image[constants.BUILD_TARGET]) or constants.FLAVOR_PHONE
538            # infer avd_type from build_target.
539            for avd_type, avd_type_abbr in constants.AVD_TYPES_MAPPING.items():
540                if re.match(r"(.*_)?%s_" % avd_type_abbr,
541                            self._remote_image[constants.BUILD_TARGET]):
542                    self._avd_type = avd_type
543                    break
544
545        self._remote_image[constants.BUILD_ID] = args.build_id
546        if not self._remote_image[constants.BUILD_ID]:
547            build_client = android_build_client.AndroidBuildClient(
548                auth.CreateCredentials(self._cfg))
549
550            self._remote_image[constants.BUILD_ID] = build_client.GetLKGB(
551                self._remote_image[constants.BUILD_TARGET],
552                self._remote_image[constants.BUILD_BRANCH])
553
554        self._remote_image[constants.CHEEPS_BETTY_IMAGE] = (
555            args.cheeps_betty_image)
556
557        # Process system image and kernel image.
558        self._system_build_info = {constants.BUILD_ID: args.system_build_id,
559                                   constants.BUILD_BRANCH: args.system_branch,
560                                   constants.BUILD_TARGET: args.system_build_target}
561        self._kernel_build_info = {constants.BUILD_ID: args.kernel_build_id,
562                                   constants.BUILD_BRANCH: args.kernel_branch,
563                                   constants.BUILD_TARGET: args.kernel_build_target}
564
565    @staticmethod
566    def _CheckCFBuildTarget(instance_type):
567        """Check build target for the given instance type
568
569        Args:
570            instance_type: String of instance type
571
572        Raises:
573            errors.GetLocalImageError if the pattern is not match with
574                current build target.
575        """
576        build_target = utils.GetBuildEnvironmentVariable(
577            constants.ENV_BUILD_TARGET)
578        pattern = constants.CF_AVD_BUILD_TARGET_PATTERN_MAPPING[instance_type]
579        if pattern not in build_target:
580            utils.PrintColorString(
581                "%s is not a %s target (Try lunching a proper cuttlefish "
582                "target and running 'm')" % (build_target, pattern),
583                utils.TextColors.WARNING)
584
585    @staticmethod
586    def _GetGitRemote():
587        """Get the remote repo.
588
589        We'll go to a project we know exists (tools/acloud) and grab the git
590        remote output from there.
591
592        Returns:
593            remote: String, git remote (e.g. "aosp").
594        """
595        try:
596            android_build_top = os.environ[constants.ENV_ANDROID_BUILD_TOP]
597        except KeyError:
598            raise errors.GetAndroidBuildEnvVarError(
599                "Could not get environment var: %s\n"
600                "Try to run '#source build/envsetup.sh && lunch <target>'"
601                % _ENV_ANDROID_BUILD_TOP
602            )
603
604        acloud_project = os.path.join(android_build_top, "tools", "acloud")
605        return EscapeAnsi(utils.CheckOutput(_COMMAND_GIT_REMOTE,
606                                            cwd=acloud_project).strip())
607
608    def _GetBuildBranch(self, build_id, build_target):
609        """Infer build branch if user didn't specify branch name.
610
611        Args:
612            build_id: String, Build id, e.g. "2263051", "P2804227"
613            build_target: String, the build target, e.g. cf_x86_phone-userdebug
614
615        Returns:
616            String, name of build branch.
617        """
618        # Infer branch from build_target and build_id
619        if build_id and build_target:
620            build_client = android_build_client.AndroidBuildClient(
621                auth.CreateCredentials(self._cfg))
622            return build_client.GetBranch(build_target, build_id)
623
624        return self._GetBranchFromRepo()
625
626    def _GetBranchFromRepo(self):
627        """Get branch information from command "repo info".
628
629        If branch can't get from "repo info", it will be set as default branch
630        "aosp-master".
631
632        Returns:
633            branch: String, git branch name. e.g. "aosp-master"
634        """
635        branch = None
636        # TODO(149460014): Migrate acloud to py3, then remove this
637        # workaround.
638        env = os.environ.copy()
639        env.pop("PYTHONPATH", None)
640        logger.info("Running command \"%s\"", _COMMAND_REPO_INFO)
641        # TODO(154173071): Migrate acloud to py3, then apply Popen to append with encoding
642        process = subprocess.Popen(_COMMAND_REPO_INFO, shell=True, stdin=None,
643                                   stdout=subprocess.PIPE,
644                                   stderr=subprocess.STDOUT, env=env)
645        timer = threading.Timer(_REPO_TIMEOUT, process.kill)
646        timer.start()
647        stdout, _ = process.communicate()
648        if stdout:
649            for line in stdout.splitlines():
650                match = _BRANCH_RE.match(EscapeAnsi(line.decode()))
651                if match:
652                    branch_prefix = _BRANCH_PREFIX.get(self._GetGitRemote(),
653                                                       _DEFAULT_BRANCH_PREFIX)
654                    branch = branch_prefix + match.group("branch")
655        timer.cancel()
656        if branch:
657            return branch
658        utils.PrintColorString(
659            "Unable to determine your repo branch, defaulting to %s"
660            % _DEFAULT_BRANCH, utils.TextColors.WARNING)
661        return _DEFAULT_BRANCH
662
663    def _GetBuildTarget(self, args):
664        """Infer build target if user doesn't specified target name.
665
666        Target = {REPO_PREFIX}{avd_type}_{bitness}_{flavor}-
667            {DEFAULT_BUILD_TARGET_TYPE}.
668        Example target: aosp_cf_x86_phone-userdebug
669
670        Args:
671            args: Namespace object from argparse.parse_args.
672
673        Returns:
674            build_target: String, name of build target.
675        """
676        branch = re.split("-|_", self._remote_image[constants.BUILD_BRANCH])[0]
677        return "%s%s_%s_%s-%s" % (
678            _BRANCH_TARGET_PREFIX.get(branch, ""),
679            constants.AVD_TYPES_MAPPING[args.avd_type],
680            _DEFAULT_BUILD_BITNESS, self._flavor,
681            _DEFAULT_BUILD_TYPE)
682
683    @property
684    def instance_type(self):
685        """Return the instance type."""
686        return self._instance_type
687
688    @property
689    def image_source(self):
690        """Return the image type."""
691        return self._image_source
692
693    @property
694    def hw_property(self):
695        """Return the hw_property."""
696        return self._hw_property
697
698    @property
699    def local_image_dir(self):
700        """Return local image dir."""
701        return self._local_image_dir
702
703    @property
704    def local_image_artifact(self):
705        """Return local image artifact."""
706        return self._local_image_artifact
707
708    @property
709    def local_system_image_dir(self):
710        """Return local system image dir."""
711        return self._local_system_image_dir
712
713    @property
714    def local_tool_dirs(self):
715        """Return a list of local tool directories."""
716        return self._local_tool_dirs
717
718    @property
719    def avd_type(self):
720        """Return the avd type."""
721        return self._avd_type
722
723    @property
724    def autoconnect(self):
725        """autoconnect.
726
727        args.autoconnect could pass as Boolean or String.
728
729        Return: Boolean, True only if self._autoconnect is not False.
730        """
731        return self._autoconnect is not False
732
733    @property
734    def connect_adb(self):
735        """Auto-connect to adb.
736
737        Return: Boolean, whether autoconnect is enabled.
738        """
739        return self._autoconnect is not False
740
741    @property
742    def connect_vnc(self):
743        """Launch vnc.
744
745        Return: Boolean, True if self._autoconnect is 'vnc'.
746        """
747        return self._autoconnect == constants.INS_KEY_VNC
748
749    @property
750    def connect_webrtc(self):
751        """Auto-launch webRTC AVD on the browser.
752
753        Return: Boolean, True if args.autoconnect is "webrtc".
754        """
755        return self._autoconnect == constants.INS_KEY_WEBRTC
756
757    @property
758    def unlock_screen(self):
759        """Return unlock_screen."""
760        return self._unlock_screen
761
762    @property
763    def remote_image(self):
764        """Return the remote image."""
765        return self._remote_image
766
767    @property
768    def num(self):
769        """Return num of instances."""
770        return self._num_of_instances
771
772    @property
773    def num_avds_per_instance(self):
774        """Return num_avds_per_instance."""
775        return self._num_avds_per_instance
776
777    @property
778    def report_internal_ip(self):
779        """Return report internal ip."""
780        return self._report_internal_ip
781
782    @property
783    def kernel_build_info(self):
784        """Return kernel build info."""
785        return self._kernel_build_info
786
787    @property
788    def flavor(self):
789        """Return flavor."""
790        return self._flavor
791
792    @property
793    def cfg(self):
794        """Return cfg instance."""
795        return self._cfg
796
797    @property
798    def image_download_dir(self):
799        """Return image download dir."""
800        return self._image_download_dir
801
802    @image_download_dir.setter
803    def image_download_dir(self, value):
804        """Set image download dir."""
805        self._image_download_dir = value
806
807    @property
808    def serial_log_file(self):
809        """Return serial log file path."""
810        return self._serial_log_file
811
812    @property
813    def gpu(self):
814        """Return gpu."""
815        return self._gpu
816
817    @property
818    def emulator_build_id(self):
819        """Return emulator_build_id."""
820        return self._emulator_build_id
821
822    @property
823    def client_adb_port(self):
824        """Return the client adb port."""
825        return self._client_adb_port
826
827    @property
828    def stable_cheeps_host_image_name(self):
829        """Return the Cheeps host image name."""
830        return self._stable_cheeps_host_image_name
831
832    # pylint: disable=invalid-name
833    @property
834    def stable_cheeps_host_image_project(self):
835        """Return the project hosting the Cheeps host image."""
836        return self._stable_cheeps_host_image_project
837
838    @property
839    def username(self):
840        """Return username."""
841        return self._username
842
843    @property
844    def password(self):
845        """Return password."""
846        return self._password
847
848    @property
849    def boot_timeout_secs(self):
850        """Return boot_timeout_secs."""
851        return self._boot_timeout_secs
852
853    @property
854    def ins_timeout_secs(self):
855        """Return ins_timeout_secs."""
856        return self._ins_timeout_secs
857
858    @property
859    def system_build_info(self):
860        """Return system_build_info."""
861        return self._system_build_info
862
863    @property
864    def local_instance_id(self):
865        """Return local_instance_id."""
866        return self._local_instance_id
867
868    @property
869    def instance_name_to_reuse(self):
870        """Return instance_name_to_reuse."""
871        return self._instance_name_to_reuse
872
873    @property
874    def remote_host(self):
875        """Return host."""
876        return self._remote_host
877
878    @property
879    def host_user(self):
880        """Return host_user."""
881        return self._host_user
882
883    @property
884    def host_ssh_private_key_path(self):
885        """Return host_ssh_private_key_path."""
886        return self._host_ssh_private_key_path
887
888    @property
889    def no_pull_log(self):
890        """Return no_pull_log."""
891        return self._no_pull_log
892