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