1# Copyright 2016 Google Inc. 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 15import base64 16import concurrent.futures 17import datetime 18import errno 19import inspect 20import io 21import logging 22import os 23import pipes 24import platform 25import random 26import re 27import signal 28import string 29import subprocess 30import threading 31import time 32import traceback 33from typing import Tuple, overload 34 35import portpicker 36 37# TODO(#851): Remove this try/except statement and typing_extensions from 38# install_requires when Python 3.8 is the minimum version we support. 39try: 40 from typing import Literal 41except ImportError: 42 from typing_extensions import Literal 43 44# File name length is limited to 255 chars on some OS, so we need to make sure 45# the file names we output fits within the limit. 46MAX_FILENAME_LEN = 255 47# Number of times to retry to get available port 48MAX_PORT_ALLOCATION_RETRY = 50 49 50ascii_letters_and_digits = string.ascii_letters + string.digits 51valid_filename_chars = f'-_.{ascii_letters_and_digits}' 52 53GMT_to_olson = { 54 'GMT-9': 'America/Anchorage', 55 'GMT-8': 'US/Pacific', 56 'GMT-7': 'US/Mountain', 57 'GMT-6': 'US/Central', 58 'GMT-5': 'US/Eastern', 59 'GMT-4': 'America/Barbados', 60 'GMT-3': 'America/Buenos_Aires', 61 'GMT-2': 'Atlantic/South_Georgia', 62 'GMT-1': 'Atlantic/Azores', 63 'GMT+0': 'Africa/Casablanca', 64 'GMT+1': 'Europe/Amsterdam', 65 'GMT+2': 'Europe/Athens', 66 'GMT+3': 'Europe/Moscow', 67 'GMT+4': 'Asia/Baku', 68 'GMT+5': 'Asia/Oral', 69 'GMT+6': 'Asia/Almaty', 70 'GMT+7': 'Asia/Bangkok', 71 'GMT+8': 'Asia/Hong_Kong', 72 'GMT+9': 'Asia/Tokyo', 73 'GMT+10': 'Pacific/Guam', 74 'GMT+11': 'Pacific/Noumea', 75 'GMT+12': 'Pacific/Fiji', 76 'GMT+13': 'Pacific/Tongatapu', 77 'GMT-11': 'Pacific/Midway', 78 'GMT-10': 'Pacific/Honolulu' 79} 80 81 82class Error(Exception): 83 """Raised when an error occurs in a util""" 84 85 86def abs_path(path): 87 """Resolve the '.' and '~' in a path to get the absolute path. 88 89 Args: 90 path: The path to expand. 91 92 Returns: 93 The absolute path of the input path. 94 """ 95 return os.path.abspath(os.path.expanduser(path)) 96 97 98def create_dir(path): 99 """Creates a directory if it does not exist already. 100 101 Args: 102 path: The path of the directory to create. 103 """ 104 full_path = abs_path(path) 105 if not os.path.exists(full_path): 106 try: 107 os.makedirs(full_path) 108 except OSError as e: 109 # ignore the error for dir already exist. 110 if e.errno != errno.EEXIST: 111 raise 112 113 114def create_alias(target_path, alias_path): 115 """Creates an alias at 'alias_path' pointing to the file 'target_path'. 116 117 On Unix, this is implemented via symlink. On Windows, this is done by 118 creating a Windows shortcut file. 119 120 Args: 121 target_path: Destination path that the alias should point to. 122 alias_path: Path at which to create the new alias. 123 """ 124 if platform.system() == 'Windows' and not alias_path.endswith('.lnk'): 125 alias_path += '.lnk' 126 if os.path.lexists(alias_path): 127 os.remove(alias_path) 128 if platform.system() == 'Windows': 129 from win32com import client 130 shell = client.Dispatch('WScript.Shell') 131 shortcut = shell.CreateShortCut(alias_path) 132 shortcut.Targetpath = target_path 133 shortcut.save() 134 else: 135 os.symlink(target_path, alias_path) 136 137 138def get_current_epoch_time(): 139 """Current epoch time in milliseconds. 140 141 Returns: 142 An integer representing the current epoch time in milliseconds. 143 """ 144 return int(round(time.time() * 1000)) 145 146 147def get_current_human_time(): 148 """Returns the current time in human readable format. 149 150 Returns: 151 The current time stamp in Month-Day-Year Hour:Min:Sec format. 152 """ 153 return time.strftime('%m-%d-%Y %H:%M:%S ') 154 155 156def epoch_to_human_time(epoch_time): 157 """Converts an epoch timestamp to human readable time. 158 159 This essentially converts an output of get_current_epoch_time to an output 160 of get_current_human_time 161 162 Args: 163 epoch_time: An integer representing an epoch timestamp in milliseconds. 164 165 Returns: 166 A time string representing the input time. 167 None if input param is invalid. 168 """ 169 if isinstance(epoch_time, int): 170 try: 171 d = datetime.datetime.fromtimestamp(epoch_time / 1000) 172 return d.strftime('%m-%d-%Y %H:%M:%S ') 173 except ValueError: 174 return None 175 176 177def get_timezone_olson_id(): 178 """Return the Olson ID of the local (non-DST) timezone. 179 180 Returns: 181 A string representing one of the Olson IDs of the local (non-DST) 182 timezone. 183 """ 184 tzoffset = int(time.timezone / 3600) 185 if tzoffset <= 0: 186 gmt = f'GMT+{-tzoffset}' 187 else: 188 gmt = f'GMT-{tzoffset}' 189 return GMT_to_olson[gmt] 190 191 192def find_files(paths, file_predicate): 193 """Locate files whose names and extensions match the given predicate in 194 the specified directories. 195 196 Args: 197 paths: A list of directory paths where to find the files. 198 file_predicate: A function that returns True if the file name and 199 extension are desired. 200 201 Returns: 202 A list of files that match the predicate. 203 """ 204 file_list = [] 205 for path in paths: 206 p = abs_path(path) 207 for dirPath, _, fileList in os.walk(p): 208 for fname in fileList: 209 name, ext = os.path.splitext(fname) 210 if file_predicate(name, ext): 211 file_list.append((dirPath, name, ext)) 212 return file_list 213 214 215def load_file_to_base64_str(f_path): 216 """Loads the content of a file into a base64 string. 217 218 Args: 219 f_path: full path to the file including the file name. 220 221 Returns: 222 A base64 string representing the content of the file in utf-8 encoding. 223 """ 224 path = abs_path(f_path) 225 with io.open(path, 'rb') as f: 226 f_bytes = f.read() 227 base64_str = base64.b64encode(f_bytes).decode('utf-8') 228 return base64_str 229 230 231def find_field(item_list, cond, comparator, target_field): 232 """Finds the value of a field in a dict object that satisfies certain 233 conditions. 234 235 Args: 236 item_list: A list of dict objects. 237 cond: A param that defines the condition. 238 comparator: A function that checks if an dict satisfies the condition. 239 target_field: Name of the field whose value to be returned if an item 240 satisfies the condition. 241 242 Returns: 243 Target value or None if no item satisfies the condition. 244 """ 245 for item in item_list: 246 if comparator(item, cond) and target_field in item: 247 return item[target_field] 248 return None 249 250 251def rand_ascii_str(length): 252 """Generates a random string of specified length, composed of ascii letters 253 and digits. 254 255 Args: 256 length: The number of characters in the string. 257 258 Returns: 259 The random string generated. 260 """ 261 letters = [random.choice(ascii_letters_and_digits) for _ in range(length)] 262 return ''.join(letters) 263 264 265# Thead/Process related functions. 266def _collect_process_tree(starting_pid): 267 """Collects PID list of the descendant processes from the given PID. 268 269 This function only available on Unix like system. 270 271 Args: 272 starting_pid: The PID to start recursively traverse. 273 274 Returns: 275 A list of pid of the descendant processes. 276 """ 277 ret = [] 278 stack = [starting_pid] 279 280 while stack: 281 pid = stack.pop() 282 try: 283 ps_results = subprocess.check_output([ 284 'ps', 285 '-o', 286 'pid', 287 '--ppid', 288 str(pid), 289 '--noheaders', 290 ]).decode().strip() 291 except subprocess.CalledProcessError: 292 # Ignore if there is not child process. 293 continue 294 295 children_pid_list = list(map(int, ps_results.split('\n '))) 296 stack.extend(children_pid_list) 297 ret.extend(children_pid_list) 298 299 return ret 300 301 302def _kill_process_tree(proc): 303 """Kills the subprocess and its descendants.""" 304 if os.name == 'nt': 305 # The taskkill command with "/T" option ends the specified process and any 306 # child processes started by it: 307 # https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/taskkill 308 subprocess.check_output([ 309 'taskkill', 310 '/F', 311 '/T', 312 '/PID', 313 str(proc.pid), 314 ]) 315 return 316 317 failed = [] 318 for child_pid in _collect_process_tree(proc.pid): 319 try: 320 os.kill(child_pid, signal.SIGTERM) 321 except Exception: # pylint: disable=broad-except 322 failed.append(child_pid) 323 logging.exception('Failed to kill standing subprocess %d', child_pid) 324 325 try: 326 proc.kill() 327 except Exception: # pylint: disable=broad-except 328 failed.append(proc.pid) 329 logging.exception('Failed to kill standing subprocess %d', proc.pid) 330 331 if failed: 332 raise Error('Failed to kill standing subprocesses: %s' % failed) 333 334 335def concurrent_exec(func, param_list, max_workers=30, raise_on_exception=False): 336 """Executes a function with different parameters pseudo-concurrently. 337 338 This is basically a map function. Each element (should be an iterable) in 339 the param_list is unpacked and passed into the function. Due to Python's 340 GIL, there's no true concurrency. This is suited for IO-bound tasks. 341 342 Args: 343 func: The function that performs a task. 344 param_list: A list of iterables, each being a set of params to be 345 passed into the function. 346 max_workers: int, the number of workers to use for parallelizing the 347 tasks. By default, this is 30 workers. 348 raise_on_exception: bool, raises all of the task failures if any of the 349 tasks failed if `True`. By default, this is `False`. 350 351 Returns: 352 A list of return values from each function execution. If an execution 353 caused an exception, the exception object will be the corresponding 354 result. 355 356 Raises: 357 RuntimeError: If executing any of the tasks failed and 358 `raise_on_exception` is True. 359 """ 360 with concurrent.futures.ThreadPoolExecutor( 361 max_workers=max_workers) as executor: 362 # Start the load operations and mark each future with its params 363 future_to_params = {executor.submit(func, *p): p for p in param_list} 364 return_vals = [] 365 exceptions = [] 366 for future in concurrent.futures.as_completed(future_to_params): 367 params = future_to_params[future] 368 try: 369 return_vals.append(future.result()) 370 except Exception as exc: # pylint: disable=broad-except 371 logging.exception('%s generated an exception: %s', params, 372 traceback.format_exc()) 373 return_vals.append(exc) 374 exceptions.append(exc) 375 if raise_on_exception and exceptions: 376 error_messages = [] 377 for exception in exceptions: 378 error_messages.append(''.join( 379 traceback.format_exception(exception.__class__, exception, 380 exception.__traceback__))) 381 raise RuntimeError('\n\n'.join(error_messages)) 382 return return_vals 383 384 385# Provide hint for pytype checker to avoid the Union[bytes, str] case. 386@overload 387def run_command(cmd, 388 stdout=..., 389 stderr=..., 390 shell=..., 391 timeout=..., 392 cwd=..., 393 env=..., 394 universal_newlines: Literal[False] = ... 395 ) -> Tuple[int, bytes, bytes]: 396 ... 397 398 399@overload 400def run_command(cmd, 401 stdout=..., 402 stderr=..., 403 shell=..., 404 timeout=..., 405 cwd=..., 406 env=..., 407 universal_newlines: Literal[True] = ... 408 ) -> Tuple[int, str, str]: 409 ... 410 411 412def run_command(cmd, 413 stdout=None, 414 stderr=None, 415 shell=False, 416 timeout=None, 417 cwd=None, 418 env=None, 419 universal_newlines=False): 420 """Runs a command in a subprocess. 421 422 This function is very similar to subprocess.check_output. The main 423 difference is that it returns the return code and std error output as well 424 as supporting a timeout parameter. 425 426 Args: 427 cmd: string or list of strings, the command to run. 428 See subprocess.Popen() documentation. 429 stdout: file handle, the file handle to write std out to. If None is 430 given, then subprocess.PIPE is used. See subprocess.Popen() 431 documentation. 432 stderr: file handle, the file handle to write std err to. If None is 433 given, then subprocess.PIPE is used. See subprocess.Popen() 434 documentation. 435 shell: bool, True to run this command through the system shell, 436 False to invoke it directly. See subprocess.Popen() docs. 437 timeout: float, the number of seconds to wait before timing out. 438 If not specified, no timeout takes effect. 439 cwd: string, the path to change the child's current directory to before 440 it is executed. Note that this directory is not considered when 441 searching the executable, so you can't specify the program's path 442 relative to cwd. 443 env: dict, a mapping that defines the environment variables for the 444 new process. Default behavior is inheriting the current process' 445 environment. 446 universal_newlines: bool, True to open file objects in text mode, False in 447 binary mode. 448 449 Returns: 450 A 3-tuple of the consisting of the return code, the std output, and the 451 std error. 452 453 Raises: 454 subprocess.TimeoutExpired: The command timed out. 455 """ 456 if stdout is None: 457 stdout = subprocess.PIPE 458 if stderr is None: 459 stderr = subprocess.PIPE 460 process = subprocess.Popen(cmd, 461 stdout=stdout, 462 stderr=stderr, 463 shell=shell, 464 cwd=cwd, 465 env=env, 466 universal_newlines=universal_newlines) 467 timer = None 468 timer_triggered = threading.Event() 469 if timeout and timeout > 0: 470 # The wait method on process will hang when used with PIPEs with large 471 # outputs, so use a timer thread instead. 472 473 def timeout_expired(): 474 timer_triggered.set() 475 process.terminate() 476 477 timer = threading.Timer(timeout, timeout_expired) 478 timer.start() 479 # If the command takes longer than the timeout, then the timer thread 480 # will kill the subprocess, which will make it terminate. 481 out, err = process.communicate() 482 if timer is not None: 483 timer.cancel() 484 if timer_triggered.is_set(): 485 raise subprocess.TimeoutExpired(cmd=cmd, 486 timeout=timeout, 487 output=out, 488 stderr=err) 489 return process.returncode, out, err 490 491 492def start_standing_subprocess(cmd, shell=False, env=None): 493 """Starts a long-running subprocess. 494 495 This is not a blocking call and the subprocess started by it should be 496 explicitly terminated with stop_standing_subprocess. 497 498 For short-running commands, you should use subprocess.check_call, which 499 blocks. 500 501 Args: 502 cmd: string, the command to start the subprocess with. 503 shell: bool, True to run this command through the system shell, 504 False to invoke it directly. See subprocess.Proc() docs. 505 env: dict, a custom environment to run the standing subprocess. If not 506 specified, inherits the current environment. See subprocess.Popen() 507 docs. 508 509 Returns: 510 The subprocess that was started. 511 """ 512 logging.debug('Starting standing subprocess with: %s', cmd) 513 proc = subprocess.Popen(cmd, 514 stdin=subprocess.PIPE, 515 stdout=subprocess.PIPE, 516 stderr=subprocess.PIPE, 517 shell=shell, 518 env=env) 519 # Leaving stdin open causes problems for input, e.g. breaking the 520 # code.inspect() shell (http://stackoverflow.com/a/25512460/1612937), so 521 # explicitly close it assuming it is not needed for standing subprocesses. 522 proc.stdin.close() 523 proc.stdin = None 524 logging.debug('Started standing subprocess %d', proc.pid) 525 return proc 526 527 528def stop_standing_subprocess(proc): 529 """Stops a subprocess started by start_standing_subprocess. 530 531 Before killing the process, we check if the process is running, if it has 532 terminated, Error is raised. 533 534 Catches and ignores the PermissionError which only happens on Macs. 535 536 Args: 537 proc: Subprocess to terminate. 538 539 Raises: 540 Error: if the subprocess could not be stopped. 541 """ 542 logging.debug('Stopping standing subprocess %d', proc.pid) 543 544 _kill_process_tree(proc) 545 546 # Call wait and close pipes on the original Python object so we don't get 547 # runtime warnings. 548 if proc.stdout: 549 proc.stdout.close() 550 if proc.stderr: 551 proc.stderr.close() 552 proc.wait() 553 logging.debug('Stopped standing subprocess %d', proc.pid) 554 555 556def wait_for_standing_subprocess(proc, timeout=None): 557 """Waits for a subprocess started by start_standing_subprocess to finish 558 or times out. 559 560 Propagates the exception raised by the subprocess.wait(.) function. 561 The subprocess.TimeoutExpired exception is raised if the process timed-out 562 rather than terminating. 563 564 If no exception is raised: the subprocess terminated on its own. No need 565 to call stop_standing_subprocess() to kill it. 566 567 If an exception is raised: the subprocess is still alive - it did not 568 terminate. Either call stop_standing_subprocess() to kill it, or call 569 wait_for_standing_subprocess() to keep waiting for it to terminate on its 570 own. 571 572 If the corresponding subprocess command generates a large amount of output 573 and this method is called with a timeout value, then the command can hang 574 indefinitely. See http://go/pylib/subprocess.html#subprocess.Popen.wait 575 576 This function does not support Python 2. 577 578 Args: 579 p: Subprocess to wait for. 580 timeout: An integer number of seconds to wait before timing out. 581 """ 582 proc.wait(timeout) 583 584 585def get_available_host_port(): 586 """Gets a host port number available for adb forward. 587 588 Returns: 589 An integer representing a port number on the host available for adb 590 forward. 591 592 Raises: 593 Error: when no port is found after MAX_PORT_ALLOCATION_RETRY times. 594 """ 595 # Only import adb module if needed. 596 from mobly.controllers.android_device_lib import adb 597 port = portpicker.pick_unused_port() 598 if not adb.is_adb_available(): 599 return port 600 for _ in range(MAX_PORT_ALLOCATION_RETRY): 601 # Make sure adb is not using this port so we don't accidentally 602 # interrupt ongoing runs by trying to bind to the port. 603 if port not in adb.list_occupied_adb_ports(): 604 return port 605 port = portpicker.pick_unused_port() 606 raise Error('Failed to find available port after {} retries'.format( 607 MAX_PORT_ALLOCATION_RETRY)) 608 609 610def grep(regex, output): 611 """Similar to linux's `grep`, this returns the line in an output stream 612 that matches a given regex pattern. 613 614 It does not rely on the `grep` binary and is not sensitive to line endings, 615 so it can be used cross-platform. 616 617 Args: 618 regex: string, a regex that matches the expected pattern. 619 output: byte string, the raw output of the adb cmd. 620 621 Returns: 622 A list of strings, all of which are output lines that matches the 623 regex pattern. 624 """ 625 lines = output.decode('utf-8').strip().splitlines() 626 results = [] 627 for line in lines: 628 if re.search(regex, line): 629 results.append(line.strip()) 630 return results 631 632 633def cli_cmd_to_string(args): 634 """Converts a cmd arg list to string. 635 636 Args: 637 args: list of strings, the arguments of a command. 638 639 Returns: 640 String representation of the command. 641 """ 642 if isinstance(args, str): 643 # Return directly if it's already a string. 644 return args 645 return ' '.join([pipes.quote(arg) for arg in args]) 646 647 648def get_settable_properties(cls): 649 """Gets the settable properties of a class. 650 651 Only returns the explicitly defined properties with setters. 652 653 Args: 654 cls: A class in Python. 655 """ 656 results = [] 657 for attr, value in vars(cls).items(): 658 if isinstance(value, property) and value.fset is not None: 659 results.append(attr) 660 return results 661 662 663def find_subclasses_in_module(base_classes, module): 664 """Finds the subclasses of the given classes in the given module. 665 666 Args: 667 base_classes: list of classes, the base classes to look for the 668 subclasses of in the module. 669 module: module, the module to look for the subclasses in. 670 671 Returns: 672 A list of all of the subclasses found in the module. 673 """ 674 subclasses = [] 675 for _, module_member in module.__dict__.items(): 676 if inspect.isclass(module_member): 677 for base_class in base_classes: 678 if issubclass(module_member, base_class): 679 subclasses.append(module_member) 680 return subclasses 681 682 683def find_subclass_in_module(base_class, module): 684 """Finds the single subclass of the given base class in the given module. 685 686 Args: 687 base_class: class, the base class to look for a subclass of in the module. 688 module: module, the module to look for the single subclass in. 689 690 Returns: 691 The single subclass of the given base class. 692 693 Raises: 694 ValueError: If the number of subclasses found was not exactly one. 695 """ 696 subclasses = find_subclasses_in_module([base_class], module) 697 if len(subclasses) != 1: 698 raise ValueError( 699 'Expected 1 subclass of %s per module, found %s.' % 700 (base_class.__name__, [subclass.__name__ for subclass in subclasses])) 701 return subclasses[0] 702