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