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