• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2016 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import base64
18import concurrent.futures
19import copy
20import datetime
21import functools
22import ipaddress
23import json
24import logging
25import os
26import platform
27import psutil
28import random
29import re
30import signal
31import string
32import socket
33import subprocess
34import time
35import threading
36import traceback
37import zipfile
38from concurrent.futures import ThreadPoolExecutor
39from zeroconf import IPVersion, Zeroconf
40
41from acts import signals
42from acts.controllers.adb_lib.error import AdbError
43from acts.libs.proc import job
44
45# File name length is limited to 255 chars on some OS, so we need to make sure
46# the file names we output fits within the limit.
47MAX_FILENAME_LEN = 255
48
49
50class ActsUtilsError(Exception):
51    """Generic error raised for exceptions in ACTS utils."""
52
53
54class NexusModelNames:
55    # TODO(angli): This will be fixed later by angli.
56    ONE = 'sprout'
57    N5 = 'hammerhead'
58    N5v2 = 'bullhead'
59    N6 = 'shamu'
60    N6v2 = 'angler'
61    N6v3 = 'marlin'
62    N5v3 = 'sailfish'
63
64
65class DozeModeStatus:
66    ACTIVE = "ACTIVE"
67    IDLE = "IDLE"
68
69
70ascii_letters_and_digits = string.ascii_letters + string.digits
71valid_filename_chars = "-_." + ascii_letters_and_digits
72
73models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg",
74          "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu",
75          "ryu", "marlin", "sailfish")
76
77manufacture_name_to_model = {
78    "flo": "razor",
79    "flo_lte": "razorg",
80    "flounder": "volantis",
81    "flounder_lte": "volantisg",
82    "dragon": "ryu"
83}
84
85GMT_to_olson = {
86    "GMT-9": "America/Anchorage",
87    "GMT-8": "US/Pacific",
88    "GMT-7": "US/Mountain",
89    "GMT-6": "US/Central",
90    "GMT-5": "US/Eastern",
91    "GMT-4": "America/Barbados",
92    "GMT-3": "America/Buenos_Aires",
93    "GMT-2": "Atlantic/South_Georgia",
94    "GMT-1": "Atlantic/Azores",
95    "GMT+0": "Africa/Casablanca",
96    "GMT+1": "Europe/Amsterdam",
97    "GMT+2": "Europe/Athens",
98    "GMT+3": "Europe/Moscow",
99    "GMT+4": "Asia/Baku",
100    "GMT+5": "Asia/Oral",
101    "GMT+6": "Asia/Almaty",
102    "GMT+7": "Asia/Bangkok",
103    "GMT+8": "Asia/Hong_Kong",
104    "GMT+9": "Asia/Tokyo",
105    "GMT+10": "Pacific/Guam",
106    "GMT+11": "Pacific/Noumea",
107    "GMT+12": "Pacific/Fiji",
108    "GMT+13": "Pacific/Tongatapu",
109    "GMT-11": "Pacific/Midway",
110    "GMT-10": "Pacific/Honolulu"
111}
112
113
114def abs_path(path):
115    """Resolve the '.' and '~' in a path to get the absolute path.
116
117    Args:
118        path: The path to expand.
119
120    Returns:
121        The absolute path of the input path.
122    """
123    return os.path.abspath(os.path.expanduser(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 get_next_device(test_bed_controllers, used_devices):
182    """Gets the next device in a list of testbed controllers
183
184    Args:
185        test_bed_controllers: A list of testbed controllers of a particular
186            type, for example a list ACTS Android devices.
187        used_devices: A list of devices that have been used.  This can be a
188            mix of devices, for example a fuchsia device and an Android device.
189    Returns:
190        The next device in the test_bed_controllers list or None if there are
191        no items that are not in the used devices list.
192    """
193    if test_bed_controllers:
194        device_list = test_bed_controllers
195    else:
196        raise ValueError('test_bed_controllers is empty.')
197    for used_device in used_devices:
198        if used_device in device_list:
199            device_list.remove(used_device)
200    if device_list:
201        return device_list[0]
202    else:
203        return None
204
205
206def find_files(paths, file_predicate):
207    """Locate files whose names and extensions match the given predicate in
208    the specified directories.
209
210    Args:
211        paths: A list of directory paths where to find the files.
212        file_predicate: A function that returns True if the file name and
213          extension are desired.
214
215    Returns:
216        A list of files that match the predicate.
217    """
218    file_list = []
219    if not isinstance(paths, list):
220        paths = [paths]
221    for path in paths:
222        p = abs_path(path)
223        for dirPath, subdirList, fileList in os.walk(p):
224            for fname in fileList:
225                name, ext = os.path.splitext(fname)
226                if file_predicate(name, ext):
227                    file_list.append((dirPath, name, ext))
228    return file_list
229
230
231def load_config(file_full_path, log_errors=True):
232    """Loads a JSON config file.
233
234    Returns:
235        A JSON object.
236    """
237    with open(file_full_path, 'r') as f:
238        try:
239            return json.load(f)
240        except Exception as e:
241            if log_errors:
242                logging.error("Exception error to load %s: %s", f, e)
243            raise
244
245
246def load_file_to_base64_str(f_path):
247    """Loads the content of a file into a base64 string.
248
249    Args:
250        f_path: full path to the file including the file name.
251
252    Returns:
253        A base64 string representing the content of the file in utf-8 encoding.
254    """
255    path = abs_path(f_path)
256    with open(path, 'rb') as f:
257        f_bytes = f.read()
258        base64_str = base64.b64encode(f_bytes).decode("utf-8")
259        return base64_str
260
261
262def dump_string_to_file(content, file_path, mode='w'):
263    """ Dump content of a string to
264
265    Args:
266        content: content to be dumped to file
267        file_path: full path to the file including the file name.
268        mode: file open mode, 'w' (truncating file) by default
269    :return:
270    """
271    full_path = abs_path(file_path)
272    with open(full_path, mode) as f:
273        f.write(content)
274
275
276def list_of_dict_to_dict_of_dict(list_of_dicts, dict_key):
277    """Transforms a list of dicts to a dict of dicts.
278
279    For instance:
280    >>> list_of_dict_to_dict_of_dict([{'a': '1', 'b':'2'},
281    >>>                               {'a': '3', 'b':'4'}],
282    >>>                              'b')
283
284    returns:
285
286    >>> {'2': {'a': '1', 'b':'2'},
287    >>>  '4': {'a': '3', 'b':'4'}}
288
289    Args:
290        list_of_dicts: A list of dictionaries.
291        dict_key: The key in the inner dict to be used as the key for the
292                  outer dict.
293    Returns:
294        A dict of dicts.
295    """
296    return {d[dict_key]: d for d in list_of_dicts}
297
298
299def dict_purge_key_if_value_is_none(dictionary):
300    """Removes all pairs with value None from dictionary."""
301    for k, v in dict(dictionary).items():
302        if v is None:
303            del dictionary[k]
304    return dictionary
305
306
307def find_field(item_list, cond, comparator, target_field):
308    """Finds the value of a field in a dict object that satisfies certain
309    conditions.
310
311    Args:
312        item_list: A list of dict objects.
313        cond: A param that defines the condition.
314        comparator: A function that checks if an dict satisfies the condition.
315        target_field: Name of the field whose value to be returned if an item
316            satisfies the condition.
317
318    Returns:
319        Target value or None if no item satisfies the condition.
320    """
321    for item in item_list:
322        if comparator(item, cond) and target_field in item:
323            return item[target_field]
324    return None
325
326
327def rand_ascii_str(length):
328    """Generates a random string of specified length, composed of ascii letters
329    and digits.
330
331    Args:
332        length: The number of characters in the string.
333
334    Returns:
335        The random string generated.
336    """
337    letters = [random.choice(ascii_letters_and_digits) for i in range(length)]
338    return ''.join(letters)
339
340
341def rand_hex_str(length):
342    """Generates a random string of specified length, composed of hex digits
343
344    Args:
345        length: The number of characters in the string.
346
347    Returns:
348        The random string generated.
349    """
350    letters = [random.choice(string.hexdigits) for i in range(length)]
351    return ''.join(letters)
352
353
354# Thead/Process related functions.
355def concurrent_exec(func, param_list):
356    """Executes a function with different parameters pseudo-concurrently.
357
358    This is basically a map function. Each element (should be an iterable) in
359    the param_list is unpacked and passed into the function. Due to Python's
360    GIL, there's no true concurrency. This is suited for IO-bound tasks.
361
362    Args:
363        func: The function that parforms a task.
364        param_list: A list of iterables, each being a set of params to be
365            passed into the function.
366
367    Returns:
368        A list of return values from each function execution. If an execution
369        caused an exception, the exception object will be the corresponding
370        result.
371    """
372    with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
373        # Start the load operations and mark each future with its params
374        future_to_params = {executor.submit(func, *p): p for p in param_list}
375        return_vals = []
376        for future in concurrent.futures.as_completed(future_to_params):
377            params = future_to_params[future]
378            try:
379                return_vals.append(future.result())
380            except Exception as exc:
381                print("{} generated an exception: {}".format(
382                    params, traceback.format_exc()))
383                return_vals.append(exc)
384        return return_vals
385
386
387def exe_cmd(*cmds):
388    """Executes commands in a new shell.
389
390    Args:
391        cmds: A sequence of commands and arguments.
392
393    Returns:
394        The output of the command run.
395
396    Raises:
397        OSError is raised if an error occurred during the command execution.
398    """
399    cmd = ' '.join(cmds)
400    proc = subprocess.Popen(cmd,
401                            stdout=subprocess.PIPE,
402                            stderr=subprocess.PIPE,
403                            shell=True)
404    (out, err) = proc.communicate()
405    if not err:
406        return out
407    raise OSError(err)
408
409
410def require_sl4a(android_devices):
411    """Makes sure sl4a connection is established on the given AndroidDevice
412    objects.
413
414    Args:
415        android_devices: A list of AndroidDevice objects.
416
417    Raises:
418        AssertionError is raised if any given android device does not have SL4A
419        connection established.
420    """
421    for ad in android_devices:
422        msg = "SL4A connection not established properly on %s." % ad.serial
423        assert ad.droid, msg
424
425
426def _assert_subprocess_running(proc):
427    """Checks if a subprocess has terminated on its own.
428
429    Args:
430        proc: A subprocess returned by subprocess.Popen.
431
432    Raises:
433        ActsUtilsError is raised if the subprocess has stopped.
434    """
435    ret = proc.poll()
436    if ret is not None:
437        out, err = proc.communicate()
438        raise ActsUtilsError("Process %d has terminated. ret: %d, stderr: %s,"
439                             " stdout: %s" % (proc.pid, ret, err, out))
440
441
442def start_standing_subprocess(cmd, check_health_delay=0, shell=True):
443    """Starts a long-running subprocess.
444
445    This is not a blocking call and the subprocess started by it should be
446    explicitly terminated with stop_standing_subprocess.
447
448    For short-running commands, you should use exe_cmd, which blocks.
449
450    You can specify a health check after the subprocess is started to make sure
451    it did not stop prematurely.
452
453    Args:
454        cmd: string, the command to start the subprocess with.
455        check_health_delay: float, the number of seconds to wait after the
456                            subprocess starts to check its health. Default is 0,
457                            which means no check.
458
459    Returns:
460        The subprocess that got started.
461    """
462    proc = subprocess.Popen(cmd,
463                            stdout=subprocess.PIPE,
464                            stderr=subprocess.PIPE,
465                            shell=shell,
466                            preexec_fn=os.setpgrp)
467    logging.debug("Start standing subprocess with cmd: %s", cmd)
468    if check_health_delay > 0:
469        time.sleep(check_health_delay)
470        _assert_subprocess_running(proc)
471    return proc
472
473
474def stop_standing_subprocess(proc, kill_signal=signal.SIGTERM):
475    """Stops a subprocess started by start_standing_subprocess.
476
477    Before killing the process, we check if the process is running, if it has
478    terminated, ActsUtilsError is raised.
479
480    Catches and ignores the PermissionError which only happens on Macs.
481
482    Args:
483        proc: Subprocess to terminate.
484    """
485    pid = proc.pid
486    logging.debug("Stop standing subprocess %d", pid)
487    _assert_subprocess_running(proc)
488    try:
489        os.killpg(pid, kill_signal)
490    except PermissionError:
491        pass
492
493
494def wait_for_standing_subprocess(proc, timeout=None):
495    """Waits for a subprocess started by start_standing_subprocess to finish
496    or times out.
497
498    Propagates the exception raised by the subprocess.wait(.) function.
499    The subprocess.TimeoutExpired exception is raised if the process timed-out
500    rather then terminating.
501
502    If no exception is raised: the subprocess terminated on its own. No need
503    to call stop_standing_subprocess() to kill it.
504
505    If an exception is raised: the subprocess is still alive - it did not
506    terminate. Either call stop_standing_subprocess() to kill it, or call
507    wait_for_standing_subprocess() to keep waiting for it to terminate on its
508    own.
509
510    Args:
511        p: Subprocess to wait for.
512        timeout: An integer number of seconds to wait before timing out.
513    """
514    proc.wait(timeout)
515
516
517def sync_device_time(ad):
518    """Sync the time of an android device with the current system time.
519
520    Both epoch time and the timezone will be synced.
521
522    Args:
523        ad: The android device to sync time on.
524    """
525    ad.adb.shell("settings put global auto_time 0", ignore_status=True)
526    ad.adb.shell("settings put global auto_time_zone 0", ignore_status=True)
527    droid = ad.droid
528    droid.setTimeZone(get_timezone_olson_id())
529    droid.setTime(get_current_epoch_time())
530
531
532# Timeout decorator block
533class TimeoutError(Exception):
534    """Exception for timeout decorator related errors.
535    """
536    pass
537
538
539def _timeout_handler(signum, frame):
540    """Handler function used by signal to terminate a timed out function.
541    """
542    raise TimeoutError()
543
544
545def timeout(sec):
546    """A decorator used to add time out check to a function.
547
548    This only works in main thread due to its dependency on signal module.
549    Do NOT use it if the decorated funtion does not run in the Main thread.
550
551    Args:
552        sec: Number of seconds to wait before the function times out.
553            No timeout if set to 0
554
555    Returns:
556        What the decorated function returns.
557
558    Raises:
559        TimeoutError is raised when time out happens.
560    """
561    def decorator(func):
562        @functools.wraps(func)
563        def wrapper(*args, **kwargs):
564            if sec:
565                signal.signal(signal.SIGALRM, _timeout_handler)
566                signal.alarm(sec)
567            try:
568                return func(*args, **kwargs)
569            except TimeoutError:
570                raise TimeoutError(("Function {} timed out after {} "
571                                    "seconds.").format(func.__name__, sec))
572            finally:
573                signal.alarm(0)
574
575        return wrapper
576
577    return decorator
578
579
580def trim_model_name(model):
581    """Trim any prefix and postfix and return the android designation of the
582    model name.
583
584    e.g. "m_shamu" will be trimmed to "shamu".
585
586    Args:
587        model: model name to be trimmed.
588
589    Returns
590        Trimmed model name if one of the known model names is found.
591        None otherwise.
592    """
593    # Directly look up first.
594    if model in models:
595        return model
596    if model in manufacture_name_to_model:
597        return manufacture_name_to_model[model]
598    # If not found, try trimming off prefix/postfix and look up again.
599    tokens = re.split("_|-", model)
600    for t in tokens:
601        if t in models:
602            return t
603        if t in manufacture_name_to_model:
604            return manufacture_name_to_model[t]
605    return None
606
607
608def force_airplane_mode(ad, new_state, timeout_value=60):
609    """Force the device to set airplane mode on or off by adb shell command.
610
611    Args:
612        ad: android device object.
613        new_state: Turn on airplane mode if True.
614            Turn off airplane mode if False.
615        timeout_value: max wait time for 'adb wait-for-device'
616
617    Returns:
618        True if success.
619        False if timeout.
620    """
621
622    # Using timeout decorator.
623    # Wait for device with timeout. If after <timeout_value> seconds, adb
624    # is still waiting for device, throw TimeoutError exception.
625    @timeout(timeout_value)
626    def wait_for_device_with_timeout(ad):
627        ad.adb.wait_for_device()
628
629    try:
630        wait_for_device_with_timeout(ad)
631        ad.adb.shell("settings put global airplane_mode_on {}".format(
632            1 if new_state else 0))
633        ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE")
634    except TimeoutError:
635        # adb wait for device timeout
636        return False
637    return True
638
639
640def get_battery_level(ad):
641    """Gets battery level from device
642
643    Returns:
644        battery_level: int indicating battery level
645    """
646    output = ad.adb.shell("dumpsys battery")
647    match = re.search(r"level: (?P<battery_level>\S+)", output)
648    battery_level = int(match.group("battery_level"))
649    return battery_level
650
651
652def get_device_usb_charging_status(ad):
653    """ Returns the usb charging status of the device.
654
655    Args:
656        ad: android device object
657
658    Returns:
659        True if charging
660        False if not charging
661     """
662    adb_shell_result = ad.adb.shell("dumpsys deviceidle get charging")
663    ad.log.info("Device Charging State: {}".format(adb_shell_result))
664    return adb_shell_result == 'true'
665
666
667def disable_usb_charging(ad):
668    """ Unplug device from usb charging.
669
670    Args:
671        ad: android device object
672
673    Returns:
674        True if device is unplugged
675        False otherwise
676    """
677    ad.adb.shell("dumpsys battery unplug")
678    if not get_device_usb_charging_status(ad):
679        return True
680    else:
681        ad.log.info("Could not disable USB charging")
682        return False
683
684
685def enable_usb_charging(ad):
686    """ Plug device to usb charging.
687
688    Args:
689        ad: android device object
690
691    Returns:
692        True if device is Plugged
693        False otherwise
694    """
695    ad.adb.shell("dumpsys battery reset")
696    if get_device_usb_charging_status(ad):
697        return True
698    else:
699        ad.log.info("Could not enable USB charging")
700        return False
701
702
703def enable_doze(ad):
704    """Force the device into doze mode.
705
706    Args:
707        ad: android device object.
708
709    Returns:
710        True if device is in doze mode.
711        False otherwise.
712    """
713    ad.adb.shell("dumpsys battery unplug")
714    ad.adb.shell("dumpsys deviceidle enable")
715    ad.adb.shell("dumpsys deviceidle force-idle")
716    ad.droid.goToSleepNow()
717    time.sleep(5)
718    adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep")
719    if not adb_shell_result.startswith(DozeModeStatus.IDLE):
720        info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result))
721        print(info)
722        return False
723    return True
724
725
726def disable_doze(ad):
727    """Force the device not in doze mode.
728
729    Args:
730        ad: android device object.
731
732    Returns:
733        True if device is not in doze mode.
734        False otherwise.
735    """
736    ad.adb.shell("dumpsys deviceidle disable")
737    ad.adb.shell("dumpsys battery reset")
738    adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep")
739    if not adb_shell_result.startswith(DozeModeStatus.ACTIVE):
740        info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result))
741        print(info)
742        return False
743    return True
744
745
746def enable_doze_light(ad):
747    """Force the device into doze light mode.
748
749    Args:
750        ad: android device object.
751
752    Returns:
753        True if device is in doze light mode.
754        False otherwise.
755    """
756    ad.adb.shell("dumpsys battery unplug")
757    ad.droid.goToSleepNow()
758    time.sleep(5)
759    ad.adb.shell("cmd deviceidle enable light")
760    ad.adb.shell("cmd deviceidle step light")
761    adb_shell_result = ad.adb.shell("dumpsys deviceidle get light")
762    if not adb_shell_result.startswith(DozeModeStatus.IDLE):
763        info = ("dumpsys deviceidle get light: {}".format(adb_shell_result))
764        print(info)
765        return False
766    return True
767
768
769def disable_doze_light(ad):
770    """Force the device not in doze light mode.
771
772    Args:
773        ad: android device object.
774
775    Returns:
776        True if device is not in doze light mode.
777        False otherwise.
778    """
779    ad.adb.shell("dumpsys battery reset")
780    ad.adb.shell("cmd deviceidle disable light")
781    adb_shell_result = ad.adb.shell("dumpsys deviceidle get light")
782    if not adb_shell_result.startswith(DozeModeStatus.ACTIVE):
783        info = ("dumpsys deviceidle get light: {}".format(adb_shell_result))
784        print(info)
785        return False
786    return True
787
788
789def set_ambient_display(ad, new_state):
790    """Set "Ambient Display" in Settings->Display
791
792    Args:
793        ad: android device object.
794        new_state: new state for "Ambient Display". True or False.
795    """
796    ad.adb.shell(
797        "settings put secure doze_enabled {}".format(1 if new_state else 0))
798
799
800def set_adaptive_brightness(ad, new_state):
801    """Set "Adaptive Brightness" in Settings->Display
802
803    Args:
804        ad: android device object.
805        new_state: new state for "Adaptive Brightness". True or False.
806    """
807    ad.adb.shell("settings put system screen_brightness_mode {}".format(
808        1 if new_state else 0))
809
810
811def set_auto_rotate(ad, new_state):
812    """Set "Auto-rotate" in QuickSetting
813
814    Args:
815        ad: android device object.
816        new_state: new state for "Auto-rotate". True or False.
817    """
818    ad.adb.shell("settings put system accelerometer_rotation {}".format(
819        1 if new_state else 0))
820
821
822def set_location_service(ad, new_state):
823    """Set Location service on/off in Settings->Location
824
825    Args:
826        ad: android device object.
827        new_state: new state for "Location service".
828            If new_state is False, turn off location service.
829            If new_state if True, set location service to "High accuracy".
830    """
831    ad.adb.shell("content insert --uri "
832                 " content://com.google.settings/partner --bind "
833                 "name:s:network_location_opt_in --bind value:s:1")
834    ad.adb.shell("content insert --uri "
835                 " content://com.google.settings/partner --bind "
836                 "name:s:use_location_for_services --bind value:s:1")
837    if new_state:
838        ad.adb.shell("settings put secure location_mode 3")
839    else:
840        ad.adb.shell("settings put secure location_mode 0")
841
842
843def set_mobile_data_always_on(ad, new_state):
844    """Set Mobile_Data_Always_On feature bit
845
846    Args:
847        ad: android device object.
848        new_state: new state for "mobile_data_always_on"
849            if new_state is False, set mobile_data_always_on disabled.
850            if new_state if True, set mobile_data_always_on enabled.
851    """
852    ad.adb.shell("settings put global mobile_data_always_on {}".format(
853        1 if new_state else 0))
854
855
856def bypass_setup_wizard(ad):
857    """Bypass the setup wizard on an input Android device
858
859    Args:
860        ad: android device object.
861
862    Returns:
863        True if Android device successfully bypassed the setup wizard.
864        False if failed.
865    """
866    try:
867        ad.adb.shell("am start -n \"com.google.android.setupwizard/"
868                     ".SetupWizardExitActivity\"")
869        logging.debug("No error during default bypass call.")
870    except AdbError as adb_error:
871        if adb_error.stdout == "ADB_CMD_OUTPUT:0":
872            if adb_error.stderr and \
873                    not adb_error.stderr.startswith("Error type 3\n"):
874                logging.error("ADB_CMD_OUTPUT:0, but error is %s " %
875                              adb_error.stderr)
876                raise adb_error
877            logging.debug("Bypass wizard call received harmless error 3: "
878                          "No setup to bypass.")
879        elif adb_error.stdout == "ADB_CMD_OUTPUT:255":
880            # Run it again as root.
881            ad.adb.root_adb()
882            logging.debug("Need root access to bypass setup wizard.")
883            try:
884                ad.adb.shell("am start -n \"com.google.android.setupwizard/"
885                             ".SetupWizardExitActivity\"")
886                logging.debug("No error during rooted bypass call.")
887            except AdbError as adb_error:
888                if adb_error.stdout == "ADB_CMD_OUTPUT:0":
889                    if adb_error.stderr and \
890                            not adb_error.stderr.startswith("Error type 3\n"):
891                        logging.error("Rooted ADB_CMD_OUTPUT:0, but error is "
892                                      "%s " % adb_error.stderr)
893                        raise adb_error
894                    logging.debug(
895                        "Rooted bypass wizard call received harmless "
896                        "error 3: No setup to bypass.")
897
898    # magical sleep to wait for the gservices override broadcast to complete
899    time.sleep(3)
900
901    provisioned_state = int(
902        ad.adb.shell("settings get global device_provisioned"))
903    if provisioned_state != 1:
904        logging.error("Failed to bypass setup wizard.")
905        return False
906    logging.debug("Setup wizard successfully bypassed.")
907    return True
908
909
910def parse_ping_ouput(ad, count, out, loss_tolerance=20):
911    """Ping Parsing util.
912
913    Args:
914        ad: Android Device Object.
915        count: Number of ICMP packets sent
916        out: shell output text of ping operation
917        loss_tolerance: Threshold after which flag test as false
918    Returns:
919        False: if packet loss is more than loss_tolerance%
920        True: if all good
921    """
922    result = re.search(
923        r"(\d+) packets transmitted, (\d+) received, (\d+)% packet loss", out)
924    if not result:
925        ad.log.info("Ping failed with %s", out)
926        return False
927
928    packet_loss = int(result.group(3))
929    packet_xmit = int(result.group(1))
930    packet_rcvd = int(result.group(2))
931    min_packet_xmit_rcvd = (100 - loss_tolerance) * 0.01
932    if (packet_loss > loss_tolerance
933            or packet_xmit < count * min_packet_xmit_rcvd
934            or packet_rcvd < count * min_packet_xmit_rcvd):
935        ad.log.error("%s, ping failed with loss more than tolerance %s%%",
936                     result.group(0), loss_tolerance)
937        return False
938    ad.log.info("Ping succeed with %s", result.group(0))
939    return True
940
941
942def adb_shell_ping(ad,
943                   count=120,
944                   dest_ip="www.google.com",
945                   timeout=200,
946                   loss_tolerance=20):
947    """Ping utility using adb shell.
948
949    Args:
950        ad: Android Device Object.
951        count: Number of ICMP packets to send
952        dest_ip: hostname or IP address
953                 default www.google.com
954        timeout: timeout for icmp pings to complete.
955    """
956    ping_cmd = "ping -W 1"
957    if count:
958        ping_cmd += " -c %d" % count
959    if dest_ip:
960        ping_cmd += " %s" % dest_ip
961    try:
962        ad.log.info("Starting ping test to %s using adb command %s", dest_ip,
963                    ping_cmd)
964        out = ad.adb.shell(ping_cmd, timeout=timeout, ignore_status=True)
965        if not parse_ping_ouput(ad, count, out, loss_tolerance):
966            return False
967        return True
968    except Exception as e:
969        ad.log.warning("Ping Test to %s failed with exception %s", dest_ip, e)
970        return False
971
972
973def unzip_maintain_permissions(zip_path, extract_location):
974    """Unzip a .zip file while maintaining permissions.
975
976    Args:
977        zip_path: The path to the zipped file.
978        extract_location: the directory to extract to.
979    """
980    with zipfile.ZipFile(zip_path, 'r') as zip_file:
981        for info in zip_file.infolist():
982            _extract_file(zip_file, info, extract_location)
983
984
985def _extract_file(zip_file, zip_info, extract_location):
986    """Extracts a single entry from a ZipFile while maintaining permissions.
987
988    Args:
989        zip_file: A zipfile.ZipFile.
990        zip_info: A ZipInfo object from zip_file.
991        extract_location: The directory to extract to.
992    """
993    out_path = zip_file.extract(zip_info.filename, path=extract_location)
994    perm = zip_info.external_attr >> 16
995    os.chmod(out_path, perm)
996
997
998def get_directory_size(path):
999    """Computes the total size of the files in a directory, including subdirectories.
1000
1001    Args:
1002        path: The path of the directory.
1003    Returns:
1004        The size of the provided directory.
1005    """
1006    total = 0
1007    for dirpath, dirnames, filenames in os.walk(path):
1008        for filename in filenames:
1009            total += os.path.getsize(os.path.join(dirpath, filename))
1010    return total
1011
1012
1013def get_command_uptime(command_regex):
1014    """Returns the uptime for a given command.
1015
1016    Args:
1017        command_regex: A regex that matches the command line given. Must be
1018            pgrep compatible.
1019    """
1020    pid = job.run('pgrep -f %s' % command_regex).stdout
1021    runtime = ''
1022    if pid:
1023        runtime = job.run('ps -o etime= -p "%s"' % pid).stdout
1024    return runtime
1025
1026
1027def get_process_uptime(process):
1028    """Returns the runtime in [[dd-]hh:]mm:ss, or '' if not running."""
1029    pid = job.run('pidof %s' % process, ignore_status=True).stdout
1030    runtime = ''
1031    if pid:
1032        runtime = job.run('ps -o etime= -p "%s"' % pid).stdout
1033    return runtime
1034
1035
1036def get_device_process_uptime(adb, process):
1037    """Returns the uptime of a device process."""
1038    pid = adb.shell('pidof %s' % process, ignore_status=True)
1039    runtime = ''
1040    if pid:
1041        runtime = adb.shell('ps -o etime= -p "%s"' % pid)
1042    return runtime
1043
1044
1045def wait_until(func, timeout_s, condition=True, sleep_s=1.0):
1046    """Executes a function repeatedly until condition is met.
1047
1048    Args:
1049      func: The function pointer to execute.
1050      timeout_s: Amount of time (in seconds) to wait before raising an
1051                 exception.
1052      condition: The ending condition of the WaitUntil loop.
1053      sleep_s: The amount of time (in seconds) to sleep between each function
1054               execution.
1055
1056    Returns:
1057      The time in seconds before detecting a successful condition.
1058
1059    Raises:
1060      TimeoutError: If the condition was never met and timeout is hit.
1061    """
1062    start_time = time.time()
1063    end_time = start_time + timeout_s
1064    count = 0
1065    while True:
1066        count += 1
1067        if func() == condition:
1068            return time.time() - start_time
1069        if time.time() > end_time:
1070            break
1071        time.sleep(sleep_s)
1072    raise TimeoutError('Failed to complete function %s in %d seconds having '
1073                       'attempted %d times.' % (str(func), timeout_s, count))
1074
1075
1076# Adapted from
1077# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python
1078# Available under the Creative Commons Attribution-ShareAlike License
1079def levenshtein(string1, string2):
1080    """Returns the Levenshtein distance of two strings.
1081    Uses Dynamic Programming approach, only keeping track of
1082    two rows of the DP table at a time.
1083
1084    Args:
1085      string1: String to compare to string2
1086      string2: String to compare to string1
1087
1088    Returns:
1089      distance: the Levenshtein distance between string1 and string2
1090    """
1091
1092    if len(string1) < len(string2):
1093        return levenshtein(string2, string1)
1094
1095    if len(string2) == 0:
1096        return len(string1)
1097
1098    previous_row = range(len(string2) + 1)
1099    for i, char1 in enumerate(string1):
1100        current_row = [i + 1]
1101        for j, char2 in enumerate(string2):
1102            insertions = previous_row[j + 1] + 1
1103            deletions = current_row[j] + 1
1104            substitutions = previous_row[j] + (char1 != char2)
1105            current_row.append(min(insertions, deletions, substitutions))
1106        previous_row = current_row
1107
1108    return previous_row[-1]
1109
1110
1111def string_similarity(s1, s2):
1112    """Returns a similarity measurement based on Levenshtein distance.
1113
1114    Args:
1115      s1: the string to compare to s2
1116      s2: the string to compare to s1
1117
1118    Returns:
1119      result: the similarity metric
1120    """
1121    lev = levenshtein(s1, s2)
1122    try:
1123        lev_ratio = float(lev) / max(len(s1), len(s2))
1124        result = (1.0 - lev_ratio) * 100
1125    except ZeroDivisionError:
1126        result = 100 if not s2 else 0
1127    return float(result)
1128
1129
1130def run_concurrent_actions_no_raise(*calls):
1131    """Concurrently runs all callables passed in using multithreading.
1132
1133    Example:
1134
1135    >>> def test_function_1(arg1, arg2):
1136    >>>     return arg1, arg2
1137    >>>
1138    >>> def test_function_2(arg1, kwarg='kwarg'):
1139    >>>     raise arg1(kwarg)
1140    >>>
1141    >>> run_concurrent_actions_no_raise(
1142    >>>     lambda: test_function_1('arg1', 'arg2'),
1143    >>>     lambda: test_function_2(IndexError, kwarg='kwarg'),
1144    >>> )
1145    >>> # Output:
1146    >>> [('arg1', 'arg2'), IndexError('kwarg')]
1147
1148    Args:
1149        *calls: A *args list of argumentless callable objects to be called. Note
1150            that if a function has arguments it can be turned into an
1151            argumentless function via the lambda keyword or functools.partial.
1152
1153    Returns:
1154        An array of the returned values or exceptions received from calls,
1155        respective of the order given.
1156    """
1157    with ThreadPoolExecutor(max_workers=len(calls)) as executor:
1158        futures = [executor.submit(call) for call in calls]
1159
1160    results = []
1161    for future in futures:
1162        try:
1163            results.append(future.result())
1164        except Exception as e:
1165            results.append(e)
1166    return results
1167
1168
1169def run_concurrent_actions(*calls):
1170    """Runs all callables passed in concurrently using multithreading.
1171
1172    Examples:
1173
1174    >>> def test_function_1(arg1, arg2):
1175    >>>     print(arg1, arg2)
1176    >>>
1177    >>> def test_function_2(arg1, kwarg='kwarg'):
1178    >>>     raise arg1(kwarg)
1179    >>>
1180    >>> run_concurrent_actions(
1181    >>>     lambda: test_function_1('arg1', 'arg2'),
1182    >>>     lambda: test_function_2(IndexError, kwarg='kwarg'),
1183    >>> )
1184    >>> 'The above line raises IndexError("kwarg")'
1185
1186    Args:
1187        *calls: A *args list of argumentless callable objects to be called. Note
1188            that if a function has arguments it can be turned into an
1189            argumentless function via the lambda keyword or functools.partial.
1190
1191    Returns:
1192        An array of the returned values respective of the order of the calls
1193        argument.
1194
1195    Raises:
1196        If an exception is raised in any of the calls, the first exception
1197        caught will be raised.
1198    """
1199    first_exception = None
1200
1201    class WrappedException(Exception):
1202        """Raised when a passed-in callable raises an exception."""
1203
1204    def call_wrapper(call):
1205        nonlocal first_exception
1206
1207        try:
1208            return call()
1209        except Exception as e:
1210            logging.exception(e)
1211            # Note that there is a potential race condition between two
1212            # exceptions setting first_exception. Even if a locking mechanism
1213            # was added to prevent this from happening, it is still possible
1214            # that we capture the second exception as the first exception, as
1215            # the active thread can swap to the thread that raises the second
1216            # exception. There is no way to solve this with the tools we have
1217            # here, so we do not bother. The effects this issue has on the
1218            # system as a whole are negligible.
1219            if first_exception is None:
1220                first_exception = e
1221            raise WrappedException(e)
1222
1223    with ThreadPoolExecutor(max_workers=len(calls)) as executor:
1224        futures = [executor.submit(call_wrapper, call) for call in calls]
1225
1226    results = []
1227    for future in futures:
1228        try:
1229            results.append(future.result())
1230        except WrappedException:
1231            # We do not need to raise here, since first_exception will already
1232            # be set to the first exception raised by these callables.
1233            break
1234
1235    if first_exception:
1236        raise first_exception
1237
1238    return results
1239
1240
1241def test_concurrent_actions(*calls, failure_exceptions=(Exception, )):
1242    """Concurrently runs all passed in calls using multithreading.
1243
1244    If any callable raises an Exception found within failure_exceptions, the
1245    test case is marked as a failure.
1246
1247    Example:
1248    >>> def test_function_1(arg1, arg2):
1249    >>>     print(arg1, arg2)
1250    >>>
1251    >>> def test_function_2(kwarg='kwarg'):
1252    >>>     raise IndexError(kwarg)
1253    >>>
1254    >>> test_concurrent_actions(
1255    >>>     lambda: test_function_1('arg1', 'arg2'),
1256    >>>     lambda: test_function_2(kwarg='kwarg'),
1257    >>>     failure_exceptions=IndexError
1258    >>> )
1259    >>> 'raises signals.TestFailure due to IndexError being raised.'
1260
1261    Args:
1262        *calls: A *args list of argumentless callable objects to be called. Note
1263            that if a function has arguments it can be turned into an
1264            argumentless function via the lambda keyword or functools.partial.
1265        failure_exceptions: A tuple of all possible Exceptions that will mark
1266            the test as a FAILURE. Any exception that is not in this list will
1267            mark the tests as UNKNOWN.
1268
1269    Returns:
1270        An array of the returned values respective of the order of the calls
1271        argument.
1272
1273    Raises:
1274        signals.TestFailure if any call raises an Exception.
1275    """
1276    try:
1277        return run_concurrent_actions(*calls)
1278    except signals.TestFailure:
1279        # Do not modify incoming test failures
1280        raise
1281    except failure_exceptions as e:
1282        raise signals.TestFailure(e)
1283
1284
1285class SuppressLogOutput(object):
1286    """Context manager used to suppress all logging output for the specified
1287    logger and level(s).
1288    """
1289    def __init__(self, logger=logging.getLogger(), log_levels=None):
1290        """Create a SuppressLogOutput context manager
1291
1292        Args:
1293            logger: The logger object to suppress
1294            log_levels: Levels of log handlers to disable.
1295        """
1296
1297        self._logger = logger
1298        self._log_levels = log_levels or [
1299            logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
1300            logging.CRITICAL
1301        ]
1302        if isinstance(self._log_levels, int):
1303            self._log_levels = [self._log_levels]
1304        self._handlers = copy.copy(self._logger.handlers)
1305
1306    def __enter__(self):
1307        for handler in self._handlers:
1308            if handler.level in self._log_levels:
1309                self._logger.removeHandler(handler)
1310        return self
1311
1312    def __exit__(self, *_):
1313        for handler in self._handlers:
1314            self._logger.addHandler(handler)
1315
1316
1317class BlockingTimer(object):
1318    """Context manager used to block until a specified amount of time has
1319     elapsed.
1320     """
1321    def __init__(self, secs):
1322        """Initializes a BlockingTimer
1323
1324        Args:
1325            secs: Number of seconds to wait before exiting
1326        """
1327        self._thread = threading.Timer(secs, lambda: None)
1328
1329    def __enter__(self):
1330        self._thread.start()
1331        return self
1332
1333    def __exit__(self, *_):
1334        self._thread.join()
1335
1336
1337def is_valid_ipv4_address(address):
1338    try:
1339        socket.inet_pton(socket.AF_INET, address)
1340    except AttributeError:  # no inet_pton here, sorry
1341        try:
1342            socket.inet_aton(address)
1343        except socket.error:
1344            return False
1345        return address.count('.') == 3
1346    except socket.error:  # not a valid address
1347        return False
1348
1349    return True
1350
1351
1352def is_valid_ipv6_address(address):
1353    if '%' in address:
1354        address = address.split('%')[0]
1355    try:
1356        socket.inet_pton(socket.AF_INET6, address)
1357    except socket.error:  # not a valid address
1358        return False
1359    return True
1360
1361
1362def merge_dicts(*dict_args):
1363    """ Merges args list of dictionaries into a single dictionary.
1364
1365    Args:
1366        dict_args: an args list of dictionaries to be merged. If multiple
1367            dictionaries share a key, the last in the list will appear in the
1368            final result.
1369    """
1370    result = {}
1371    for dictionary in dict_args:
1372        result.update(dictionary)
1373    return result
1374
1375
1376def ascii_string(uc_string):
1377    """Converts unicode string to ascii"""
1378    return str(uc_string).encode('ASCII')
1379
1380
1381def get_interface_ip_addresses(comm_channel, interface):
1382    """Gets all of the ip addresses, ipv4 and ipv6, associated with a
1383       particular interface name.
1384
1385    Args:
1386        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
1387            etc.  Must have the run function implemented.
1388        interface: The interface name on the device, ie eth0
1389
1390    Returns:
1391        A list of dictionaries of the the various IP addresses:
1392            ipv4_private_local_addresses: Any 192.168, 172.16, 10, or 169.254
1393                addresses
1394            ipv4_public_addresses: Any IPv4 public addresses
1395            ipv6_link_local_addresses: Any fe80:: addresses
1396            ipv6_private_local_addresses: Any fd00:: addresses
1397            ipv6_public_addresses: Any publicly routable addresses
1398    """
1399    # Local imports are used here to prevent cyclic dependency.
1400    from acts.controllers.android_device import AndroidDevice
1401    from acts.controllers.fuchsia_device import FuchsiaDevice
1402    from acts.controllers.utils_lib.ssh.connection import SshConnection
1403    ipv4_private_local_addresses = []
1404    ipv4_public_addresses = []
1405    ipv6_link_local_addresses = []
1406    ipv6_private_local_addresses = []
1407    ipv6_public_addresses = []
1408    is_local = comm_channel == job
1409    if type(comm_channel) is AndroidDevice:
1410        all_interfaces_and_addresses = comm_channel.adb.shell(
1411            'ip -o addr | awk \'!/^[0-9]*: ?lo|link\/ether/ {gsub("/", " "); '
1412            'print $2" "$4}\'')
1413        ifconfig_output = comm_channel.adb.shell('ifconfig %s' % interface)
1414    elif (type(comm_channel) is SshConnection or is_local):
1415        all_interfaces_and_addresses = comm_channel.run(
1416            'ip -o addr | awk \'!/^[0-9]*: ?lo|link\/ether/ {gsub("/", " "); '
1417            'print $2" "$4}\'').stdout
1418        ifconfig_output = comm_channel.run('ifconfig %s' % interface).stdout
1419    elif type(comm_channel) is FuchsiaDevice:
1420        all_interfaces_and_addresses = []
1421        comm_channel.netstack_lib.init()
1422        interfaces = comm_channel.netstack_lib.netstackListInterfaces()
1423        if interfaces.get('error') is not None:
1424            raise ActsUtilsError('Failed with {}'.format(
1425                interfaces.get('error')))
1426        for item in interfaces.get('result'):
1427            for ipv4_address in item['ipv4_addresses']:
1428                ipv4_address = '.'.join(map(str, ipv4_address))
1429                all_interfaces_and_addresses.append(
1430                    '%s %s' % (item['name'], ipv4_address))
1431            for ipv6_address in item['ipv6_addresses']:
1432                converted_ipv6_address = []
1433                for octet in ipv6_address:
1434                    converted_ipv6_address.append(format(octet, 'x').zfill(2))
1435                ipv6_address = ''.join(converted_ipv6_address)
1436                ipv6_address = (':'.join(
1437                    ipv6_address[i:i + 4]
1438                    for i in range(0, len(ipv6_address), 4)))
1439                all_interfaces_and_addresses.append(
1440                    '%s %s' %
1441                    (item['name'], str(ipaddress.ip_address(ipv6_address))))
1442        all_interfaces_and_addresses = '\n'.join(all_interfaces_and_addresses)
1443        ifconfig_output = all_interfaces_and_addresses
1444    else:
1445        raise ValueError('Unsupported method to send command to device.')
1446
1447    for interface_line in all_interfaces_and_addresses.split('\n'):
1448        if interface != interface_line.split()[0]:
1449            continue
1450        on_device_ip = ipaddress.ip_address(interface_line.split()[1])
1451        if on_device_ip.version == 4:
1452            if on_device_ip.is_private:
1453                if str(on_device_ip) in ifconfig_output:
1454                    ipv4_private_local_addresses.append(str(on_device_ip))
1455            elif on_device_ip.is_global or (
1456                    # Carrier private doesn't have a property, so we check if
1457                    # all other values are left unset.
1458                    not on_device_ip.is_reserved
1459                    and not on_device_ip.is_unspecified
1460                    and not on_device_ip.is_link_local
1461                    and not on_device_ip.is_loopback
1462                    and not on_device_ip.is_multicast):
1463                if str(on_device_ip) in ifconfig_output:
1464                    ipv4_public_addresses.append(str(on_device_ip))
1465        elif on_device_ip.version == 6:
1466            if on_device_ip.is_link_local:
1467                if str(on_device_ip) in ifconfig_output:
1468                    ipv6_link_local_addresses.append(str(on_device_ip))
1469            elif on_device_ip.is_private:
1470                if str(on_device_ip) in ifconfig_output:
1471                    ipv6_private_local_addresses.append(str(on_device_ip))
1472            elif on_device_ip.is_global:
1473                if str(on_device_ip) in ifconfig_output:
1474                    ipv6_public_addresses.append(str(on_device_ip))
1475    return {
1476        'ipv4_private': ipv4_private_local_addresses,
1477        'ipv4_public': ipv4_public_addresses,
1478        'ipv6_link_local': ipv6_link_local_addresses,
1479        'ipv6_private_local': ipv6_private_local_addresses,
1480        'ipv6_public': ipv6_public_addresses
1481    }
1482
1483
1484def get_interface_based_on_ip(comm_channel, desired_ip_address):
1485    """Gets the interface for a particular IP
1486
1487    Args:
1488        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
1489            etc.  Must have the run function implemented.
1490        desired_ip_address: The IP address that is being looked for on a device.
1491
1492    Returns:
1493        The name of the test interface.
1494    """
1495
1496    desired_ip_address = desired_ip_address.split('%', 1)[0]
1497    all_ips_and_interfaces = comm_channel.run(
1498        '(ip -o -4 addr show; ip -o -6 addr show) | '
1499        'awk \'{print $2" "$4}\'').stdout
1500    #ipv4_addresses = comm_channel.run(
1501    #    'ip -o -4 addr show| awk \'{print $2": "$4}\'').stdout
1502    #ipv6_addresses = comm_channel._ssh_session.run(
1503    #    'ip -o -6 addr show| awk \'{print $2": "$4}\'').stdout
1504    #if desired_ip_address in ipv4_addresses:
1505    #    ip_addresses_to_search = ipv4_addresses
1506    #elif desired_ip_address in ipv6_addresses:
1507    #    ip_addresses_to_search = ipv6_addresses
1508    for ip_address_and_interface in all_ips_and_interfaces.split('\n'):
1509        if desired_ip_address in ip_address_and_interface:
1510            return ip_address_and_interface.split()[1][:-1]
1511    return None
1512
1513
1514def renew_linux_ip_address(comm_channel, interface):
1515    comm_channel.run('sudo ifconfig %s down' % interface)
1516    comm_channel.run('sudo ifconfig %s up' % interface)
1517    comm_channel.run('sudo dhclient -r %s' % interface)
1518    comm_channel.run('sudo dhclient %s' % interface)
1519
1520
1521def get_ping_command(dest_ip,
1522                     count=3,
1523                     interval=1000,
1524                     timeout=1000,
1525                     size=56,
1526                     os_type='Linux',
1527                     additional_ping_params=None):
1528    """Builds ping command string based on address type, os, and params.
1529
1530    Args:
1531        dest_ip: string, address to ping (ipv4 or ipv6)
1532        count: int, number of requests to send
1533        interval: int, time in seconds between requests
1534        timeout: int, time in seconds to wait for response
1535        size: int, number of bytes to send,
1536        os_type: string, os type of the source device (supports 'Linux',
1537            'Darwin')
1538        additional_ping_params: string, command option flags to
1539            append to the command string
1540
1541    Returns:
1542        List of string, represetning the ping command.
1543    """
1544    if is_valid_ipv4_address(dest_ip):
1545        ping_binary = 'ping'
1546    elif is_valid_ipv6_address(dest_ip):
1547        ping_binary = 'ping6'
1548    else:
1549        raise ValueError('Invalid ip addr: %s' % dest_ip)
1550
1551    if os_type == 'Darwin':
1552        if is_valid_ipv6_address(dest_ip):
1553            # ping6 on MacOS doesn't support timeout
1554            logging.warn(
1555                'Ignoring timeout, as ping6 on MacOS does not support it.')
1556            timeout_flag = []
1557        else:
1558            timeout_flag = ['-t', str(timeout / 1000)]
1559    elif os_type == 'Linux':
1560        timeout_flag = ['-W', str(timeout / 1000)]
1561    else:
1562        raise ValueError('Invalid OS.  Only Linux and MacOS are supported.')
1563
1564    if not additional_ping_params:
1565        additional_ping_params = ''
1566
1567    ping_cmd = [
1568        ping_binary, *timeout_flag, '-c',
1569        str(count), '-i',
1570        str(interval / 1000), '-s',
1571        str(size), additional_ping_params, dest_ip
1572    ]
1573    return ' '.join(ping_cmd)
1574
1575
1576def ping(comm_channel,
1577         dest_ip,
1578         count=3,
1579         interval=1000,
1580         timeout=1000,
1581         size=56,
1582         additional_ping_params=None):
1583    """ Generic linux ping function, supports local (acts.libs.proc.job) and
1584    SshConnections (acts.libs.proc.job over ssh) to Linux based OSs and MacOS.
1585
1586    NOTES: This will work with Android over SSH, but does not function over ADB
1587    as that has a unique return format.
1588
1589    Args:
1590        comm_channel: communication channel over which to send ping command.
1591            Must have 'run' function that returns at least command, stdout,
1592            stderr, and exit_status (see acts.libs.proc.job)
1593        dest_ip: address to ping (ipv4 or ipv6)
1594        count: int, number of packets to send
1595        interval: int, time in milliseconds between pings
1596        timeout: int, time in milliseconds to wait for response
1597        size: int, size of packets in bytes
1598        additional_ping_params: string, command option flags to
1599            append to the command string
1600
1601    Returns:
1602        Dict containing:
1603            command: string
1604            exit_status: int (0 or 1)
1605            stdout: string
1606            stderr: string
1607            transmitted: int, number of packets transmitted
1608            received: int, number of packets received
1609            packet_loss: int, percentage packet loss
1610            time: int, time of ping command execution (in milliseconds)
1611            rtt_min: float, minimum round trip time
1612            rtt_avg: float, average round trip time
1613            rtt_max: float, maximum round trip time
1614            rtt_mdev: float, round trip time standard deviation
1615
1616        Any values that cannot be parsed are left as None
1617    """
1618    from acts.controllers.utils_lib.ssh.connection import SshConnection
1619    is_local = comm_channel == job
1620    os_type = platform.system() if is_local else 'Linux'
1621    ping_cmd = get_ping_command(dest_ip,
1622                                count=count,
1623                                interval=interval,
1624                                timeout=timeout,
1625                                size=size,
1626                                os_type=os_type,
1627                                additional_ping_params=additional_ping_params)
1628
1629    if (type(comm_channel) is SshConnection or is_local):
1630        logging.debug(
1631            'Running ping with parameters (count: %s, interval: %s, timeout: '
1632            '%s, size: %s)' % (count, interval, timeout, size))
1633        ping_result = comm_channel.run(ping_cmd, ignore_status=True)
1634    else:
1635        raise ValueError('Unsupported comm_channel: %s' % type(comm_channel))
1636
1637    if isinstance(ping_result, job.Error):
1638        ping_result = ping_result.result
1639
1640    transmitted = None
1641    received = None
1642    packet_loss = None
1643    time = None
1644    rtt_min = None
1645    rtt_avg = None
1646    rtt_max = None
1647    rtt_mdev = None
1648
1649    summary = re.search(
1650        '([0-9]+) packets transmitted.*?([0-9]+) received.*?([0-9]+)% packet '
1651        'loss.*?time ([0-9]+)', ping_result.stdout)
1652    if summary:
1653        transmitted = summary[1]
1654        received = summary[2]
1655        packet_loss = summary[3]
1656        time = summary[4]
1657
1658    rtt_stats = re.search('= ([0-9.]+)/([0-9.]+)/([0-9.]+)/([0-9.]+)',
1659                          ping_result.stdout)
1660    if rtt_stats:
1661        rtt_min = rtt_stats[1]
1662        rtt_avg = rtt_stats[2]
1663        rtt_max = rtt_stats[3]
1664        rtt_mdev = rtt_stats[4]
1665
1666    return {
1667        'command': ping_result.command,
1668        'exit_status': ping_result.exit_status,
1669        'stdout': ping_result.stdout,
1670        'stderr': ping_result.stderr,
1671        'transmitted': transmitted,
1672        'received': received,
1673        'packet_loss': packet_loss,
1674        'time': time,
1675        'rtt_min': rtt_min,
1676        'rtt_avg': rtt_avg,
1677        'rtt_max': rtt_max,
1678        'rtt_mdev': rtt_mdev
1679    }
1680
1681
1682def can_ping(comm_channel,
1683             dest_ip,
1684             count=1,
1685             interval=1000,
1686             timeout=1000,
1687             size=56,
1688             additional_ping_params=None):
1689    """Returns whether device connected via comm_channel can ping a dest
1690    address"""
1691    ping_results = ping(comm_channel,
1692                        dest_ip,
1693                        count=count,
1694                        interval=interval,
1695                        timeout=timeout,
1696                        size=size,
1697                        additional_ping_params=additional_ping_params)
1698
1699    return ping_results['exit_status'] == 0
1700
1701
1702def ip_in_subnet(ip, subnet):
1703    """Validate that ip is in a given subnet.
1704
1705    Args:
1706        ip: string, ip address to verify (eg. '192.168.42.158')
1707        subnet: string, subnet to check (eg. '192.168.42.0/24')
1708
1709    Returns:
1710        True, if ip in subnet, else False
1711    """
1712    return ipaddress.ip_address(ip) in ipaddress.ip_network(subnet)
1713
1714
1715def mac_address_str_to_list(mac_addr_str):
1716    """Converts mac address string to list of decimal octets.
1717
1718    Args:
1719        mac_addr_string: string, mac address
1720            e.g. '12:34:56:78:9a:bc'
1721
1722    Returns
1723        list, representing mac address octets in decimal
1724            e.g. [18, 52, 86, 120, 154, 188]
1725    """
1726    return [int(octet, 16) for octet in mac_addr_str.split(':')]
1727
1728
1729def mac_address_list_to_str(mac_addr_list):
1730    """Converts list of decimal octets represeting mac address to string.
1731
1732    Args:
1733        mac_addr_list: list, representing mac address octets in decimal
1734            e.g. [18, 52, 86, 120, 154, 188]
1735
1736    Returns:
1737        string, mac address
1738            e.g. '12:34:56:78:9a:bc'
1739    """
1740    hex_list = []
1741    for octet in mac_addr_list:
1742        hex_octet = hex(octet)[2:]
1743        if octet < 16:
1744            hex_list.append('0%s' % hex_octet)
1745        else:
1746            hex_list.append(hex_octet)
1747
1748    return ':'.join(hex_list)
1749
1750
1751def get_fuchsia_mdns_ipv6_address(device_mdns_name):
1752    """Gets the ipv6 link local address from a fuchsia device over mdns
1753
1754    Args:
1755        device_mdns_name: name of fuchsia device, ie gig-clone-sugar-slash
1756
1757    Returns:
1758        string, ipv6 link local address
1759    """
1760    if not device_mdns_name:
1761        return None
1762    mdns_type = '_fuchsia._udp.local.'
1763    interface_list = psutil.net_if_addrs()
1764    for interface in interface_list:
1765        interface_ipv6_link_local = \
1766            get_interface_ip_addresses(job, interface)['ipv6_link_local']
1767        if 'fe80::1' in interface_ipv6_link_local:
1768            logging.info('Removing IPv6 loopback IP from %s interface list.'
1769                         '  Not modifying actual system IP addresses.' %
1770                         interface)
1771            # This is needed as the Zeroconf library crashes if you try to
1772            # instantiate it on a IPv6 loopback IP address.
1773            interface_ipv6_link_local.remove('fe80::1')
1774
1775        if interface_ipv6_link_local:
1776            zeroconf = Zeroconf(ip_version=IPVersion.V6Only,
1777                                interfaces=interface_ipv6_link_local)
1778            device_records = (zeroconf.get_service_info(
1779                mdns_type, device_mdns_name + '.' + mdns_type))
1780            if device_records:
1781                for device_ip_address in device_records.parsed_addresses():
1782                    device_ip_address = ipaddress.ip_address(device_ip_address)
1783                    if (device_ip_address.version == 6
1784                            and device_ip_address.is_link_local):
1785                        if ping(job,
1786                                dest_ip='%s%%%s' %
1787                                (str(device_ip_address),
1788                                 interface))['exit_status'] == 0:
1789                            zeroconf.close()
1790                            del zeroconf
1791                            return ('%s%%%s' %
1792                                    (str(device_ip_address), interface))
1793            zeroconf.close()
1794            del zeroconf
1795    logging.error('Unable to get ip address for %s' % device_mdns_name)
1796    return None
1797