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