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