• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 - 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.
14"""Common Utilities."""
15# pylint: disable=too-many-lines
16from __future__ import print_function
17
18import base64
19import binascii
20import collections
21import errno
22import getpass
23import grp
24import logging
25import os
26import platform
27import re
28import shlex
29import shutil
30import signal
31import struct
32import socket
33import stat
34import subprocess
35import sys
36import tarfile
37import tempfile
38import time
39import uuid
40import webbrowser
41import zipfile
42
43from acloud import errors
44from acloud.internal import constants
45
46
47logger = logging.getLogger(__name__)
48
49SSH_KEYGEN_CMD = ["ssh-keygen", "-t", "rsa", "-b", "4096"]
50SSH_KEYGEN_PUB_CMD = ["ssh-keygen", "-y"]
51SSH_ARGS = ["-o", "UserKnownHostsFile=/dev/null",
52            "-o", "StrictHostKeyChecking=no"]
53SSH_CMD = ["ssh"] + SSH_ARGS
54SCP_CMD = ["scp"] + SSH_ARGS
55GET_BUILD_VAR_CMD = ["build/soong/soong_ui.bash", "--dumpvar-mode"]
56DEFAULT_RETRY_BACKOFF_FACTOR = 1
57DEFAULT_SLEEP_MULTIPLIER = 0
58
59_SSH_TUNNEL_ARGS = (
60    "-i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null "
61    "-o StrictHostKeyChecking=no "
62    "%(port_mapping)s"
63    "-N -f -l %(ssh_user)s %(ip_addr)s")
64_SSH_COMMAND_PS = (
65    "exec %(ssh_bin)s -i %(rsa_key_file)s -o UserKnownHostsFile=/dev/null "
66    "-o StrictHostKeyChecking=no %(extra_args)s -l %(ssh_user)s %(ip_addr)s "
67    "ps aux")
68PORT_MAPPING = "-L %(local_port)d:127.0.0.1:%(target_port)d "
69_RELEASE_PORT_CMD = "kill $(lsof -t -i :%d)"
70_WEBRTC_OPERATOR_PATTERN = re.compile(r"(.+)(webrtc_operator )(.+)")
71_PORT_8443 = 8443
72_PORT_1443 = 1443
73PortMapping = collections.namedtuple("PortMapping", ["local", "target"])
74# Acloud uses only part of default webrtc port range to support both local and remote.
75# The default webrtc port range is [15550, 15599].
76WEBRTC_PORT_START = 15555
77WEBRTC_PORT_END = 15579
78WEBRTC_PORTS_MAPPING = [PortMapping(port, port) for port in range(WEBRTC_PORT_START, WEBRTC_PORT_END + 1)]
79_RE_GROUP_WEBRTC = "local_webrtc_port"
80_RE_WEBRTC_SSH_TUNNEL_PATTERN = (
81    r"((.*-L\s)(?P<local_webrtc_port>\d+):127.0.0.1:%s)(.+%s)")
82_ADB_CONNECT_ARGS = "connect 127.0.0.1:%(adb_port)d"
83# Store the ports that vnc/adb are forwarded to, both are integers.
84ForwardedPorts = collections.namedtuple("ForwardedPorts", [constants.VNC_PORT,
85                                                           constants.ADB_PORT,
86                                                           constants.FASTBOOT_PORT])
87
88AVD_PORT_DICT = {
89    constants.TYPE_GCE: ForwardedPorts(constants.GCE_VNC_PORT,
90                                       constants.GCE_ADB_PORT,
91                                       None),
92    constants.TYPE_CF: ForwardedPorts(constants.CF_VNC_PORT,
93                                      constants.CF_ADB_PORT,
94                                      constants.CF_FASTBOOT_PORT),
95    constants.TYPE_GF: ForwardedPorts(constants.GF_VNC_PORT,
96                                      constants.GF_ADB_PORT,
97                                      None),
98    constants.TYPE_CHEEPS: ForwardedPorts(constants.CHEEPS_VNC_PORT,
99                                          constants.CHEEPS_ADB_PORT,
100                                          None),
101    constants.TYPE_FVP: ForwardedPorts(None, constants.FVP_ADB_PORT, None),
102}
103
104_VNC_BIN = "ssvnc"
105# search_dirs and the files can be symbolic links. The -H flag makes the
106# command skip the links except search_dirs. The returned files are unique.
107_CMD_FIND_FILES = "find -H %(search_dirs)s -type f"
108_CMD_KILL = ["pkill", "-9", "-f"]
109_CMD_SG = "sg "
110_CMD_START_VNC = "%(bin)s vnc://127.0.0.1:%(port)d"
111_CMD_INSTALL_SSVNC = "sudo apt-get --assume-yes install ssvnc"
112_ENV_DISPLAY = "DISPLAY"
113_SSVNC_ENV_VARS = {"SSVNC_NO_ENC_WARN": "1", "SSVNC_SCALE": "auto", "VNCVIEWER_X11CURSOR": "1"}
114_DEFAULT_DISPLAY_SCALE = 1.0
115_DIST_DIR = "DIST_DIR"
116
117# For webrtc
118_WEBRTC_URL = "https://%(webrtc_ip)s:%(webrtc_port)d"
119
120_CONFIRM_CONTINUE = ("In order to display the screen to the AVD, we'll need to "
121                     "install a vnc client (ssvnc). \nWould you like acloud to "
122                     "install it for you? (%s) \nPress 'y' to continue or "
123                     "anything else to abort it[y/N]: ") % _CMD_INSTALL_SSVNC
124_EvaluatedResult = collections.namedtuple("EvaluatedResult",
125                                          ["is_result_ok", "result_message"])
126# dict of supported system and their distributions.
127_SUPPORTED_SYSTEMS_AND_DISTS = {"Linux": ["Ubuntu", "ubuntu", "Debian", "debian"]}
128_DEFAULT_TIMEOUT_ERR = "Function did not complete within %d secs."
129_SSVNC_VIEWER_PATTERN = "vnc://127.0.0.1:%(vnc_port)d"
130
131# Determine the environment whether to support kvm.
132_KVM_PATH = "/dev/kvm"
133
134
135class TempDir:
136    """A context manager that ceates a temporary directory.
137
138    Attributes:
139        path: The path of the temporary directory.
140    """
141
142    def __init__(self):
143        self.path = tempfile.mkdtemp()
144        os.chmod(self.path, 0o700)
145        logger.debug("Created temporary dir %s", self.path)
146
147    def __enter__(self):
148        """Enter."""
149        return self.path
150
151    def __exit__(self, exc_type, exc_value, traceback):
152        """Exit.
153
154        Args:
155            exc_type: Exception type raised within the context manager.
156                      None if no execption is raised.
157            exc_value: Exception instance raised within the context manager.
158                       None if no execption is raised.
159            traceback: Traceback for exeception that is raised within
160                       the context manager.
161                       None if no execption is raised.
162        Raises:
163            EnvironmentError or OSError when failed to delete temp directory.
164        """
165        try:
166            if self.path:
167                shutil.rmtree(self.path)
168                logger.debug("Deleted temporary dir %s", self.path)
169        except EnvironmentError as e:
170            # Ignore error if there is no exception raised
171            # within the with-clause and the EnvironementError is
172            # about problem that directory or file does not exist.
173            if not exc_type and e.errno != errno.ENOENT:
174                raise
175        except Exception as e:  # pylint: disable=W0703
176            if exc_type:
177                logger.error(
178                    "Encountered error while deleting %s: %s",
179                    self.path,
180                    str(e),
181                    exc_info=True)
182            else:
183                raise
184
185
186def RetryOnException(retry_checker,
187                     max_retries,
188                     sleep_multiplier=0,
189                     retry_backoff_factor=1):
190    """Decorater which retries the function call if |retry_checker| returns true.
191
192    Args:
193        retry_checker: A callback function which should take an exception instance
194                       and return True if functor(*args, **kwargs) should be retried
195                       when such exception is raised, and return False if it should
196                       not be retried.
197        max_retries: Maximum number of retries allowed.
198        sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
199                          retry_backoff_factor is 1.  Will sleep
200                          sleep_multiplier * (
201                              retry_backoff_factor ** (attempt_count -  1))
202                          if retry_backoff_factor != 1.
203        retry_backoff_factor: See explanation of sleep_multiplier.
204
205    Returns:
206        The function wrapper.
207    """
208
209    def _Wrapper(func):
210        def _FunctionWrapper(*args, **kwargs):
211            return Retry(retry_checker, max_retries, func, sleep_multiplier,
212                         retry_backoff_factor, *args, **kwargs)
213
214        return _FunctionWrapper
215
216    return _Wrapper
217
218
219def Retry(retry_checker, max_retries, functor, sleep_multiplier,
220          retry_backoff_factor, *args, **kwargs):
221    """Conditionally retry a function.
222
223    Args:
224        retry_checker: A callback function which should take an exception instance
225                       and return True if functor(*args, **kwargs) should be retried
226                       when such exception is raised, and return False if it should
227                       not be retried.
228        max_retries: Maximum number of retries allowed.
229        functor: The function to call, will call functor(*args, **kwargs).
230        sleep_multiplier: Will sleep sleep_multiplier * attempt_count seconds if
231                          retry_backoff_factor is 1.  Will sleep
232                          sleep_multiplier * (
233                              retry_backoff_factor ** (attempt_count -  1))
234                          if retry_backoff_factor != 1.
235        retry_backoff_factor: See explanation of sleep_multiplier.
236        *args: Arguments to pass to the functor.
237        **kwargs: Key-val based arguments to pass to the functor.
238
239    Returns:
240        The return value of the functor.
241
242    Raises:
243        Exception: The exception that functor(*args, **kwargs) throws.
244    """
245    attempt_count = 0
246    while attempt_count <= max_retries:
247        try:
248            attempt_count += 1
249            return_value = functor(*args, **kwargs)
250            return return_value
251        except Exception as e:  # pylint: disable=W0703
252            if retry_checker(e) and attempt_count <= max_retries:
253                if retry_backoff_factor != 1:
254                    sleep = sleep_multiplier * (retry_backoff_factor**
255                                                (attempt_count - 1))
256                else:
257                    sleep = sleep_multiplier * attempt_count
258                time.sleep(sleep)
259            else:
260                raise
261
262
263def RetryExceptionType(exception_types, max_retries, functor, *args, **kwargs):
264    """Retry exception if it is one of the given types.
265
266    Args:
267        exception_types: A tuple of exception types, e.g. (ValueError, KeyError)
268        max_retries: Max number of retries allowed.
269        functor: The function to call. Will be retried if exception is raised and
270                 the exception is one of the exception_types.
271        *args: Arguments to pass to Retry function.
272        **kwargs: Key-val based arguments to pass to Retry functions.
273
274    Returns:
275        The value returned by calling functor.
276    """
277    return Retry(lambda e: isinstance(e, exception_types), max_retries,
278                 functor, *args, **kwargs)
279
280
281def PollAndWait(func, expected_return, timeout_exception, timeout_secs,
282                sleep_interval_secs, *args, **kwargs):
283    """Call a function until the function returns expected value or times out.
284
285    Args:
286        func: Function to call.
287        expected_return: The expected return value.
288        timeout_exception: Exception to raise when it hits timeout.
289        timeout_secs: Timeout seconds.
290                      If 0 or less than zero, the function will run once and
291                      we will not wait on it.
292        sleep_interval_secs: Time to sleep between two attemps.
293        *args: list of args to pass to func.
294        **kwargs: dictionary of keyword based args to pass to func.
295
296    Raises:
297        timeout_exception: if the run of function times out.
298    """
299    # TODO(fdeng): Currently this method does not kill
300    # |func|, if |func| takes longer than |timeout_secs|.
301    # We can use a more robust version from chromite.
302    start = time.time()
303    while True:
304        return_value = func(*args, **kwargs)
305        if return_value == expected_return:
306            return
307        if time.time() - start > timeout_secs:
308            raise timeout_exception
309        if sleep_interval_secs > 0:
310            time.sleep(sleep_interval_secs)
311
312
313def GenerateUniqueName(prefix=None, suffix=None):
314    """Generate a random unique name using uuid4.
315
316    Args:
317        prefix: String, desired prefix to prepend to the generated name.
318        suffix: String, desired suffix to append to the generated name.
319
320    Returns:
321        String, a random name.
322    """
323    name = uuid.uuid4().hex
324    if prefix:
325        name = "-".join([prefix, name])
326    if suffix:
327        name = "-".join([name, suffix])
328    return name
329
330
331def MakeTarFile(src_dict, dest):
332    """Archive files in tar.gz format to a file named as |dest|.
333
334    Args:
335        src_dict: A dictionary that maps a path to be archived
336                  to the corresponding name that appears in the archive.
337        dest: String, path to output file, e.g. /tmp/myfile.tar.gz
338    """
339    logger.info("Compressing %s into %s.", src_dict.keys(), dest)
340    with tarfile.open(dest, "w:gz") as tar:
341        for src, arcname in src_dict.items():
342            tar.add(src, arcname=arcname)
343
344def CreateSshKeyPairIfNotExist(private_key_path, public_key_path):
345    """Create the ssh key pair if they don't exist.
346
347    Case1. If the private key doesn't exist, we will create both the public key
348           and the private key.
349    Case2. If the private key exists but public key doesn't, we will create the
350           public key by using the private key.
351    Case3. If the public key exists but the private key doesn't, we will create
352           a new private key and overwrite the public key.
353
354    Args:
355        private_key_path: Path to the private key file.
356                          e.g. ~/.ssh/acloud_rsa
357        public_key_path: Path to the public key file.
358                         e.g. ~/.ssh/acloud_rsa.pub
359
360    Raises:
361        error.DriverError: If failed to create the key pair.
362    """
363    public_key_path = os.path.expanduser(public_key_path)
364    private_key_path = os.path.expanduser(private_key_path)
365    public_key_exist = os.path.exists(public_key_path)
366    private_key_exist = os.path.exists(private_key_path)
367    if public_key_exist and private_key_exist:
368        logger.debug(
369            "The ssh private key (%s) and public key (%s) already exist,"
370            "will not automatically create the key pairs.", private_key_path,
371            public_key_path)
372        return
373    key_folder = os.path.dirname(private_key_path)
374    if not os.path.exists(key_folder):
375        os.makedirs(key_folder)
376    try:
377        if private_key_exist:
378            cmd = SSH_KEYGEN_PUB_CMD + ["-f", private_key_path]
379            with open(public_key_path, 'w') as outfile:
380                stream_content = CheckOutput(cmd)
381                outfile.write(
382                    stream_content.rstrip('\n') + " " + getpass.getuser())
383            logger.info(
384                "The ssh public key (%s) do not exist, "
385                "automatically creating public key, calling: %s",
386                public_key_path, " ".join(cmd))
387        else:
388            cmd = SSH_KEYGEN_CMD + [
389                "-C", getpass.getuser(), "-f", private_key_path
390            ]
391            logger.info(
392                "Creating public key from private key (%s) via cmd: %s",
393                private_key_path, " ".join(cmd))
394            subprocess.check_call(cmd, stdout=sys.stderr, stderr=sys.stdout)
395    except subprocess.CalledProcessError as e:
396        raise errors.DriverError("Failed to create ssh key pair: %s" % str(e))
397    except OSError as e:
398        raise errors.DriverError(
399            "Failed to create ssh key pair, please make sure "
400            "'ssh-keygen' is installed: %s" % str(e))
401
402    # By default ssh-keygen will create a public key file
403    # by append .pub to the private key file name. Rename it
404    # to what's requested by public_key_path.
405    default_pub_key_path = "%s.pub" % private_key_path
406    try:
407        if default_pub_key_path != public_key_path:
408            os.rename(default_pub_key_path, public_key_path)
409    except OSError as e:
410        raise errors.DriverError(
411            "Failed to rename %s to %s: %s" % (default_pub_key_path,
412                                               public_key_path, str(e)))
413
414    logger.info("Created ssh private key (%s) and public key (%s)",
415                private_key_path, public_key_path)
416
417
418def VerifyRsaPubKey(rsa):
419    """Verify the format of rsa public key.
420
421    Args:
422        rsa: content of rsa public key. It should follow the format of
423             ssh-rsa AAAAB3NzaC1yc2EA.... test@test.com
424
425    Raises:
426        DriverError if the format is not correct.
427    """
428    if not rsa or not all(ord(c) < 128 for c in rsa):
429        raise errors.DriverError(
430            "rsa key is empty or contains non-ascii character: %s" % rsa)
431
432    elements = rsa.split()
433    if len(elements) != 3:
434        raise errors.DriverError("rsa key is invalid, wrong format: %s" % rsa)
435
436    key_type, data, _ = elements
437    try:
438        binary_data = base64.decodebytes(data.encode())
439        # number of bytes of int type
440        int_length = 4
441        # binary_data is like "7ssh-key..." in a binary format.
442        # The first 4 bytes should represent 7, which should be
443        # the length of the following string "ssh-key".
444        # And the next 7 bytes should be string "ssh-key".
445        # We will verify that the rsa conforms to this format.
446        # ">I" in the following line means "big-endian unsigned integer".
447        type_length = struct.unpack(">I", binary_data[:int_length])[0]
448        if binary_data[int_length:int_length + type_length] != key_type.encode():
449            raise errors.DriverError("rsa key is invalid: %s" % rsa)
450    except (struct.error, binascii.Error) as e:
451        raise errors.DriverError(
452            "rsa key is invalid: %s, error: %s" % (rsa, str(e)))
453
454
455def Decompress(sourcefile, dest=None):
456    """Decompress .zip or .tar.gz.
457
458    Args:
459        sourcefile: A string, a source file path to decompress.
460        dest: A string, a folder path as decompress destination.
461
462    Raises:
463        errors.UnsupportedCompressionFileType: Not supported extension.
464    """
465    logger.info("Start to decompress %s!", sourcefile)
466    dest_path = dest if dest else "."
467    if sourcefile.endswith(".tar.gz"):
468        with tarfile.open(sourcefile, "r:gz") as compressor:
469            compressor.extractall(dest_path)
470    elif sourcefile.endswith(".zip"):
471        with zipfile.ZipFile(sourcefile, 'r') as compressor:
472            compressor.extractall(dest_path)
473    else:
474        raise errors.UnsupportedCompressionFileType(
475            "Sorry, we could only support compression file type "
476            "for zip or tar.gz.")
477
478
479# pylint: disable=no-init
480class TextColors:
481    """A class that defines common color ANSI code."""
482
483    HEADER = "\033[95m"
484    OKBLUE = "\033[94m"
485    OKGREEN = "\033[92m"
486    WARNING = "\033[33m"
487    FAIL = "\033[91m"
488    ENDC = "\033[0m"
489    BOLD = "\033[1m"
490    UNDERLINE = "\033[4m"
491
492
493def PrintColorString(message, colors=TextColors.OKBLUE, **kwargs):
494    """A helper function to print out colored text.
495
496    Use print function "print(message, end="")" to show message in one line.
497    Example code:
498        DisplayMessages("Creating GCE instance...", end="")
499        # Job execute 20s
500        DisplayMessages("Done! (20s)")
501    Display:
502        Creating GCE instance...
503        # After job finished, messages update as following:
504        Creating GCE instance...Done! (20s)
505
506    Args:
507        message: String, the message text.
508        colors: String, color code.
509        **kwargs: dictionary of keyword based args to pass to func.
510    """
511    print(colors + message + TextColors.ENDC, **kwargs)
512    sys.stdout.flush()
513
514
515def InteractWithQuestion(question, colors=TextColors.WARNING):
516    """A helper function to define the common way to run interactive cmd.
517
518    Args:
519        question: String, the question to ask user.
520        colors: String, color code.
521
522    Returns:
523        String, input from user.
524    """
525    return str(input(colors + question + TextColors.ENDC).strip())
526
527
528def GetUserAnswerYes(question):
529    """Ask user about acloud setup question.
530
531    Args:
532        question: String of question for user. Enter is equivalent to pressing
533                  n. We should hint user with upper case N surrounded in square
534                  brackets.
535                  Ex: "Are you sure to change bucket name[y/N]:"
536
537    Returns:
538        Boolean, True if answer is "Yes", False otherwise.
539    """
540    answer = InteractWithQuestion(question)
541    return answer.lower() in constants.USER_ANSWER_YES
542
543
544class BatchHttpRequestExecutor:
545    """A helper class that executes requests in batch with retry.
546
547    This executor executes http requests in a batch and retry
548    those that have failed. It iteratively updates the dictionary
549    self._final_results with latest results, which can be retrieved
550    via GetResults.
551    """
552
553    def __init__(self,
554                 execute_once_functor,
555                 requests,
556                 retry_http_codes=None,
557                 max_retry=None,
558                 sleep=None,
559                 backoff_factor=None,
560                 other_retriable_errors=None):
561        """Initializes the executor.
562
563        Args:
564            execute_once_functor: A function that execute requests in batch once.
565                                  It should return a dictionary like
566                                  {request_id: (response, exception)}
567            requests: A dictionary where key is request id picked by caller,
568                      and value is a apiclient.http.HttpRequest.
569            retry_http_codes: A list of http codes to retry.
570            max_retry: See utils.Retry.
571            sleep: See utils.Retry.
572            backoff_factor: See utils.Retry.
573            other_retriable_errors: A tuple of error types that should be retried
574                                    other than errors.HttpError.
575        """
576        self._execute_once_functor = execute_once_functor
577        self._requests = requests
578        # A dictionary that maps request id to pending request.
579        self._pending_requests = {}
580        # A dictionary that maps request id to a tuple (response, exception).
581        self._final_results = {}
582        self._retry_http_codes = retry_http_codes
583        self._max_retry = max_retry
584        self._sleep = sleep
585        self._backoff_factor = backoff_factor
586        self._other_retriable_errors = other_retriable_errors
587
588    def _ShoudRetry(self, exception):
589        """Check if an exception is retriable.
590
591        Args:
592            exception: An exception instance.
593        """
594        if isinstance(exception, self._other_retriable_errors):
595            return True
596
597        if (isinstance(exception, errors.HttpError)
598                and exception.code in self._retry_http_codes):
599            return True
600        return False
601
602    def _ExecuteOnce(self):
603        """Executes pending requests and update it with failed, retriable ones.
604
605        Raises:
606            HasRetriableRequestsError: if some requests fail and are retriable.
607        """
608        results = self._execute_once_functor(self._pending_requests)
609        # Update final_results with latest results.
610        self._final_results.update(results)
611        # Clear pending_requests
612        self._pending_requests.clear()
613        for request_id, result in results.items():
614            exception = result[1]
615            if exception is not None and self._ShoudRetry(exception):
616                # If this is a retriable exception, put it in pending_requests
617                self._pending_requests[request_id] = self._requests[request_id]
618        if self._pending_requests:
619            # If there is still retriable requests pending, raise an error
620            # so that Retry will retry this function with pending_requests.
621            raise errors.HasRetriableRequestsError(
622                "Retriable errors: %s" %
623                [str(results[rid][1]) for rid in self._pending_requests])
624
625    def Execute(self):
626        """Executes the requests and retry if necessary.
627
628        Will populate self._final_results.
629        """
630
631        def _ShouldRetryHandler(exc):
632            """Check if |exc| is a retriable exception.
633
634            Args:
635                exc: An exception.
636
637            Returns:
638                True if exception is of type HasRetriableRequestsError; False otherwise.
639            """
640            should_retry = isinstance(exc, errors.HasRetriableRequestsError)
641            if should_retry:
642                logger.info("Will retry failed requests.", exc_info=True)
643                logger.info("%s", exc)
644            return should_retry
645
646        try:
647            self._pending_requests = self._requests.copy()
648            Retry(
649                _ShouldRetryHandler,
650                max_retries=self._max_retry,
651                functor=self._ExecuteOnce,
652                sleep_multiplier=self._sleep,
653                retry_backoff_factor=self._backoff_factor)
654        except errors.HasRetriableRequestsError:
655            logger.debug("Some requests did not succeed after retry.")
656
657    def GetResults(self):
658        """Returns final results.
659
660        Returns:
661            results, a dictionary in the following format
662            {request_id: (response, exception)}
663            request_ids are those from requests; response
664            is the http response for the request or None on error;
665            exception is an instance of DriverError or None if no error.
666        """
667        return self._final_results
668
669
670def DefaultEvaluator(result):
671    """Default Evaluator always return result is ok.
672
673    Args:
674        result:the return value of the target function.
675
676    Returns:
677        _EvaluatedResults namedtuple.
678    """
679    return _EvaluatedResult(is_result_ok=True, result_message=result)
680
681
682def ReportEvaluator(report):
683    """Evalute the acloud operation by the report.
684
685    Args:
686        report: acloud.public.report() object.
687
688    Returns:
689        _EvaluatedResults namedtuple.
690    """
691    if report is None or report.errors:
692        return _EvaluatedResult(is_result_ok=False,
693                                result_message=report.errors)
694
695    return _EvaluatedResult(is_result_ok=True, result_message=None)
696
697
698def BootEvaluator(boot_dict):
699    """Evaluate if the device booted successfully.
700
701    Args:
702        boot_dict: Dict of instance_name:boot error.
703
704    Returns:
705        _EvaluatedResults namedtuple.
706    """
707    if boot_dict:
708        return _EvaluatedResult(is_result_ok=False, result_message=boot_dict)
709    return _EvaluatedResult(is_result_ok=True, result_message=None)
710
711
712class TimeExecute:
713    """Count the function execute time."""
714
715    def __init__(self, function_description=None, print_before_call=True,
716                 print_status=True, result_evaluator=DefaultEvaluator,
717                 display_waiting_dots=True):
718        """Initializes the class.
719
720        Args:
721            function_description: String that describes function (e.g."Creating
722                                  Instance...")
723            print_before_call: Boolean, print the function description before
724                               calling the function, default True.
725            print_status: Boolean, print the status of the function after the
726                          function has completed, default True ("OK" or "Fail").
727            result_evaluator: Func object. Pass func to evaluate result.
728                              Default evaluator always report result is ok and
729                              failed result will be identified only in exception
730                              case.
731            display_waiting_dots: Boolean, if true print the function_description
732                                  followed by waiting dot.
733        """
734        self._function_description = function_description
735        self._print_before_call = print_before_call
736        self._print_status = print_status
737        self._result_evaluator = result_evaluator
738        self._display_waiting_dots = display_waiting_dots
739
740    def __call__(self, func):
741        def DecoratorFunction(*args, **kargs):
742            """Decorator function.
743
744            Args:
745                *args: Arguments to pass to the functor.
746                **kwargs: Key-val based arguments to pass to the functor.
747
748            Raises:
749                Exception: The exception that functor(*args, **kwargs) throws.
750            """
751            timestart = time.time()
752            if self._print_before_call:
753                waiting_dots = "..." if self._display_waiting_dots else ""
754                PrintColorString("%s %s"% (self._function_description,
755                                           waiting_dots), end="")
756            try:
757                result = func(*args, **kargs)
758                result_time = time.time() - timestart
759                if not self._print_before_call:
760                    PrintColorString("%s (%ds)" % (self._function_description,
761                                                   result_time),
762                                     TextColors.OKGREEN)
763                if self._print_status:
764                    evaluated_result = self._result_evaluator(result)
765                    if evaluated_result.is_result_ok:
766                        PrintColorString("OK! (%ds)" % (result_time),
767                                         TextColors.OKGREEN)
768                    else:
769                        PrintColorString("Fail! (%ds)" % (result_time),
770                                         TextColors.FAIL)
771                        PrintColorString("Error: %s" %
772                                         evaluated_result.result_message,
773                                         TextColors.FAIL)
774                return result
775            except:
776                if self._print_status:
777                    PrintColorString("Fail! (%ds)" % (time.time() - timestart),
778                                     TextColors.FAIL)
779                raise
780        return DecoratorFunction
781
782
783def PickFreePort():
784    """Helper to pick a free port.
785
786    Returns:
787        Integer, a free port number.
788    """
789    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
790    tcp_socket.bind(("", 0))
791    port = tcp_socket.getsockname()[1]
792    tcp_socket.close()
793    return port
794
795
796def CheckPortFree(port):
797    """Check the availablity of the tcp port.
798
799    Args:
800        Integer, a port number.
801
802    Raises:
803        PortOccupied: This port is not available.
804    """
805    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
806    try:
807        tcp_socket.bind(("", port))
808    except socket.error as port_error:
809        raise errors.PortOccupied("Port (%d) is taken, please choose another "
810                                  "port." % port) from port_error
811    tcp_socket.close()
812
813
814def _ExecuteCommand(cmd, args):
815    """Execute command.
816
817    Args:
818        cmd: Strings of execute binary name.
819        args: List of args to pass in with cmd.
820
821    Raises:
822        errors.NoExecuteBin: Can't find the execute bin file.
823    """
824    bin_path = FindExecutable(cmd)
825    if not bin_path:
826        raise errors.NoExecuteCmd("unable to locate %s" % cmd)
827    command = [bin_path] + args
828    logger.debug("Running '%s'", ' '.join(command))
829    with open(os.devnull, "w") as dev_null:
830        subprocess.check_call(command, stderr=dev_null, stdout=dev_null)
831
832
833def ReleasePort(port):
834    """Release local port.
835
836    Args:
837        port: Integer of local port number.
838    """
839    try:
840        with open(os.devnull, "w") as dev_null:
841            subprocess.check_call(_RELEASE_PORT_CMD % port,
842                                  stderr=dev_null, stdout=dev_null, shell=True)
843    except subprocess.CalledProcessError:
844        logger.debug("The port %d is available.", constants.WEBRTC_LOCAL_PORT)
845
846
847def EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user,
848                       port_mapping, extra_args_ssh_tunnel=None):
849    """Create an ssh tunnel.
850
851    Args:
852        ip_addr: String, use to build the adb, fastboot & vnc tunnel between local
853                 and remote instance.
854        rsa_key_file: String, Private key file path to use when creating
855                      the ssh tunnels.
856        ssh_user: String of user login into the instance.
857        port_mapping: List of tuples, each tuple is a pair of integers
858                      representing a local port and a remote port.
859        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
860
861    Raises:
862        subprocess.CalledProcessError if the ssh command fails.
863    """
864    port_mapping = [PORT_MAPPING % {
865        "local_port": ports[0],
866        "target_port": ports[1]} for ports in port_mapping]
867    ssh_tunnel_args = _SSH_TUNNEL_ARGS % {
868        "rsa_key_file": rsa_key_file,
869        "ssh_user": ssh_user,
870        "ip_addr": ip_addr,
871        "port_mapping": " ".join(port_mapping)}
872    ssh_tunnel_args_list = shlex.split(ssh_tunnel_args)
873    if extra_args_ssh_tunnel:
874        ssh_tunnel_args_list.extend(shlex.split(extra_args_ssh_tunnel))
875    _ExecuteCommand(constants.SSH_BIN, ssh_tunnel_args_list)
876
877
878def EstablishWebRTCSshTunnel(ip_addr, webrtc_local_port, rsa_key_file, ssh_user,
879                             extra_args_ssh_tunnel=None):
880    """Create ssh tunnels for webrtc.
881
882    Pick up an available local port to establish one WebRTC tunnel and forward to
883    the port of the webrtc operator of the remote instance.
884
885    Args:
886        ip_addr: String, use to build the adb, fastboot & vnc tunnel between local
887                 and remote instance.
888        webrtc_local_port: Integer, pick a free port as webrtc local port.
889        rsa_key_file: String, Private key file path to use when creating
890                      the ssh tunnels.
891        ssh_user: String of user login into the instance.
892        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
893
894    Raises:
895        subprocess.CalledProcessError if the ssh command fails.
896    """
897    webrtc_server_port = GetWebRTCServerPort(
898        ip_addr, rsa_key_file, ssh_user, extra_args_ssh_tunnel)
899
900    # TODO(b/209502647): design a better way to forward webrtc ports.
901    if extra_args_ssh_tunnel:
902        for webrtc_port in WEBRTC_PORTS_MAPPING:
903            ReleasePort(webrtc_port.local)
904    port_mapping = (WEBRTC_PORTS_MAPPING +
905                    [PortMapping(webrtc_local_port, webrtc_server_port)])
906    try:
907        EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user,
908                           port_mapping, extra_args_ssh_tunnel)
909    except subprocess.CalledProcessError as e:
910        PrintColorString("\n%s\nFailed to create ssh tunnels, retry with '#acloud "
911                         "reconnect'." % e, TextColors.FAIL)
912
913
914def GetWebRTCServerPort(ip_addr, rsa_key_file, ssh_user,
915                        extra_args_ssh_tunnel=None):
916    """Get WebRTC server port.
917
918    List all process information to find the "webrtc_operator" process, then
919    determine the WebRTC server port is 8443 or 1443.
920
921    Args:
922        ip_addr: String, use to build the adb, fastboot & vnc tunnel between local
923                 and remote instance.
924        rsa_key_file: String, Private key file path to use when creating
925                      the ssh tunnels.
926        ssh_user: String of user login into the instance.
927        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
928
929    Returns:
930        The WebRTC server port number.
931
932    Raises:
933        subprocess.CalledProcessError if the ssh command fails.
934    """
935    ssh_cmd = _SSH_COMMAND_PS % {
936        "ssh_bin": FindExecutable(constants.SSH_BIN),
937        "rsa_key_file": rsa_key_file,
938        "ssh_user": ssh_user,
939        "extra_args": extra_args_ssh_tunnel or "",
940        "ip_addr": ip_addr}
941    logger.info("Running command \"%s\"", ssh_cmd)
942    try:
943        process = subprocess.Popen(
944            ssh_cmd, shell=True, stdin=None, universal_newlines=True,
945            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
946        stdout, _ = process.communicate()
947        for line in stdout.splitlines():
948            webrtc_match = _WEBRTC_OPERATOR_PATTERN.match(line)
949            if webrtc_match:
950                return _PORT_8443
951    except subprocess.CalledProcessError as e:
952        logger.debug("Failed to list processes: %s", e)
953    return _PORT_1443
954
955
956def GetWebrtcPortFromSSHTunnel(ip):
957    """Get forwarding webrtc port from ssh tunnel.
958
959    Args:
960        ip: String, ip address.
961
962    Returns:
963        webrtc local port.
964    """
965    re_pattern = re.compile(_RE_WEBRTC_SSH_TUNNEL_PATTERN %
966                            (constants.WEBRTC_LOCAL_PORT, ip))
967    process_output = CheckOutput(constants.COMMAND_PS)
968    for line in process_output.splitlines():
969        match = re_pattern.match(line)
970        if match:
971            webrtc_port = int(match.group(_RE_GROUP_WEBRTC))
972            return webrtc_port
973
974    logger.debug("Can't get webrtc local port from ip %s.", ip)
975    return None
976
977
978# TODO(147337696): create ssh tunnels tear down as adb, fastboot and vnc.
979# pylint: disable=too-many-locals
980def AutoConnect(ip_addr, rsa_key_file, target_vnc_port, target_adb_port, target_fastboot_port,
981                ssh_user, client_adb_port=None, client_fastboot_port=None,
982                extra_args_ssh_tunnel=None):
983    """Autoconnect to an AVD instance.
984
985    Args:
986        ip_addr: String, use to build the adb, fastboot & vnc tunnel between local
987                 and remote instance.
988        rsa_key_file: String, Private key file path to use when creating
989                      the ssh tunnels.
990        target_vnc_port: Integer of target vnc port number.
991        target_adb_port: Integer of target adb port number.
992        target_fastboot_port: Integer of target fastboot port number.
993        ssh_user: String of user login into the instance.
994        client_adb_port: Integer, Specified adb port to establish connection.
995        client_fastboot_port: Integer, Specified fastboot port to establish connection.
996        extra_args_ssh_tunnel: String, extra args for ssh tunnel connection.
997
998    Returns:
999        NamedTuple of (vnc_port, adb_port, fastboot_port) SSHTUNNEL of the connect, both are
1000        integers.
1001    """
1002    local_adb_port = client_adb_port or PickFreePort()
1003    port_mapping = [(local_adb_port, target_adb_port)]
1004    local_fastboot_port = client_fastboot_port or PickFreePort()
1005    if target_fastboot_port:
1006        port_mapping.append((local_fastboot_port, target_fastboot_port))
1007    local_free_vnc_port = None
1008    if target_vnc_port:
1009        local_free_vnc_port = PickFreePort()
1010        port_mapping.append((local_free_vnc_port, target_vnc_port))
1011    try:
1012        EstablishSshTunnel(ip_addr, rsa_key_file, ssh_user,
1013                           port_mapping, extra_args_ssh_tunnel)
1014    except subprocess.CalledProcessError as e:
1015        PrintColorString("\n%s\nFailed to create ssh tunnels, retry with '#acloud "
1016                         "reconnect'." % e, TextColors.FAIL)
1017        return ForwardedPorts(vnc_port=None, adb_port=None, fastboot_port=None)
1018
1019    try:
1020        adb_connect_args = _ADB_CONNECT_ARGS % {"adb_port": local_adb_port}
1021        _ExecuteCommand(constants.ADB_BIN, adb_connect_args.split())
1022    except subprocess.CalledProcessError:
1023        PrintColorString("Failed to adb connect, retry with "
1024                         "'#acloud reconnect'", TextColors.FAIL)
1025
1026    return ForwardedPorts(vnc_port=local_free_vnc_port,
1027                          adb_port=local_adb_port,
1028                          fastboot_port=local_fastboot_port)
1029
1030
1031def FindRemoteFiles(ssh_obj, search_dirs):
1032    """Get all files, except symbolic links, under remote directories.
1033
1034    Args:
1035        ssh_obj: An Ssh object.
1036        search_dirs: A list of strings, the remote directories.
1037
1038    Returns:
1039        A list of strings, the file paths.
1040    """
1041    if not search_dirs:
1042        return []
1043    ssh_cmd = (ssh_obj.GetBaseCmd(constants.SSH_BIN) + " " +
1044               _CMD_FIND_FILES % {"search_dirs": " ".join(search_dirs)})
1045    proc = subprocess.run(ssh_cmd, shell=True, capture_output=True,
1046                          check=False)
1047    if proc.stderr:
1048        logger.debug("`%s` stderr: %s", ssh_cmd, proc.stderr.decode())
1049    if proc.stdout:
1050        return proc.stdout.decode().splitlines()
1051    return []
1052
1053
1054def GetAnswerFromList(answer_list, enable_choose_all=False):
1055    """Get answer from a list.
1056
1057    Args:
1058        answer_list: list of the answers to choose from.
1059        enable_choose_all: True to choose all items from answer list.
1060
1061    Return:
1062        List holding the answer(s).
1063    """
1064    print("[0] to exit.")
1065    start_index = 1
1066    max_choice = len(answer_list)
1067
1068    for num, item in enumerate(answer_list, start_index):
1069        print("[%d] %s" % (num, item))
1070    if enable_choose_all:
1071        max_choice += 1
1072        print("[%d] for all." % max_choice)
1073
1074    choice = -1
1075
1076    while True:
1077        try:
1078            choice = input("Enter your choice[0-%d]: " % max_choice)
1079            choice = int(choice)
1080        except ValueError:
1081            print("'%s' is not a valid integer.", choice)
1082            continue
1083        # Filter out choices
1084        if choice == 0:
1085            sys.exit(constants.EXIT_BY_USER)
1086        if enable_choose_all and choice == max_choice:
1087            return answer_list
1088        if choice < 0 or choice > max_choice:
1089            print("please choose between 0 and %d" % max_choice)
1090        else:
1091            return [answer_list[choice-start_index]]
1092
1093
1094def LaunchVNCFromReport(report, avd_spec, no_prompts=False):
1095    """Launch vnc client according to the instances report.
1096
1097    Args:
1098        report: Report object, that stores and generates report.
1099        avd_spec: AVDSpec object that tells us what we're going to create.
1100        no_prompts: Boolean, True to skip all prompts.
1101    """
1102    for device in report.data.get("devices", []):
1103        if device.get(constants.VNC_PORT):
1104            LaunchVncClient(device.get(constants.VNC_PORT),
1105                            avd_width=avd_spec.hw_property["x_res"],
1106                            avd_height=avd_spec.hw_property["y_res"],
1107                            no_prompts=no_prompts)
1108        else:
1109            PrintColorString("No VNC port specified, skipping VNC startup.",
1110                             TextColors.FAIL)
1111
1112
1113def LaunchBrowserFromReport(report):
1114    """Open browser when autoconnect to webrtc according to the instances report.
1115
1116    Args:
1117        report: Report object, that stores and generates report.
1118    """
1119    for device in report.data.get("devices", []):
1120        if device.get("ip"):
1121            LaunchBrowser(constants.WEBRTC_LOCAL_HOST,
1122                          device.get(constants.WEBRTC_PORT,
1123                                     constants.WEBRTC_LOCAL_PORT))
1124
1125
1126def LaunchBrowser(ip_addr, port):
1127    """Launch browser to connect the webrtc AVD.
1128
1129    Args:
1130        ip_addr: String, use to connect to webrtc AVD on the instance.
1131        port: Integer, port number.
1132    """
1133    webrtc_link = _WEBRTC_URL % {
1134        "webrtc_ip": ip_addr,
1135        "webrtc_port": port}
1136    if os.environ.get(_ENV_DISPLAY, None):
1137        webbrowser.open_new_tab(webrtc_link)
1138    else:
1139        PrintColorString("Remote terminal can't support launch webbrowser.",
1140                         TextColors.FAIL)
1141        PrintColorString("WebRTC AVD URL: %s "% webrtc_link)
1142
1143
1144def LaunchVncClient(port, avd_width=None, avd_height=None, no_prompts=False):
1145    """Launch ssvnc.
1146
1147    Args:
1148        port: Integer, port number.
1149        avd_width: String, the width of avd.
1150        avd_height: String, the height of avd.
1151        no_prompts: Boolean, True to skip all prompts.
1152    """
1153    try:
1154        os.environ[_ENV_DISPLAY]
1155    except KeyError:
1156        PrintColorString("Remote terminal can't support VNC. "
1157                         "Skipping VNC startup. "
1158                         "VNC server is listening at 127.0.0.1:{}.".format(port),
1159                         TextColors.FAIL)
1160        return
1161
1162    if IsSupportedPlatform() and not FindExecutable(_VNC_BIN):
1163        if no_prompts or GetUserAnswerYes(_CONFIRM_CONTINUE):
1164            try:
1165                PrintColorString("Installing ssvnc vnc client... ", end="")
1166                sys.stdout.flush()
1167                CheckOutput(_CMD_INSTALL_SSVNC, shell=True)
1168                PrintColorString("Done", TextColors.OKGREEN)
1169            except subprocess.CalledProcessError as cpe:
1170                PrintColorString("Failed to install ssvnc: %s" %
1171                                 cpe.output, TextColors.FAIL)
1172                return
1173        else:
1174            return
1175    ssvnc_env = os.environ.copy()
1176    ssvnc_env.update(_SSVNC_ENV_VARS)
1177    # Override SSVNC_SCALE
1178    if avd_width or avd_height:
1179        scale_ratio = CalculateVNCScreenRatio(avd_width, avd_height)
1180        ssvnc_env["SSVNC_SCALE"] = str(scale_ratio)
1181        logger.debug("SSVNC_SCALE:%s", scale_ratio)
1182
1183    ssvnc_args = _CMD_START_VNC % {"bin": FindExecutable(_VNC_BIN),
1184                                   "port": port}
1185    subprocess.Popen(ssvnc_args.split(), env=ssvnc_env)
1186
1187
1188def PrintDeviceSummary(report):
1189    """Display summary of devices.
1190
1191    -Display device details from the report instance.
1192        report example:
1193            'data': [{'devices':[{'instance_name': 'ins-f6a397-none-53363',
1194                                  'ip': u'35.234.10.162'}]}]
1195    -Display error message from report.error.
1196
1197    Args:
1198        report: A Report instance.
1199    """
1200    PrintColorString("\n")
1201    PrintColorString("Device summary:")
1202    for device in report.data.get("devices", []):
1203        adb_serial = device.get(constants.DEVICE_SERIAL)
1204        if not adb_serial:
1205            adb_port = device.get("adb_port")
1206            if adb_port:
1207                adb_serial = constants.LOCALHOST_ADB_SERIAL % adb_port
1208            else:
1209                adb_serial = "(None)"
1210
1211        instance_name = device.get("instance_name")
1212        instance_ip = device.get("ip")
1213        instance_details = "" if not instance_name else "(%s[%s])" % (
1214            instance_name, instance_ip)
1215        PrintColorString(f" - device serial: {adb_serial} {instance_details}")
1216        PrintColorString("\n")
1217        PrintColorString("Note: To ensure Tradefed uses this AVD, please run:")
1218        PrintColorString("\texport ANDROID_SERIAL=%s" % adb_serial)
1219        ssh_command = device.get("ssh_command")
1220        if ssh_command:
1221            PrintColorString("\n")
1222            PrintColorString("Note: To ssh connect to the device, please run:")
1223            PrintColorString(f"\tssh command: {ssh_command}")
1224        screen_command = device.get("screen_command")
1225        if screen_command:
1226            PrintColorString("\n")
1227            PrintColorString("Note: To access the console, please run:")
1228            PrintColorString(f"\tscreen command: {screen_command}")
1229
1230    # TODO(b/117245508): Help user to delete instance if it got created.
1231    if report.errors:
1232        error_msg = "\n".join(report.errors)
1233        PrintColorString("Fail in:\n%s\n" % error_msg, TextColors.FAIL)
1234
1235
1236# pylint: disable=import-outside-toplevel
1237def CalculateVNCScreenRatio(avd_width, avd_height):
1238    """calculate the vnc screen scale ratio to fit into user's monitor.
1239
1240    Args:
1241        avd_width: String, the width of avd.
1242        avd_height: String, the height of avd.
1243    Return:
1244        Float, scale ratio for vnc client.
1245    """
1246    try:
1247        import Tkinter
1248    # Some python interpreters may not be configured for Tk, just return default scale ratio.
1249    except ImportError:
1250        try:
1251            import tkinter as Tkinter
1252        except ImportError:
1253            PrintColorString(
1254                "no module named tkinter, vnc display scale were not be fit."
1255                "please run 'sudo apt-get install python3-tk' to install it.")
1256            return _DEFAULT_DISPLAY_SCALE
1257    root = Tkinter.Tk()
1258    margin = 100 # leave some space on user's monitor.
1259    screen_height = root.winfo_screenheight() - margin
1260    screen_width = root.winfo_screenwidth() - margin
1261
1262    scale_h = _DEFAULT_DISPLAY_SCALE
1263    scale_w = _DEFAULT_DISPLAY_SCALE
1264    if float(screen_height) < float(avd_height):
1265        scale_h = round(float(screen_height) / float(avd_height), 1)
1266
1267    if float(screen_width) < float(avd_width):
1268        scale_w = round(float(screen_width) / float(avd_width), 1)
1269
1270    logger.debug("scale_h: %s (screen_h: %s/avd_h: %s),"
1271                 " scale_w: %s (screen_w: %s/avd_w: %s)",
1272                 scale_h, screen_height, avd_height,
1273                 scale_w, screen_width, avd_width)
1274
1275    # Return the larger scale-down ratio.
1276    return scale_h if scale_h < scale_w else scale_w
1277
1278
1279def IsCommandRunning(command):
1280    """Check if command is running.
1281
1282    Args:
1283        command: String of command name.
1284
1285    Returns:
1286        Boolean, True if command is running. False otherwise.
1287    """
1288    try:
1289        with open(os.devnull, "w") as dev_null:
1290            subprocess.check_call([constants.CMD_PGREP, "-af", command],
1291                                  stderr=dev_null, stdout=dev_null)
1292        return True
1293    except subprocess.CalledProcessError:
1294        return False
1295
1296
1297def AddUserGroupsToCmd(cmd, user_groups):
1298    """Add the user groups to the command if necessary.
1299
1300    As part of local host setup to enable local instance support, the user is
1301    added to certain groups. For those settings to take effect systemwide
1302    requires the user to log out and log back in. In the scenario where the
1303    user has run setup and hasn't logged out, we still want them to be able to
1304    launch a local instance so add the user to the groups as part of the
1305    command to ensure success.
1306
1307    The reason using here-doc instead of '&' is all operations need to be ran in
1308    ths same pid.  Here's an example cmd:
1309    $ sg kvm  << EOF
1310    sg libvirt
1311    sg cvdnetwork
1312    launch_cvd --cpus 2 --x_res 1280 --y_res 720 --dpi 160 --memory_mb 4096
1313    EOF
1314
1315    Args:
1316        cmd: String of the command to prepend the user groups to.
1317        user_groups: List of user groups name.(String)
1318
1319    Returns:
1320        String of the command with the user groups prepended to it if necessary,
1321        otherwise the same existing command.
1322    """
1323    user_group_cmd = ""
1324    if not CheckUserInGroups(user_groups):
1325        logger.debug("Need to add user groups to the command")
1326        for idx, group in enumerate(user_groups):
1327            user_group_cmd += _CMD_SG + group
1328            if idx == 0:
1329                user_group_cmd += " <<EOF\n"
1330            else:
1331                user_group_cmd += "\n"
1332        cmd += "\nEOF"
1333    user_group_cmd += cmd
1334    logger.debug("user group cmd: %s", user_group_cmd)
1335    return user_group_cmd
1336
1337
1338def CheckUserInGroups(group_name_list):
1339    """Check if the current user is in the group.
1340
1341    Args:
1342        group_name_list: The list of group name.
1343    Returns:
1344        True if current user is in all the groups.
1345    """
1346    logger.info("Checking if user is in following groups: %s", group_name_list)
1347    all_groups = [g.gr_name for g in grp.getgrall()]
1348    for group in group_name_list:
1349        if group not in all_groups:
1350            logger.info("This group doesn't exist: %s", group)
1351            return False
1352        if getpass.getuser() not in grp.getgrnam(group).gr_mem:
1353            logger.info("Current user isn't in this group: %s", group)
1354            return False
1355    return True
1356
1357
1358def IsSupportedPlatform(print_warning=False):
1359    """Check if user's os is the supported platform.
1360
1361    platform.version() return such as '#1 SMP Debian 5.6.14-1rodete2...'
1362    and use to judge supported or not.
1363
1364    Args:
1365        print_warning: Boolean, print the unsupported warning
1366                       if True.
1367    Returns:
1368        Boolean, True if user is using supported platform.
1369    """
1370    system = platform.system()
1371    # TODO(b/161085678): After python3 fully migrated, then use distro to fix.
1372    platform_supported = False
1373    if system in _SUPPORTED_SYSTEMS_AND_DISTS:
1374        for dist in _SUPPORTED_SYSTEMS_AND_DISTS[system]:
1375            if dist in platform.version():
1376                platform_supported = True
1377                break
1378
1379    logger.info("Updated supported system and dists: %s",
1380                _SUPPORTED_SYSTEMS_AND_DISTS)
1381    platform_supported_msg = ("%s[%s] %s supported platform" %
1382                              (system,
1383                               platform.version(),
1384                               "is a" if platform_supported else "is not a"))
1385    if print_warning and not platform_supported:
1386        PrintColorString(platform_supported_msg, TextColors.WARNING)
1387    else:
1388        logger.info(platform_supported_msg)
1389
1390    return platform_supported
1391
1392def IsSupportedKvm():
1393    """Check if support kvm.
1394
1395    Returns:
1396        True if environment supported kvm.
1397    """
1398    if os.path.exists(_KVM_PATH):
1399        return True
1400
1401    PrintColorString(
1402        "The environment doesn't support virtualization. Please run "
1403        "the remote instance by \"acloud create\" instead. If you want to "
1404        "launch AVD on the local instance, Please refer to http://go/"
1405        "acloud-cloudtop#acloud-create-local-instance-on-the-cloudtop",
1406        TextColors.FAIL)
1407    return False
1408
1409def GetDistDir():
1410    """Return the absolute path to the dist dir."""
1411    android_build_top = os.environ.get(constants.ENV_ANDROID_BUILD_TOP)
1412    if not android_build_top:
1413        return None
1414    dist_cmd = GET_BUILD_VAR_CMD[:]
1415    dist_cmd.append(_DIST_DIR)
1416    try:
1417        dist_dir = CheckOutput(dist_cmd, cwd=android_build_top)
1418    except subprocess.CalledProcessError:
1419        return None
1420    return os.path.join(android_build_top, dist_dir.strip())
1421
1422
1423def CleanupProcess(pattern):
1424    """Cleanup process with pattern.
1425
1426    Args:
1427        pattern: String, string of process pattern.
1428    """
1429    if IsCommandRunning(pattern):
1430        command_kill = _CMD_KILL + [pattern]
1431        subprocess.check_call(command_kill)
1432
1433
1434def TimeoutException(timeout_secs, timeout_error=_DEFAULT_TIMEOUT_ERR):
1435    """Decorater which function timeout setup and raise custom exception.
1436
1437    Args:
1438        timeout_secs: Number of maximum seconds of waiting time.
1439        timeout_error: String to describe timeout exception.
1440
1441    Returns:
1442        The function wrapper.
1443    """
1444    if timeout_error == _DEFAULT_TIMEOUT_ERR:
1445        timeout_error = timeout_error % timeout_secs
1446
1447    def _Wrapper(func):
1448        # pylint: disable=unused-argument
1449        def _HandleTimeout(signum, frame):
1450            raise errors.FunctionTimeoutError(timeout_error)
1451
1452        def _FunctionWrapper(*args, **kwargs):
1453            signal.signal(signal.SIGALRM, _HandleTimeout)
1454            signal.alarm(timeout_secs)
1455            try:
1456                result = func(*args, **kwargs)
1457            finally:
1458                signal.alarm(0)
1459            return result
1460
1461        return _FunctionWrapper
1462
1463    return _Wrapper
1464
1465
1466def GetBuildEnvironmentVariable(variable_name):
1467    """Get build environment variable.
1468
1469    Args:
1470        variable_name: String of variable name.
1471
1472    Returns:
1473        String, the value of the variable.
1474
1475    Raises:
1476        errors.GetAndroidBuildEnvVarError: No environment variable found.
1477    """
1478    try:
1479        return os.environ[variable_name]
1480    except KeyError as no_env_error:
1481        raise errors.GetAndroidBuildEnvVarError(
1482            "Could not get environment var: %s\n"
1483            "Try to run 'source build/envsetup.sh && lunch <target>'"
1484            % variable_name
1485        ) from no_env_error
1486
1487
1488# pylint: disable=no-member,import-outside-toplevel
1489def FindExecutable(filename):
1490    """A compatibility function to find execution file path.
1491
1492    Args:
1493        filename: String of execution filename.
1494
1495    Returns:
1496        String: execution file path.
1497    """
1498    try:
1499        from distutils.spawn import find_executable
1500        return find_executable(filename)
1501    except ImportError:
1502        return shutil.which(filename)
1503
1504
1505def GetDictItems(namedtuple_object):
1506    """A compatibility function to access the OrdereDict object from the given namedtuple object.
1507
1508    Args:
1509        namedtuple_object: namedtuple object.
1510
1511    Returns:
1512        collections.namedtuple._asdict().items() when using python3.
1513    """
1514    return namedtuple_object._asdict().items()
1515
1516
1517def CleanupSSVncviewer(vnc_port):
1518    """Cleanup the old disconnected ssvnc viewer.
1519
1520    Args:
1521        vnc_port: Integer, port number of vnc.
1522    """
1523    ssvnc_viewer_pattern = _SSVNC_VIEWER_PATTERN % {"vnc_port":vnc_port}
1524    CleanupProcess(ssvnc_viewer_pattern)
1525
1526
1527def CheckOutput(cmd, **kwargs):
1528    """Call subprocess.check_output to get output.
1529
1530    The subprocess.check_output return type is "bytes" in python 3, we have
1531    to convert bytes as string with .decode() in advance.
1532
1533    Args:
1534        cmd: String of command.
1535        **kwargs: dictionary of keyword based args to pass to func.
1536
1537    Return:
1538        String to command output.
1539    """
1540    return subprocess.check_output(cmd, **kwargs).decode()
1541
1542
1543def Popen(*command, **popen_args):
1544    """Execute subprocess.Popen command and log the output.
1545
1546    This method waits for the process to terminate. It kills the process
1547    if it's interrupted due to timeout.
1548
1549    Args:
1550        command: Strings, the command.
1551        popen_kwargs: The arguments to be passed to subprocess.Popen.
1552
1553    Raises:
1554        errors.SubprocessFail if the process returns non-zero.
1555    """
1556    proc = None
1557    try:
1558        logger.info("Execute %s", command)
1559        popen_args["stdin"] = subprocess.PIPE
1560        popen_args["stdout"] = subprocess.PIPE
1561        popen_args["stderr"] = subprocess.PIPE
1562
1563        # Some OTA tools are Python scripts in different versions. The
1564        # PYTHONPATH for acloud may be incompatible with the tools.
1565        if "env" not in popen_args and "PYTHONPATH" in os.environ:
1566            popen_env = os.environ.copy()
1567            del popen_env["PYTHONPATH"]
1568            popen_args["env"] = popen_env
1569
1570        proc = subprocess.Popen(command, **popen_args)
1571        stdout, stderr = proc.communicate()
1572        logger.info("%s stdout: %s", command[0], stdout)
1573        logger.info("%s stderr: %s", command[0], stderr)
1574
1575        if proc.returncode != 0:
1576            raise errors.SubprocessFail("%s returned %d." %
1577                                        (command[0], proc.returncode))
1578    finally:
1579        if proc and proc.poll() is None:
1580            logger.info("Kill %s", command[0])
1581            proc.kill()
1582
1583
1584def SetExecutable(path):
1585    """Grant the persmission to execute a file.
1586
1587    Args:
1588        path: String, the file path.
1589
1590    Raises:
1591        OSError if any file operation fails.
1592    """
1593    mode = os.stat(path).st_mode
1594    os.chmod(path, mode | (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH |
1595                           stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH))
1596
1597
1598def SetDirectoryTreeExecutable(dir_path):
1599    """Grant the permission to execute all files in a directory.
1600
1601    Args:
1602        dir_path: String, the directory path.
1603
1604    Raises:
1605        OSError if any file operation fails.
1606    """
1607    for parent_dir, _, file_names in os.walk(dir_path):
1608        for name in file_names:
1609            SetExecutable(os.path.join(parent_dir, name))
1610
1611
1612def GetCvdPorts():
1613    """Get CVD ports
1614
1615
1616    Returns:
1617        ForwardedPorts: vnc, adb and fastboot ports.
1618    """
1619    return AVD_PORT_DICT[constants.TYPE_CF]
1620
1621
1622def SetCvdPorts(base_instance_num):
1623    """Adjust ports by base_instance_num.
1624
1625    Args:
1626        base_instance_num: int, cuttlefish base_instance_num.
1627    """
1628    offset = (base_instance_num or 1) - 1
1629    AVD_PORT_DICT[constants.TYPE_CF] = ForwardedPorts(
1630        constants.CF_VNC_PORT + offset,
1631        constants.CF_ADB_PORT + offset,
1632        constants.CF_FASTBOOT_PORT + offset)
1633
1634    # TODO: adjust WebRTC ports
1635