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