• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#
2# Copyright (C) 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17import base64
18import concurrent.futures
19import datetime
20import functools
21import json
22import logging
23import os
24import random
25import re
26import signal
27import string
28import subprocess
29import time
30import traceback
31
32try:
33    # TODO: remove when we stop supporting Python 2
34    import thread
35except ImportError as e:
36    import _thread as thread
37
38# File name length is limited to 255 chars on some OS, so we need to make sure
39# the file names we output fits within the limit.
40MAX_FILENAME_LEN = 255
41# Path length is limited to 4096 chars on some OS, so we need to make sure
42# the path we output fits within the limit.
43MAX_PATH_LEN = 4096
44
45
46class VTSUtilsError(Exception):
47    """Generic error raised for exceptions in VTS utils."""
48
49
50class NexusModelNames:
51    # TODO(angli): This will be fixed later by angli.
52    ONE = 'sprout'
53    N5 = 'hammerhead'
54    N5v2 = 'bullhead'
55    N6 = 'shamu'
56    N6v2 = 'angler'
57
58
59ascii_letters_and_digits = string.ascii_letters + string.digits
60valid_filename_chars = "-_." + ascii_letters_and_digits
61
62models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg",
63          "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu",
64          "ryu")
65
66manufacture_name_to_model = {
67    "flo": "razor",
68    "flo_lte": "razorg",
69    "flounder": "volantis",
70    "flounder_lte": "volantisg",
71    "dragon": "ryu"
72}
73
74GMT_to_olson = {
75    "GMT-9": "America/Anchorage",
76    "GMT-8": "US/Pacific",
77    "GMT-7": "US/Mountain",
78    "GMT-6": "US/Central",
79    "GMT-5": "US/Eastern",
80    "GMT-4": "America/Barbados",
81    "GMT-3": "America/Buenos_Aires",
82    "GMT-2": "Atlantic/South_Georgia",
83    "GMT-1": "Atlantic/Azores",
84    "GMT+0": "Africa/Casablanca",
85    "GMT+1": "Europe/Amsterdam",
86    "GMT+2": "Europe/Athens",
87    "GMT+3": "Europe/Moscow",
88    "GMT+4": "Asia/Baku",
89    "GMT+5": "Asia/Oral",
90    "GMT+6": "Asia/Almaty",
91    "GMT+7": "Asia/Bangkok",
92    "GMT+8": "Asia/Hong_Kong",
93    "GMT+9": "Asia/Tokyo",
94    "GMT+10": "Pacific/Guam",
95    "GMT+11": "Pacific/Noumea",
96    "GMT+12": "Pacific/Fiji",
97    "GMT+13": "Pacific/Tongatapu",
98    "GMT-11": "Pacific/Midway",
99    "GMT-10": "Pacific/Honolulu"
100}
101
102
103def abs_path(path):
104    """Resolve the '.' and '~' in a path to get the absolute path.
105
106    Args:
107        path: The path to expand.
108
109    Returns:
110        The absolute path of the input path.
111    """
112    return os.path.abspath(os.path.expanduser(path))
113
114
115def create_dir(path):
116    """Creates a directory if it does not exist already.
117
118    Args:
119        path: The path of the directory to create.
120    """
121    full_path = abs_path(path)
122    if not os.path.exists(full_path):
123        os.makedirs(full_path)
124
125
126def get_current_epoch_time():
127    """Current epoch time in milliseconds.
128
129    Returns:
130        An integer representing the current epoch time in milliseconds.
131    """
132    return int(round(time.time() * 1000))
133
134
135def get_current_human_time():
136    """Returns the current time in human readable format.
137
138    Returns:
139        The current time stamp in Month-Day-Year Hour:Min:Sec format.
140    """
141    return time.strftime("%m-%d-%Y %H:%M:%S ")
142
143
144def epoch_to_human_time(epoch_time):
145    """Converts an epoch timestamp to human readable time.
146
147    This essentially converts an output of get_current_epoch_time to an output
148    of get_current_human_time
149
150    Args:
151        epoch_time: An integer representing an epoch timestamp in milliseconds.
152
153    Returns:
154        A time string representing the input time.
155        None if input param is invalid.
156    """
157    if isinstance(epoch_time, int):
158        try:
159            d = datetime.datetime.fromtimestamp(epoch_time / 1000)
160            return d.strftime("%m-%d-%Y %H:%M:%S ")
161        except ValueError:
162            return None
163
164
165def get_timezone_olson_id():
166    """Return the Olson ID of the local (non-DST) timezone.
167
168    Returns:
169        A string representing one of the Olson IDs of the local (non-DST)
170        timezone.
171    """
172    tzoffset = int(time.timezone / 3600)
173    gmt = None
174    if tzoffset <= 0:
175        gmt = "GMT+{}".format(-tzoffset)
176    else:
177        gmt = "GMT-{}".format(tzoffset)
178    return GMT_to_olson[gmt]
179
180
181def find_files(paths, file_predicate):
182    """Locate files whose names and extensions match the given predicate in
183    the specified directories.
184
185    Args:
186        paths: A list of directory paths where to find the files.
187        file_predicate: A function that returns True if the file name and
188          extension are desired.
189
190    Returns:
191        A list of files that match the predicate.
192    """
193    file_list = []
194    for path in paths:
195        p = abs_path(path)
196        for dirPath, subdirList, fileList in os.walk(p):
197            for fname in fileList:
198                name, ext = os.path.splitext(fname)
199                if file_predicate(name, ext):
200                    file_list.append((dirPath, name, ext))
201    return file_list
202
203
204def iterate_files(dir_path):
205    """A generator yielding regular files in a directory recursively.
206
207    Args:
208        dir_path: A string representing the path to search.
209
210    Yields:
211        A tuple of strings (directory, file). The directory containing
212        the file and the file name.
213    """
214    for root_dir, dir_names, file_names in os.walk(dir_path):
215        for file_name in file_names:
216            yield root_dir, file_name
217
218
219def load_config(file_full_path):
220    """Loads a JSON config file.
221
222    Returns:
223        A JSON object.
224    """
225    if not os.path.isfile(file_full_path):
226        logging.warning('cwd: %s', os.getcwd())
227        pypath = os.environ['PYTHONPATH']
228        if pypath:
229            for base_path in pypath.split(':'):
230                logging.debug('checking base_path %s', base_path)
231                new_path = os.path.join(base_path, file_full_path)
232                if os.path.isfile(new_path):
233                    logging.debug('new_path %s found', new_path)
234                    file_full_path = new_path
235                    break
236
237    with open(file_full_path, 'r') as f:
238        conf = json.load(f)
239        return conf
240
241
242def load_file_to_base64_str(f_path):
243    """Loads the content of a file into a base64 string.
244
245    Args:
246        f_path: full path to the file including the file name.
247
248    Returns:
249        A base64 string representing the content of the file in utf-8 encoding.
250    """
251    path = abs_path(f_path)
252    with open(path, 'rb') as f:
253        f_bytes = f.read()
254        base64_str = base64.b64encode(f_bytes).decode("utf-8")
255        return base64_str
256
257
258def find_field(item_list, cond, comparator, target_field):
259    """Finds the value of a field in a dict object that satisfies certain
260    conditions.
261
262    Args:
263        item_list: A list of dict objects.
264        cond: A param that defines the condition.
265        comparator: A function that checks if an dict satisfies the condition.
266        target_field: Name of the field whose value to be returned if an item
267            satisfies the condition.
268
269    Returns:
270        Target value or None if no item satisfies the condition.
271    """
272    for item in item_list:
273        if comparator(item, cond) and target_field in item:
274            return item[target_field]
275    return None
276
277
278def rand_ascii_str(length):
279    """Generates a random string of specified length, composed of ascii letters
280    and digits.
281
282    Args:
283        length: The number of characters in the string.
284
285    Returns:
286        The random string generated.
287    """
288    letters = [random.choice(ascii_letters_and_digits) for i in range(length)]
289    return ''.join(letters)
290
291
292# Thead/Process related functions.
293def concurrent_exec(func, param_list):
294    """Executes a function with different parameters pseudo-concurrently.
295
296    This is basically a map function. Each element (should be an iterable) in
297    the param_list is unpacked and passed into the function. Due to Python's
298    GIL, there's no true concurrency. This is suited for IO-bound tasks.
299
300    Args:
301        func: The function that parforms a task.
302        param_list: A list of iterables, each being a set of params to be
303            passed into the function.
304
305    Returns:
306        A list of return values from each function execution. If an execution
307        caused an exception, the exception object will be the corresponding
308        result.
309    """
310    with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
311        # Start the load operations and mark each future with its params
312        future_to_params = {executor.submit(func, *p): p for p in param_list}
313        return_vals = []
314        for future in concurrent.futures.as_completed(future_to_params):
315            params = future_to_params[future]
316            try:
317                return_vals.append(future.result())
318            except Exception as exc:
319                print("{} generated an exception: {}".format(
320                    params, traceback.format_exc()))
321                return_vals.append(exc)
322        return return_vals
323
324
325def exe_cmd(*cmds):
326    """Executes commands in a new shell.
327
328    Args:
329        cmds: A sequence of commands and arguments.
330
331    Returns:
332        The output of the command run.
333
334    Raises:
335        OSError is raised if an error occurred during the command execution.
336    """
337    cmd = ' '.join(cmds)
338    proc = subprocess.Popen(
339        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
340    (out, err) = proc.communicate()
341    if not err:
342        return out
343    raise OSError(err)
344
345
346def _assert_subprocess_running(proc):
347    """Checks if a subprocess has terminated on its own.
348
349    Args:
350        proc: A subprocess returned by subprocess.Popen.
351
352    Raises:
353        VTSUtilsError is raised if the subprocess has stopped.
354    """
355    ret = proc.poll()
356    if ret is not None:
357        out, err = proc.communicate()
358        raise VTSUtilsError("Process %d has terminated. ret: %d, stderr: %s,"
359                            " stdout: %s" % (proc.pid, ret, err, out))
360
361
362def is_on_windows():
363    """Checks whether the OS is Windows.
364
365    Returns:
366        A boolean representing whether the OS is Windows.
367    """
368    return os.name == "nt"
369
370
371def stop_current_process(terminate_timeout):
372    """Sends KeyboardInterrupt to main thread and then terminates process.
373
374    The daemon thread calls this function when timeout or user interrupt.
375
376    Args:
377        terminate_timeout: A float, the interval in seconds between interrupt
378                           and termination.
379    """
380    logging.error("Interrupt main thread.")
381    if not is_on_windows():
382        # Default SIGINT handler sends KeyboardInterrupt to main thread
383        # and unblocks it.
384        os.kill(os.getpid(), signal.SIGINT)
385    else:
386        # On Windows, raising CTRL_C_EVENT, which is received as
387        # SIGINT, has no effect on non-console process.
388        # interrupt_main() behaves like SIGINT but does not unblock
389        # main thread immediately.
390        thread.interrupt_main()
391
392    time.sleep(terminate_timeout)
393    logging.error("Terminate current process.")
394    # Send SIGTERM on Linux. Call terminateProcess() on Windows.
395    os.kill(os.getpid(), signal.SIGTERM)
396
397
398def kill_process_group(proc, signal_no=signal.SIGTERM):
399    """Sends signal to a process group.
400
401    Logs when there is an OSError or PermissionError. The latter one only
402    happens on Mac.
403
404    On Windows, SIGABRT, SIGINT, and SIGTERM are replaced with CTRL_BREAK_EVENT
405    so as to kill every subprocess in the group.
406
407    Args:
408        proc: The Popen object whose pid is the group id.
409        signal_no: The signal sent to the subprocess group.
410    """
411    pid = proc.pid
412    try:
413        if not is_on_windows():
414            os.killpg(pid, signal_no)
415        else:
416            if signal_no in [signal.SIGABRT,
417                             signal.SIGINT,
418                             signal.SIGTERM]:
419                windows_signal_no = signal.CTRL_BREAK_EVENT
420            else:
421                windows_signal_no = signal_no
422            os.kill(pid, windows_signal_no)
423    except (OSError, PermissionError) as e:
424        logging.exception("Cannot send signal %s to process group %d: %s",
425                          signal_no, pid, str(e))
426
427
428def start_standing_subprocess(cmd, check_health_delay=0):
429    """Starts a long-running subprocess.
430
431    This is not a blocking call and the subprocess started by it should be
432    explicitly terminated with stop_standing_subprocess.
433
434    For short-running commands, you should use exe_cmd, which blocks.
435
436    You can specify a health check after the subprocess is started to make sure
437    it did not stop prematurely.
438
439    Args:
440        cmd: string, the command to start the subprocess with.
441        check_health_delay: float, the number of seconds to wait after the
442                            subprocess starts to check its health. Default is 0,
443                            which means no check.
444
445    Returns:
446        The subprocess that got started.
447    """
448    if not is_on_windows():
449        proc = subprocess.Popen(
450            cmd,
451            stdout=subprocess.PIPE,
452            stderr=subprocess.PIPE,
453            shell=True,
454            preexec_fn=os.setpgrp)
455    else:
456        proc = subprocess.Popen(
457            cmd,
458            stdout=subprocess.PIPE,
459            stderr=subprocess.PIPE,
460            shell=True,
461            creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
462    logging.debug("Start standing subprocess with cmd: %s", cmd)
463    if check_health_delay > 0:
464        time.sleep(check_health_delay)
465        _assert_subprocess_running(proc)
466    return proc
467
468
469def stop_standing_subprocess(proc, signal_no=signal.SIGTERM):
470    """Stops a subprocess started by start_standing_subprocess.
471
472    Before killing the process, we check if the process is running, if it has
473    terminated, VTSUtilsError is raised.
474
475    Args:
476        proc: Subprocess to terminate.
477        signal_no: The signal sent to the subprocess group.
478    """
479    logging.debug("Stop standing subprocess %d", proc.pid)
480    _assert_subprocess_running(proc)
481    kill_process_group(proc, signal_no)
482
483
484def wait_for_standing_subprocess(proc, timeout=None):
485    """Waits for a subprocess started by start_standing_subprocess to finish
486    or times out.
487
488    Propagates the exception raised by the subprocess.wait(.) function.
489    The subprocess.TimeoutExpired exception is raised if the process timed-out
490    rather then terminating.
491
492    If no exception is raised: the subprocess terminated on its own. No need
493    to call stop_standing_subprocess() to kill it.
494
495    If an exception is raised: the subprocess is still alive - it did not
496    terminate. Either call stop_standing_subprocess() to kill it, or call
497    wait_for_standing_subprocess() to keep waiting for it to terminate on its
498    own.
499
500    Args:
501        p: Subprocess to wait for.
502        timeout: An integer number of seconds to wait before timing out.
503    """
504    proc.wait(timeout)
505
506
507def sync_device_time(ad):
508    """Sync the time of an android device with the current system time.
509
510    Both epoch time and the timezone will be synced.
511
512    Args:
513        ad: The android device to sync time on.
514    """
515    droid = ad.droid
516    droid.setTimeZone(get_timezone_olson_id())
517    droid.setTime(get_current_epoch_time())
518
519
520# Timeout decorator block
521class TimeoutError(Exception):
522    """Exception for timeout decorator related errors.
523    """
524    pass
525
526
527def _timeout_handler(signum, frame):
528    """Handler function used by signal to terminate a timed out function.
529    """
530    raise TimeoutError()
531
532
533def timeout(sec):
534    """A decorator used to add time out check to a function.
535
536    Args:
537        sec: Number of seconds to wait before the function times out.
538            No timeout if set to 0
539
540    Returns:
541        What the decorated function returns.
542
543    Raises:
544        TimeoutError is raised when time out happens.
545    """
546
547    def decorator(func):
548        @functools.wraps(func)
549        def wrapper(*args, **kwargs):
550            if sec:
551                signal.signal(signal.SIGALRM, _timeout_handler)
552                signal.alarm(sec)
553            try:
554                return func(*args, **kwargs)
555            except TimeoutError:
556                raise TimeoutError(("Function {} timed out after {} "
557                                    "seconds.").format(func.__name__, sec))
558            finally:
559                signal.alarm(0)
560
561        return wrapper
562
563    return decorator
564
565
566def trim_model_name(model):
567    """Trim any prefix and postfix and return the android designation of the
568    model name.
569
570    e.g. "m_shamu" will be trimmed to "shamu".
571
572    Args:
573        model: model name to be trimmed.
574
575    Returns
576        Trimmed model name if one of the known model names is found.
577        None otherwise.
578    """
579    # Directly look up first.
580    if model in models:
581        return model
582    if model in manufacture_name_to_model:
583        return manufacture_name_to_model[model]
584    # If not found, try trimming off prefix/postfix and look up again.
585    tokens = re.split("_|-", model)
586    for t in tokens:
587        if t in models:
588            return t
589        if t in manufacture_name_to_model:
590            return manufacture_name_to_model[t]
591    return None
592
593
594def force_airplane_mode(ad, new_state, timeout_value=60):
595    """Force the device to set airplane mode on or off by adb shell command.
596
597    Args:
598        ad: android device object.
599        new_state: Turn on airplane mode if True.
600            Turn off airplane mode if False.
601        timeout_value: max wait time for 'adb wait-for-device'
602
603    Returns:
604        True if success.
605        False if timeout.
606    """
607    # Using timeout decorator.
608    # Wait for device with timeout. If after <timeout_value> seconds, adb
609    # is still waiting for device, throw TimeoutError exception.
610    @timeout(timeout_value)
611    def wait_for_device_with_timeout(ad):
612        ad.adb.wait_for_device()
613
614    try:
615        wait_for_device_with_timeout(ad)
616        ad.adb.shell("settings put global airplane_mode_on {}".format(
617            1 if new_state else 0))
618    except TimeoutError:
619        # adb wait for device timeout
620        return False
621    return True
622
623
624def enable_doze(ad):
625    """Force the device into doze mode.
626
627    Args:
628        ad: android device object.
629
630    Returns:
631        True if device is in doze mode.
632        False otherwise.
633    """
634    ad.adb.shell("dumpsys battery unplug")
635    ad.adb.shell("dumpsys deviceidle enable")
636    if (ad.adb.shell("dumpsys deviceidle force-idle") !=
637            b'Now forced in to idle mode\r\n'):
638        return False
639    ad.droid.goToSleepNow()
640    time.sleep(5)
641    adb_shell_result = ad.adb.shell("dumpsys deviceidle step")
642    if adb_shell_result not in [b'Stepped to: IDLE_MAINTENANCE\r\n',
643                                b'Stepped to: IDLE\r\n']:
644        info = ("dumpsys deviceidle step: {}dumpsys battery: {}"
645                "dumpsys deviceidle: {}".format(
646                    adb_shell_result.decode('utf-8'),
647                    ad.adb.shell("dumpsys battery").decode('utf-8'),
648                    ad.adb.shell("dumpsys deviceidle").decode('utf-8')))
649        print(info)
650        return False
651    return True
652
653
654def disable_doze(ad):
655    """Force the device not in doze mode.
656
657    Args:
658        ad: android device object.
659
660    Returns:
661        True if device is not in doze mode.
662        False otherwise.
663    """
664    ad.adb.shell("dumpsys deviceidle disable")
665    ad.adb.shell("dumpsys battery reset")
666    adb_shell_result = ad.adb.shell("dumpsys deviceidle step")
667    if (adb_shell_result != b'Stepped to: ACTIVE\r\n'):
668        info = ("dumpsys deviceidle step: {}dumpsys battery: {}"
669                "dumpsys deviceidle: {}".format(
670                    adb_shell_result.decode('utf-8'),
671                    ad.adb.shell("dumpsys battery").decode('utf-8'),
672                    ad.adb.shell("dumpsys deviceidle").decode('utf-8')))
673        print(info)
674        return False
675    return True
676
677
678def set_ambient_display(ad, new_state):
679    """Set "Ambient Display" in Settings->Display
680
681    Args:
682        ad: android device object.
683        new_state: new state for "Ambient Display". True or False.
684    """
685    ad.adb.shell("settings put secure doze_enabled {}".format(1 if new_state
686                                                              else 0))
687
688
689def set_adaptive_brightness(ad, new_state):
690    """Set "Adaptive Brightness" in Settings->Display
691
692    Args:
693        ad: android device object.
694        new_state: new state for "Adaptive Brightness". True or False.
695    """
696    ad.adb.shell("settings put system screen_brightness_mode {}".format(
697        1 if new_state else 0))
698
699
700def set_auto_rotate(ad, new_state):
701    """Set "Auto-rotate" in QuickSetting
702
703    Args:
704        ad: android device object.
705        new_state: new state for "Auto-rotate". True or False.
706    """
707    ad.adb.shell("settings put system accelerometer_rotation {}".format(
708        1 if new_state else 0))
709
710
711def set_location_service(ad, new_state):
712    """Set Location service on/off in Settings->Location
713
714    Args:
715        ad: android device object.
716        new_state: new state for "Location service".
717            If new_state is False, turn off location service.
718            If new_state if True, set location service to "High accuracy".
719    """
720    if new_state:
721        ad.adb.shell("settings put secure location_providers_allowed +gps")
722        ad.adb.shell("settings put secure location_providers_allowed +network")
723    else:
724        ad.adb.shell("settings put secure location_providers_allowed -gps")
725        ad.adb.shell("settings put secure location_providers_allowed -network")
726
727
728def set_mobile_data_always_on(ad, new_state):
729    """Set Mobile_Data_Always_On feature bit
730
731    Args:
732        ad: android device object.
733        new_state: new state for "mobile_data_always_on"
734            if new_state is False, set mobile_data_always_on disabled.
735            if new_state if True, set mobile_data_always_on enabled.
736    """
737    ad.adb.shell("settings put global mobile_data_always_on {}".format(
738        1 if new_state else 0))
739