VTS integration framework initial refactor.
Bug=28004487
Commiting the integraton framework in the directory structure as
discussed offline.
Change-Id: I0314431e3b906a195290cd9b4630c93e07b494ca
diff --git a/utils/python/__init__.py b/utils/python/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/__init__.py
diff --git a/utils/python/controllers/__init__.py b/utils/python/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/__init__.py
diff --git a/utils/python/controllers/adb.py b/utils/python/controllers/adb.py
new file mode 100644
index 0000000..c6a0e96
--- /dev/null
+++ b/utils/python/controllers/adb.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3.4
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from builtins import str
+
+import random
+import socket
+import subprocess
+import time
+
+class AdbError(Exception):
+ """Raised when there is an error in adb operations."""
+
+SL4A_LAUNCH_CMD=("am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER "
+ "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT {} "
+ "com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher" )
+
+def get_available_host_port():
+ """Gets a host port number available for adb forward.
+
+ Returns:
+ An integer representing a port number on the host available for adb
+ forward.
+ """
+ while True:
+ port = random.randint(1024, 9900)
+ if is_port_available(port):
+ return port
+
+def is_port_available(port):
+ """Checks if a given port number is available on the system.
+
+ Args:
+ port: An integer which is the port number to check.
+
+ Returns:
+ True if the port is available; False otherwise.
+ """
+ # Make sure adb is not using this port so we don't accidentally interrupt
+ # ongoing runs by trying to bind to the port.
+ if port in list_occupied_adb_ports():
+ return False
+ s = None
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(('localhost', port))
+ return True
+ except socket.error:
+ return False
+ finally:
+ if s:
+ s.close()
+
+def list_occupied_adb_ports():
+ """Lists all the host ports occupied by adb forward.
+
+ This is useful because adb will silently override the binding if an attempt
+ to bind to a port already used by adb was made, instead of throwing binding
+ error. So one should always check what ports adb is using before trying to
+ bind to a port with adb.
+
+ Returns:
+ A list of integers representing occupied host ports.
+ """
+ out = AdbProxy().forward("--list")
+ clean_lines = str(out, 'utf-8').strip().split('\n')
+ used_ports = []
+ for line in clean_lines:
+ tokens = line.split(" tcp:")
+ if len(tokens) != 3:
+ continue
+ used_ports.append(int(tokens[1]))
+ return used_ports
+
+class AdbProxy():
+ """Proxy class for ADB.
+
+ For syntactic reasons, the '-' in adb commands need to be replaced with
+ '_'. Can directly execute adb commands on an object:
+ >> adb = AdbProxy(<serial>)
+ >> adb.start_server()
+ >> adb.devices() # will return the console output of "adb devices".
+ """
+ def __init__(self, serial="", log=None):
+ self.serial = serial
+ if serial:
+ self.adb_str = "adb -s {}".format(serial)
+ else:
+ self.adb_str = "adb"
+ self.log = log
+
+ def _exec_cmd(self, cmd):
+ """Executes adb commands in a new shell.
+
+ This is specific to executing adb binary because stderr is not a good
+ indicator of cmd execution status.
+
+ Args:
+ cmds: A string that is the adb command to execute.
+
+ Returns:
+ The output of the adb command run if exit code is 0.
+
+ Raises:
+ AdbError is raised if the adb command exit code is not 0.
+ """
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
+ (out, err) = proc.communicate()
+ ret = proc.returncode
+ total_output = "stdout: {}, stderr: {}, ret: {}".format(out, err, ret)
+ # TODO(angli): Fix this when global logger is done.
+ if self.log:
+ self.log.debug("{}\n{}".format(cmd, total_output))
+ if ret == 0:
+ return out
+ else:
+ raise AdbError(total_output)
+
+ def _exec_adb_cmd(self, name, arg_str):
+ return self._exec_cmd(' '.join((self.adb_str, name, arg_str)))
+
+ def tcp_forward(self, host_port, device_port):
+ """Starts tcp forwarding.
+
+ Args:
+ host_port: Port number to use on the computer.
+ device_port: Port number to use on the android device.
+ """
+ self.forward("tcp:{} tcp:{}".format(host_port, device_port))
+
+ def start_sl4a(self, port=8080):
+ """Starts sl4a server on the android device.
+
+ Args:
+ port: Port number to use on the android device.
+ """
+ MAX_SL4A_WAIT_TIME = 10
+ print(self.shell(SL4A_LAUNCH_CMD.format(port)))
+
+ for _ in range(MAX_SL4A_WAIT_TIME):
+ time.sleep(1)
+ if self.is_sl4a_running():
+ return
+ raise AdbError(
+ "com.googlecode.android_scripting process never started.")
+
+ def is_sl4a_running(self):
+ """Checks if the sl4a app is running on an android device.
+
+ Returns:
+ True if the sl4a app is running, False otherwise.
+ """
+ #Grep for process with a preceding S which means it is truly started.
+ out = self.shell('ps | grep "S com.googlecode.android_scripting"')
+ if len(out)==0:
+ return False
+ return True
+
+ def __getattr__(self, name):
+ def adb_call(*args):
+ clean_name = name.replace('_', '-')
+ arg_str = ' '.join(str(elem) for elem in args)
+ return self._exec_adb_cmd(clean_name, arg_str)
+ return adb_call
diff --git a/utils/python/controllers/android.py b/utils/python/controllers/android.py
new file mode 100644
index 0000000..cdd1412
--- /dev/null
+++ b/utils/python/controllers/android.py
@@ -0,0 +1,126 @@
+#/usr/bin/env python3.4
+#
+# Copyright (C) 2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""
+JSON RPC interface to android scripting engine.
+"""
+
+from builtins import str
+
+import json
+import os
+import socket
+import threading
+import time
+
+HOST = os.environ.get('AP_HOST', None)
+PORT = os.environ.get('AP_PORT', 9999)
+
+class SL4AException(Exception):
+ pass
+
+class SL4AAPIError(SL4AException):
+ """Raised when remote API reports an error."""
+
+class SL4AProtocolError(SL4AException):
+ """Raised when there is some error in exchanging data with server on device."""
+ NO_RESPONSE_FROM_HANDSHAKE = "No response from handshake."
+ NO_RESPONSE_FROM_SERVER = "No response from server."
+ MISMATCHED_API_ID = "Mismatched API id."
+
+def IDCounter():
+ i = 0
+ while True:
+ yield i
+ i += 1
+
+class Android(object):
+ COUNTER = IDCounter()
+
+ _SOCKET_CONNECT_TIMEOUT = 60
+
+ def __init__(self, cmd='initiate', uid=-1, port=PORT, addr=HOST, timeout=None):
+ self.lock = threading.RLock()
+ self.client = None # prevent close errors on connect failure
+ self.uid = None
+ timeout_time = time.time() + self._SOCKET_CONNECT_TIMEOUT
+ while True:
+ try:
+ self.conn = socket.create_connection(
+ (addr, port), max(1,timeout_time - time.time()))
+ self.conn.settimeout(timeout)
+ break
+ except (TimeoutError, socket.timeout):
+ print("Failed to create socket connection!")
+ raise
+ except (socket.error, IOError):
+ # TODO: optimize to only forgive some errors here
+ # error values are OS-specific so this will require
+ # additional tuning to fail faster
+ if time.time() + 1 >= timeout_time:
+ print("Failed to create socket connection!")
+ raise
+ time.sleep(1)
+
+ self.client = self.conn.makefile(mode="brw")
+
+ resp = self._cmd(cmd, uid)
+ if not resp:
+ raise SL4AProtocolError(SL4AProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
+ result = json.loads(str(resp, encoding="utf8"))
+ if result['status']:
+ self.uid = result['uid']
+ else:
+ self.uid = -1
+
+ def close(self):
+ if self.conn is not None:
+ self.conn.close()
+ self.conn = None
+
+ def _cmd(self, command, uid=None):
+ if not uid:
+ uid = self.uid
+ self.client.write(
+ json.dumps({'cmd': command, 'uid': uid})
+ .encode("utf8")+b'\n')
+ self.client.flush()
+ return self.client.readline()
+
+ def _rpc(self, method, *args):
+ self.lock.acquire()
+ apiid = next(Android.COUNTER)
+ self.lock.release()
+ data = {'id': apiid,
+ 'method': method,
+ 'params': args}
+ request = json.dumps(data)
+ self.client.write(request.encode("utf8")+b'\n')
+ self.client.flush()
+ response = self.client.readline()
+ if not response:
+ raise SL4AProtocolError(SL4AProtocolError.NO_RESPONSE_FROM_SERVER)
+ result = json.loads(str(response, encoding="utf8"))
+ if result['error']:
+ raise SL4AAPIError(result['error'])
+ if result['id'] != apiid:
+ raise SL4AProtocolError(SL4AProtocolError.MISMATCHED_API_ID)
+ return result['result']
+
+ def __getattr__(self, name):
+ def rpc_call(*args):
+ return self._rpc(name, *args)
+ return rpc_call
diff --git a/utils/python/controllers/android_device.py b/utils/python/controllers/android_device.py
new file mode 100644
index 0000000..17ff80b
--- /dev/null
+++ b/utils/python/controllers/android_device.py
@@ -0,0 +1,731 @@
+#!/usr/bin/env python3.4
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from builtins import str
+from builtins import open
+
+import os
+import time
+import traceback
+
+from vts.runners.host import logger as vts_logger
+from vts.runners.host import signals
+from vts.runners.host import utils
+from vts.runners.utils.pythoncontrollers import adb
+from vts.runners.utils.pythoncontrollers import android
+from vts.runners.utils.pythoncontrollers import event_dispatcher
+from vts.runners.utils.pythoncontrollers import fastboot
+
+VTS_CONTROLLER_CONFIG_NAME = "AndroidDevice"
+VTS_CONTROLLER_REFERENCE_NAME = "android_devices"
+
+ANDROID_DEVICE_PICK_ALL_TOKEN = "*"
+# Key name for adb logcat extra params in config file.
+ANDROID_DEVICE_ADB_LOGCAT_PARAM_KEY = "adb_logcat_param"
+ANDROID_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
+ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
+
+class AndroidDeviceError(signals.ControllerError):
+ pass
+
+def create(configs, logger):
+ if not configs:
+ raise AndroidDeviceError(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
+ elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN:
+ ads = get_all_instances(logger=logger)
+ elif not isinstance(configs, list):
+ raise AndroidDeviceError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
+ elif isinstance(configs[0], str):
+ # Configs is a list of serials.
+ ads = get_instances(configs, logger)
+ else:
+ # Configs is a list of dicts.
+ ads = get_instances_with_configs(configs, logger)
+ connected_ads = list_adb_devices()
+ for ad in ads:
+ if ad.serial not in connected_ads:
+ raise AndroidDeviceError(("Android device %s is specified in config"
+ " but is not attached.") % ad.serial)
+ ad.startAdbLogcat()
+ try:
+ ad.getSl4aClient()
+ ad.ed.start()
+ except:
+ # This exception is logged here to help with debugging under py2,
+ # because "exception raised while processing another exception" is
+ # only printed under py3.
+ msg = "Failed to start sl4a on %s" % ad.serial
+ logger.exception(msg)
+ raise AndroidDeviceError(msg)
+ return ads
+
+def destroy(ads):
+ for ad in ads:
+ try:
+ ad.closeAllSl4aSession()
+ except:
+ pass
+ if ad.adb_logcat_process:
+ ad.stopAdbLogcat()
+
+def _parse_device_list(device_list_str, key):
+ """Parses a byte string representing a list of devices. The string is
+ generated by calling either adb or fastboot.
+
+ Args:
+ device_list_str: Output of adb or fastboot.
+ key: The token that signifies a device in device_list_str.
+
+ Returns:
+ A list of android device serial numbers.
+ """
+ clean_lines = str(device_list_str, 'utf-8').strip().split('\n')
+ results = []
+ for line in clean_lines:
+ tokens = line.strip().split('\t')
+ if len(tokens) == 2 and tokens[1] == key:
+ results.append(tokens[0])
+ return results
+
+def list_adb_devices():
+ """List all android devices connected to the computer that are detected by
+ adb.
+
+ Returns:
+ A list of android device serials. Empty if there's none.
+ """
+ out = adb.AdbProxy().devices()
+ return _parse_device_list(out, "device")
+
+def list_fastboot_devices():
+ """List all android devices connected to the computer that are in in
+ fastboot mode. These are detected by fastboot.
+
+ Returns:
+ A list of android device serials. Empty if there's none.
+ """
+ out = fastboot.FastbootProxy().devices()
+ return _parse_device_list(out, "fastboot")
+
+def get_instances(serials, logger=None):
+ """Create AndroidDevice instances from a list of serials.
+
+ Args:
+ serials: A list of android device serials.
+ logger: A logger to be passed to each instance.
+
+ Returns:
+ A list of AndroidDevice objects.
+ """
+ results = []
+ for s in serials:
+ results.append(AndroidDevice(s, logger=logger))
+ return results
+
+def get_instances_with_configs(configs, logger=None):
+ """Create AndroidDevice instances from a list of json configs.
+
+ Each config should have the required key-value pair "serial".
+
+ Args:
+ configs: A list of dicts each representing the configuration of one
+ android device.
+ logger: A logger to be passed to each instance.
+
+ Returns:
+ A list of AndroidDevice objects.
+ """
+ results = []
+ for c in configs:
+ try:
+ serial = c.pop("serial")
+ except KeyError:
+ raise AndroidDeviceError(('Required value "serial" is missing in '
+ 'AndroidDevice config %s.') % c)
+ ad = AndroidDevice(serial, logger=logger)
+ ad.loadConfig(c)
+ results.append(ad)
+ return results
+
+def get_all_instances(include_fastboot=False, logger=None):
+ """Create AndroidDevice instances for all attached android devices.
+
+ Args:
+ include_fastboot: Whether to include devices in bootloader mode or not.
+ logger: A logger to be passed to each instance.
+
+ Returns:
+ A list of AndroidDevice objects each representing an android device
+ attached to the computer.
+ """
+ if include_fastboot:
+ serial_list = list_adb_devices() + list_fastboot_devices()
+ return get_instances(serial_list, logger=logger)
+ return get_instances(list_adb_devices(), logger=logger)
+
+def filter_devices(ads, func):
+ """Finds the AndroidDevice instances from a list that match certain
+ conditions.
+
+ Args:
+ ads: A list of AndroidDevice instances.
+ func: A function that takes an AndroidDevice object and returns True
+ if the device satisfies the filter condition.
+
+ Returns:
+ A list of AndroidDevice instances that satisfy the filter condition.
+ """
+ results = []
+ for ad in ads:
+ if func(ad):
+ results.append(ad)
+ return results
+
+def get_device(ads, **kwargs):
+ """Finds a unique AndroidDevice instance from a list that has specific
+ attributes of certain values.
+
+ Example:
+ get_device(android_devices, label="foo", phone_number="1234567890")
+ get_device(android_devices, model="angler")
+
+ Args:
+ ads: A list of AndroidDevice instances.
+ kwargs: keyword arguments used to filter AndroidDevice instances.
+
+ Returns:
+ The target AndroidDevice instance.
+
+ Raises:
+ AndroidDeviceError is raised if none or more than one device is
+ matched.
+ """
+ def _get_device_filter(ad):
+ for k, v in kwargs.items():
+ if not hasattr(ad, k):
+ return False
+ elif getattr(ad, k) != v:
+ return False
+ return True
+ filtered = filter_devices(ads, _get_device_filter)
+ if not filtered:
+ raise AndroidDeviceError(("Could not find a target device that matches"
+ " condition: %s.") % kwargs)
+ elif len(filtered) == 1:
+ return filtered[0]
+ else:
+ serials = [ad.serial for ad in filtered]
+ raise AndroidDeviceError("More than one device matched: %s" % serials)
+
+def takeBugReports(ads, test_name, begin_time):
+ """Takes bug reports on a list of android devices.
+
+ If you want to take a bug report, call this function with a list of
+ android_device objects in on_fail. But reports will be taken on all the
+ devices in the list concurrently. Bug report takes a relative long
+ time to take, so use this cautiously.
+
+ Args:
+ ads: A list of AndroidDevice instances.
+ test_name: Name of the test case that triggered this bug report.
+ begin_time: Logline format timestamp taken when the test started.
+ """
+ begin_time = vts_logger.normalizeLogLineTimestamp(begin_time)
+ def take_br(test_name, begin_time, ad):
+ ad.takeBugReport(test_name, begin_time)
+ args = [(test_name, begin_time, ad) for ad in ads]
+ utils.concurrent_exec(take_br, args)
+
+class AndroidDevice:
+ """Class representing an android device.
+
+ Each object of this class represents one Android device in ACTS, including
+ handles to adb, fastboot, and sl4a clients. In addition to direct adb
+ commands, this object also uses adb port forwarding to talk to the Android
+ device.
+
+ Attributes:
+ serial: A string that's the serial number of the Androi device.
+ h_port: An integer that's the port number for adb port forwarding used
+ on the computer the Android device is connected
+ d_port: An integer that's the port number used on the Android device
+ for adb port forwarding.
+ log: A LoggerProxy object used for the class's internal logging.
+ log_path: A string that is the path where all logs collected on this
+ android device should be stored.
+ adb_logcat_process: A process that collects the adb logcat.
+ adb_logcat_file_path: A string that's the full path to the adb logcat
+ file collected, if any.
+ adb: An AdbProxy object used for interacting with the device via adb.
+ fastboot: A FastbootProxy object used for interacting with the device
+ via fastboot.
+ """
+
+ def __init__(self, serial="", host_port=None, device_port=8080,
+ logger=None):
+ self.serial = serial
+ self.h_port = host_port
+ self.d_port = device_port
+ self.log = logging.getLogger()
+ lp = self.log.log_path
+ self.log_path = os.path.join(lp, "AndroidDevice%s" % serial)
+ self._droid_sessions = {}
+ self._event_dispatchers = {}
+ self.adb_logcat_process = None
+ self.adb_logcat_file_path = None
+ self.adb = adb.AdbProxy(serial)
+ self.fastboot = fastboot.FastbootProxy(serial)
+ if not self.isBootloaderMode:
+ self.rootAdb()
+
+ def __del__(self):
+ if self.h_port:
+ self.adb.forward("--remove tcp:%d" % self.h_port)
+ if self.adb_logcat_process:
+ self.stopAdbLogcat()
+
+ @property
+ def isBootloaderMode(self):
+ """True if the device is in bootloader mode.
+ """
+ return self.serial in list_fastboot_devices()
+
+ @property
+ def isAdbRoot(self):
+ """True if adb is running as root for this device.
+ """
+ return "root" in self.adb.shell("id -u").decode("utf-8")
+
+ @property
+ def model(self):
+ """The Android code name for the device.
+ """
+ # If device is in bootloader mode, get mode name from fastboot.
+ if self.isBootloaderMode:
+ out = self.fastboot.getvar("product").strip()
+ # "out" is never empty because of the "total time" message fastboot
+ # writes to stderr.
+ lines = out.decode("utf-8").split('\n', 1)
+ if lines:
+ tokens = lines[0].split(' ')
+ if len(tokens) > 1:
+ return tokens[1].lower()
+ return None
+ out = self.adb.shell('getprop | grep ro.build.product')
+ model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
+ if model == "sprout":
+ return model
+ else:
+ out = self.adb.shell('getprop | grep ro.product.name')
+ model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
+ return model
+
+ @property
+ def droid(self):
+ """The first sl4a session initiated on this device. None if there isn't
+ one.
+ """
+ try:
+ session_id = sorted(self._droid_sessions)[0]
+ return self._droid_sessions[session_id][0]
+ except IndexError:
+ return None
+
+ @property
+ def ed(self):
+ """The first event_dispatcher instance created on this device. None if
+ there isn't one.
+ """
+ try:
+ session_id = sorted(self._event_dispatchers)[0]
+ return self._event_dispatchers[session_id]
+ except IndexError:
+ return None
+
+ @property
+ def droids(self):
+ """A list of the active sl4a sessions on this device.
+
+ If multiple connections exist for the same session, only one connection
+ is listed.
+ """
+ keys = sorted(self._droid_sessions)
+ results = []
+ for k in keys:
+ results.append(self._droid_sessions[k][0])
+ return results
+
+ @property
+ def eds(self):
+ """A list of the event_dispatcher objects on this device.
+
+ The indexing of the list matches that of the droids property.
+ """
+ keys = sorted(self._event_dispatchers)
+ results = []
+ for k in keys:
+ results.append(self._event_dispatchers[k])
+ return results
+
+ @property
+ def isAdbLogcatOn(self):
+ """Whether there is an ongoing adb logcat collection.
+ """
+ if self.adb_logcat_process:
+ return True
+ return False
+
+ def loadConfig(self, config):
+ """Add attributes to the AndroidDevice object based on json config.
+
+ Args:
+ config: A dictionary representing the configs.
+
+ Raises:
+ AndroidDeviceError is raised if the config is trying to overwrite
+ an existing attribute.
+ """
+ for k, v in config.items():
+ if hasattr(self, k):
+ raise AndroidDeviceError(("Attempting to set existing "
+ "attribute %s on %s") % (k, self.serial))
+ setattr(self, k, v)
+
+ def rootAdb(self):
+ """Change adb to root mode for this device.
+ """
+ if not self.isAdbRoot:
+ self.adb.root()
+ self.adb.wait_for_device()
+ self.adb.remount()
+ self.adb.wait_for_device()
+
+ def getSl4aClient(self, handle_event=True):
+ """Create an sl4a connection to the device.
+
+ Return the connection handler 'droid'. By default, another connection
+ on the same session is made for EventDispatcher, and the dispatcher is
+ returned to the caller as well.
+ If sl4a server is not started on the device, try to start it.
+
+ Args:
+ handle_event: True if this droid session will need to handle
+ events.
+
+ Returns:
+ droid: Android object used to communicate with sl4a on the android
+ device.
+ ed: An optional EventDispatcher to organize events for this droid.
+
+ Examples:
+ Don't need event handling:
+ >>> ad = AndroidDevice()
+ >>> droid = ad.getSl4aClient(False)
+
+ Need event handling:
+ >>> ad = AndroidDevice()
+ >>> droid, ed = ad.getSl4aClient()
+ """
+ if not self.h_port or not adb.is_port_available(self.h_port):
+ self.h_port = adb.get_available_host_port()
+ self.adb.tcp_forward(self.h_port, self.d_port)
+ try:
+ droid = self.start_new_session()
+ except:
+ self.adb.start_sl4a()
+ droid = self.start_new_session()
+ if handle_event:
+ ed = self.getSl4aEventDispatcher(droid)
+ return droid, ed
+ return droid
+
+ def getSl4aEventDispatcher(self, droid):
+ """Return an EventDispatcher for an sl4a session
+
+ Args:
+ droid: Session to create EventDispatcher for.
+
+ Returns:
+ ed: An EventDispatcher for specified session.
+ """
+ ed_key = self.serial + str(droid.uid)
+ if ed_key in self._event_dispatchers:
+ if self._event_dispatchers[ed_key] is None:
+ raise AndroidDeviceError("EventDispatcher Key Empty")
+ self.log.debug("Returning existing key %s for event dispatcher!",
+ ed_key)
+ return self._event_dispatchers[ed_key]
+ event_droid = self.add_new_connection_to_session(droid.uid)
+ ed = event_dispatcher.EventDispatcher(event_droid)
+ self._event_dispatchers[ed_key] = ed
+ return ed
+
+ def _is_timestamp_in_range(self, target, begin_time, end_time):
+ low = vts_logger.logLineTimestampComparator(begin_time, target) <= 0
+ high = vts_logger.logLineTimestampComparator(end_time, target) >= 0
+ return low and high
+
+ def takeAdbLogExcerpt(self, tag, begin_time):
+ """Takes an excerpt of the adb logcat log from a certain time point to
+ current time.
+
+ Args:
+ tag: An identifier of the time period, usualy the name of a test.
+ begin_time: Logline format timestamp of the beginning of the time
+ period.
+ """
+ if not self.adb_logcat_file_path:
+ raise AndroidDeviceError(("Attempting to cat adb log when none has"
+ " been collected on Android device %s."
+ ) % self.serial)
+ end_time = vts_logger.getLogLineTimestamp()
+ self.log.debug("Extracting adb log from logcat.")
+ adb_excerpt_path = os.path.join(self.log_path, "AdbLogExcerpts")
+ utils.create_dir(adb_excerpt_path)
+ f_name = os.path.basename(self.adb_logcat_file_path)
+ out_name = f_name.replace("adblog,", "").replace(".txt", "")
+ out_name = ",{},{}.txt".format(begin_time, out_name)
+ tag_len = utils.MAX_FILENAME_LEN - len(out_name)
+ tag = tag[:tag_len]
+ out_name = tag + out_name
+ full_adblog_path = os.path.join(adb_excerpt_path, out_name)
+ with open(full_adblog_path, 'w', encoding='utf-8') as out:
+ in_file = self.adb_logcat_file_path
+ with open(in_file, 'r', encoding='utf-8', errors='replace') as f:
+ in_range = False
+ while True:
+ line = None
+ try:
+ line = f.readline()
+ if not line:
+ break
+ except:
+ continue
+ line_time = line[:vts_logger.log_line_timestamp_len]
+ if not vts_logger.isValidLogLineTimestamp(line_time):
+ continue
+ if self._is_timestamp_in_range(line_time, begin_time,
+ end_time):
+ in_range = True
+ if not line.endswith('\n'):
+ line += '\n'
+ out.write(line)
+ else:
+ if in_range:
+ break
+
+ def startAdbLogcat(self):
+ """Starts a standing adb logcat collection in separate subprocesses and
+ save the logcat in a file.
+ """
+ if self.isAdbLogcatOn:
+ raise AndroidDeviceError(("Android device {} already has an adb "
+ "logcat thread going on. Cannot start "
+ "another one.").format(self.serial))
+ # Disable adb log spam filter.
+ self.adb.shell("logpersist.start")
+ f_name = "adblog,{},{}.txt".format(self.model, self.serial)
+ utils.create_dir(self.log_path)
+ logcat_file_path = os.path.join(self.log_path, f_name)
+ try:
+ extra_params = self.adb_logcat_param
+ except AttributeError:
+ extra_params = "-b all"
+ cmd = "adb -s {} logcat -v threadtime {} >> {}".format(
+ self.serial, extra_params, logcat_file_path)
+ self.adb_logcat_process = utils.start_standing_subprocess(cmd)
+ self.adb_logcat_file_path = logcat_file_path
+
+ def stopAdbLogcat(self):
+ """Stops the adb logcat collection subprocess.
+ """
+ if not self.isAdbLogcatOn:
+ raise AndroidDeviceError(("Android device {} does not have an "
+ "ongoing adb logcat collection."
+ ).format(self.serial))
+ utils.stop_standing_subprocess(self.adb_logcat_process)
+ self.adb_logcat_process = None
+
+ def takeBugReport(self, test_name, begin_time):
+ """Takes a bug report on the device and stores it in a file.
+
+ Args:
+ test_name: Name of the test case that triggered this bug report.
+ begin_time: Logline format timestamp taken when the test started.
+ """
+ br_path = os.path.join(self.log_path, "BugReports")
+ utils.create_dir(br_path)
+ base_name = ",{},{}.txt".format(begin_time, self.serial)
+ test_name_len = utils.MAX_FILENAME_LEN - len(base_name)
+ out_name = test_name[:test_name_len] + base_name
+ full_out_path = os.path.join(br_path, out_name.replace(' ', '\ '))
+ self.log.info("Taking bugreport for %s on %s", test_name, self.serial)
+ self.adb.bugreport(" > {}".format(full_out_path))
+ self.log.info("Bugreport for %s taken at %s", test_name, full_out_path)
+
+ def start_new_session(self):
+ """Start a new session in sl4a.
+
+ Also caches the droid in a dict with its uid being the key.
+
+ Returns:
+ An Android object used to communicate with sl4a on the android
+ device.
+
+ Raises:
+ SL4AException: Something is wrong with sl4a and it returned an
+ existing uid to a new session.
+ """
+ droid = android.Android(port=self.h_port)
+ if droid.uid in self._droid_sessions:
+ raise android.SL4AException(("SL4A returned an existing uid for a "
+ "new session. Abort."))
+ self._droid_sessions[droid.uid] = [droid]
+ return droid
+
+ def add_new_connection_to_session(self, session_id):
+ """Create a new connection to an existing sl4a session.
+
+ Args:
+ session_id: UID of the sl4a session to add connection to.
+
+ Returns:
+ An Android object used to communicate with sl4a on the android
+ device.
+
+ Raises:
+ AndroidDeviceError: Raised if the session it's trying to connect to
+ does not exist.
+ """
+ if session_id not in self._droid_sessions:
+ raise AndroidDeviceError("Session %d doesn't exist." % session_id)
+ droid = android.Android(cmd='continue', uid=session_id,
+ port=self.h_port)
+ return droid
+
+ def closeOneSl4aSession(self, session_id):
+ """Terminate a session in sl4a.
+
+ Send terminate signal to sl4a server; stop dispatcher associated with
+ the session. Clear corresponding droids and dispatchers from cache.
+
+ Args:
+ session_id: UID of the sl4a session to terminate.
+ """
+ if self._droid_sessions and (session_id in self._droid_sessions):
+ for droid in self._droid_sessions[session_id]:
+ droid.closeSl4aSession()
+ droid.close()
+ del self._droid_sessions[session_id]
+ ed_key = self.serial + str(session_id)
+ if ed_key in self._event_dispatchers:
+ self._event_dispatchers[ed_key].clean_up()
+ del self._event_dispatchers[ed_key]
+
+ def closeAllSl4aSession(self):
+ """Terminate all sl4a sessions on the AndroidDevice instance.
+
+ Terminate all sessions and clear caches.
+ """
+ if self._droid_sessions:
+ session_ids = list(self._droid_sessions.keys())
+ for session_id in session_ids:
+ try:
+ self.closeOneSl4aSession(session_id)
+ except:
+ msg = "Failed to terminate session %d." % session_id
+ self.log.exception(msg)
+ self.log.error(traceback.format_exc())
+ if self.h_port:
+ self.adb.forward("--remove tcp:%d" % self.h_port)
+ self.h_port = None
+
+ def runIperfClient(self, server_host, extra_args=""):
+ """Start iperf client on the device.
+
+ Return status as true if iperf client start successfully.
+ And data flow information as results.
+
+ Args:
+ server_host: Address of the iperf server.
+ extra_args: A string representing extra arguments for iperf client,
+ e.g. "-i 1 -t 30".
+
+ Returns:
+ status: true if iperf client start successfully.
+ results: results have data flow information
+ """
+ out = self.adb.shell("iperf3 -c {} {}".format(server_host, extra_args))
+ clean_out = str(out,'utf-8').strip().split('\n')
+ if "error" in clean_out[0].lower():
+ return False, clean_out
+ return True, clean_out
+
+ @utils.timeout(15 * 60)
+ def waitForBootCompletion(self):
+ """Waits for Android framework to broadcast ACTION_BOOT_COMPLETED.
+
+ This function times out after 15 minutes.
+ """
+ self.adb.wait_for_device()
+ while True:
+ try:
+ out = self.adb.shell("getprop sys.boot_completed")
+ completed = out.decode('utf-8').strip()
+ if completed == '1':
+ return
+ except adb.AdbError:
+ # adb shell calls may fail during certain period of booting
+ # process, which is normal. Ignoring these errors.
+ pass
+ time.sleep(5)
+
+ def reboot(self):
+ """Reboots the device.
+
+ Terminate all sl4a sessions, reboot the device, wait for device to
+ complete booting, and restart an sl4a session.
+
+ This is a blocking method.
+
+ This is probably going to print some error messages in console. Only
+ use if there's no other option.
+
+ Example:
+ droid, ed = ad.reboot()
+
+ Returns:
+ An sl4a session with an event_dispatcher.
+
+ Raises:
+ AndroidDeviceError is raised if waiting for completion timed
+ out.
+ """
+ if self.isBootloaderMode:
+ self.fastboot.reboot()
+ return
+ has_adb_log = self.isAdbLogcatOn
+ if has_adb_log:
+ self.stopAdbLogcat()
+ self.closeAllSl4aSession()
+ self.adb.reboot()
+ self.waitForBootCompletion()
+ self.rootAdb()
+ droid, ed = self.getSl4aClient()
+ ed.start()
+ if has_adb_log:
+ self.startAdbLogcat()
+ return droid, ed
diff --git a/utils/python/controllers/attenuator.py b/utils/python/controllers/attenuator.py
new file mode 100644
index 0000000..7dd14f2
--- /dev/null
+++ b/utils/python/controllers/attenuator.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python3.4
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib
+
+from vts.runners.host.keys import Config
+
+VTS_CONTROLLER_CONFIG_NAME = "Attenuator"
+VTS_CONTROLLER_REFERENCE_NAME = "attenuators"
+
+def create(configs, logger):
+ objs = []
+ for c in configs:
+ attn_model = c["Model"]
+ # Default to telnet.
+ protocol = "telnet"
+ if "Protocol" in c:
+ protocol = c["Protocol"]
+ module_name = "vts.utils.python.controllers.attenuator_lib.%s.%s" % (attn_model,
+ protocol)
+ module = importlib.import_module(module_name)
+ inst_cnt = c["InstrumentCount"]
+ attn_inst = module.AttenuatorInstrument(inst_cnt)
+ attn_inst.model = attn_model
+ insts = attn_inst.open(c[Config.key_address.value],
+ c[Config.key_port.value])
+ for i in range(inst_cnt):
+ attn = Attenuator(attn_inst, idx=i)
+ if "Paths" in c:
+ try:
+ setattr(attn, "path", c["Paths"][i])
+ except IndexError:
+ logger.error("No path specified for attenuator %d." % i)
+ raise
+ objs.append(attn)
+ return objs
+
+def destroy(objs):
+ return
+
+r"""
+Base classes which define how attenuators should be accessed, managed, and manipulated.
+
+Users will instantiate a specific child class, but almost all operation should be performed
+on the methods and data members defined here in the base classes or the wrapper classes.
+"""
+
+
+class AttenuatorError(Exception):
+ r"""This is the Exception class defined for all errors generated by Attenuator-related modules.
+ """
+ pass
+
+
+class InvalidDataError(AttenuatorError):
+ r"""This exception is thrown when an unexpected result is seen on the transport layer below
+ the module.
+
+ When this exception is seen, closing an re-opening the link to the attenuator instrument is
+ probably necessary. Something has gone wrong in the transport.
+ """
+ pass
+
+
+class InvalidOperationError(AttenuatorError):
+ r"""Certain methods may only be accessed when the instance upon which they are invoked is in
+ a certain state. This indicates that the object is not in the correct state for a method to be
+ called.
+ """
+ pass
+
+
+class AttenuatorInstrument():
+ r"""This is a base class that defines the primitive behavior of all attenuator
+ instruments.
+
+ The AttenuatorInstrument class is designed to provide a simple low-level interface for
+ accessing any step attenuator instrument comprised of one or more attenuators and a
+ controller. All AttenuatorInstruments should override all the methods below and call
+ AttenuatorInstrument.__init__ in their constructors. Outside of setup/teardown,
+ devices should be accessed via this generic "interface".
+ """
+ model = None
+ INVALID_MAX_ATTEN = 999.9
+
+ def __init__(self, num_atten=0):
+ r"""This is the Constructor for Attenuator Instrument.
+
+ Parameters
+ ----------
+ num_atten : This optional parameter is the number of attenuators contained within the
+ instrument. In some instances setting this number to zero will allow the driver to
+ auto-determine, the number of attenuators; however, this behavior is not guaranteed.
+
+ Raises
+ ------
+ NotImplementedError
+ This constructor should never be called directly. It may only be called by a child.
+
+ Returns
+ -------
+ self
+ Returns a newly constructed AttenuatorInstrument
+ """
+
+ if type(self) is AttenuatorInstrument:
+ raise NotImplementedError("Base class should not be instantiated directly!")
+
+ self.num_atten = num_atten
+ self.max_atten = AttenuatorInstrument.INVALID_MAX_ATTEN
+ self.properties = None
+
+ def set_atten(self, idx, value):
+ r"""This function sets the attenuation of an attenuator given its index in the instrument.
+
+ Parameters
+ ----------
+ idx : This zero-based index is the identifier for a particular attenuator in an
+ instrument.
+ value : This is a floating point value for nominal attenuation to be set.
+
+ Raises
+ ------
+ NotImplementedError
+ This constructor should never be called directly. It may only be called by a child.
+ """
+ raise NotImplementedError("Base class should not be called directly!")
+
+ def get_atten(self, idx):
+ r"""This function returns the current attenuation from an attenuator at a given index in
+ the instrument.
+
+ Parameters
+ ----------
+ idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+
+ Raises
+ ------
+ NotImplementedError
+ This constructor should never be called directly. It may only be called by a child.
+
+ Returns
+ -------
+ float
+ Returns a the current attenuation value
+ """
+ raise NotImplementedError("Base class should not be called directly!")
+
+
+class Attenuator():
+ r"""This class defines an object representing a single attenuator in a remote instrument.
+
+ A user wishing to abstract the mapping of attenuators to physical instruments should use this
+ class, which provides an object that obscures the physical implementation an allows the user
+ to think only of attenuators regardless of their location.
+ """
+
+ def __init__(self, instrument, idx=0, offset=0):
+ r"""This is the constructor for Attenuator
+
+ Parameters
+ ----------
+ instrument : Reference to an AttenuatorInstrument on which the Attenuator resides
+ idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+ offset : A power offset value for the attenuator to be used when performing future
+ operations. This could be used for either calibration or to allow group operations with
+ offsets between various attenuators.
+
+ Raises
+ ------
+ TypeError
+ Requires a valid AttenuatorInstrument to be passed in.
+ IndexError
+ The index of the attenuator in the AttenuatorInstrument must be within the valid range.
+
+ Returns
+ -------
+ self
+ Returns a newly constructed Attenuator
+ """
+ if not isinstance(instrument, AttenuatorInstrument):
+ raise TypeError("Must provide an Attenuator Instrument Ref")
+ self.model = instrument.model
+ self.instrument = instrument
+ self.idx = idx
+ self.offset = offset
+
+ if(self.idx >= instrument.num_atten):
+ raise IndexError("Attenuator index out of range for attenuator instrument")
+
+ def set_atten(self, value):
+ r"""This function sets the attenuation of Attenuator.
+
+ Parameters
+ ----------
+ value : This is a floating point value for nominal attenuation to be set.
+
+ Raises
+ ------
+ ValueError
+ The requested set value+offset must be less than the maximum value.
+ """
+
+ if value+self.offset > self.instrument.max_atten:
+ raise ValueError("Attenuator Value+Offset greater than Max Attenuation!")
+
+ self.instrument.set_atten(self.idx, value+self.offset)
+
+ def get_atten(self):
+ r"""This function returns the current attenuation setting of Attenuator, normalized by
+ the set offset.
+
+ Returns
+ -------
+ float
+ Returns a the current attenuation value
+ """
+
+ return self.instrument.get_atten(self.idx) - self.offset
+
+ def get_max_atten(self):
+ r"""This function returns the max attenuation setting of Attenuator, normalized by
+ the set offset.
+
+ Returns
+ -------
+ float
+ Returns a the max attenuation value
+ """
+ if (self.instrument.max_atten == AttenuatorInstrument.INVALID_MAX_ATTEN):
+ raise ValueError("Invalid Max Attenuator Value")
+
+ return self.instrument.max_atten - self.offset
+
+
+class AttenuatorGroup(object):
+ r"""This is a handy abstraction for groups of attenuators that will share behavior.
+
+ Attenuator groups are intended to further facilitate abstraction of testing functions from
+ the physical objects underlying them. By adding attenuators to a group, it is possible to
+ operate on functional groups that can be thought of in a common manner in the test. This
+ class is intended to provide convenience to the user and avoid re-implementation of helper
+ functions and small loops scattered throughout user code.
+
+ """
+
+ def __init__(self, name=""):
+ r"""This is the constructor for AttenuatorGroup
+
+ Parameters
+ ----------
+ name : The name is an optional parameter intended to further facilitate the passing of
+ easily tracked groups of attenuators throughout code. It is left to the user to use the
+ name in a way that meets their needs.
+
+ Returns
+ -------
+ self
+ Returns a newly constructed AttenuatorGroup
+ """
+ self.name = name
+ self.attens = []
+ self._value = 0
+
+ def add_from_instrument(self, instrument, indices):
+ r"""This function provides a way to create groups directly from the Attenuator Instrument.
+
+ This function will create Attenuator objects for all of the indices passed in and add
+ them to the group.
+
+ Parameters
+ ----------
+ instrument : A ref to the instrument from which attenuators will be added
+ indices : You pay pass in the indices either as a range, a list, or a single integer.
+
+ Raises
+ ------
+ TypeError
+ Requires a valid AttenuatorInstrument to be passed in.
+ """
+
+ if not instrument or not isinstance(instrument, AttenuatorInstrument):
+ raise TypeError("Must provide an Attenuator Instrument Ref")
+
+ if type(indices) is range or type(indices) is list:
+ for i in indices:
+ self.attens.append(Attenuator(instrument, i))
+ elif type(indices) is int:
+ self.attens.append(Attenuator(instrument, indices))
+
+ def add(self, attenuator):
+ r"""This function adds an already constructed Attenuator object to the AttenuatorGroup.
+
+ Parameters
+ ----------
+ attenuator : An Attenuator object.
+
+ Raises
+ ------
+ TypeError
+ Requires a valid Attenuator to be passed in.
+ """
+
+ if not isinstance(attenuator, Attenuator):
+ raise TypeError("Must provide an Attenuator")
+
+ self.attens.append(attenuator)
+
+ def synchronize(self):
+ r"""This function can be called to ensure all Attenuators within a group are set
+ appropriately.
+ """
+
+ self.set_atten(self._value)
+
+ def is_synchronized(self):
+ r"""This function queries all the Attenuators in the group to determine whether or not
+ they are synchronized.
+
+ Returns
+ -------
+ bool
+ True if the attenuators are synchronized.
+ """
+
+ for att in self.attens:
+ if att.get_atten() != self._value:
+ return False
+ return True
+
+ def set_atten(self, value):
+ r"""This function sets the attenuation value of all attenuators in the group.
+
+ Parameters
+ ----------
+ value : This is a floating point value for nominal attenuation to be set.
+
+ Returns
+ -------
+ bool
+ True if the attenuators are synchronized.
+ """
+
+ value = float(value)
+ for att in self.attens:
+ att.set_atten(value)
+ self._value = value
+
+ def get_atten(self):
+ r"""This function returns the current attenuation setting of AttenuatorGroup.
+
+ This returns a cached value that assumes the attenuators are synchronized. It avoids a
+ relatively expensive call for a common operation, and trusts the user to ensure
+ synchronization.
+
+ Returns
+ -------
+ float
+ Returns a the current attenuation value for the group, which is independent of any
+ individual attenuator offsets.
+ """
+
+ return float(self._value)
diff --git a/utils/python/controllers/attenuator_lib/__init__.py b/utils/python/controllers/attenuator_lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/__init__.py
diff --git a/utils/python/controllers/attenuator_lib/_tnhelper.py b/utils/python/controllers/attenuator_lib/_tnhelper.py
new file mode 100644
index 0000000..f1a35c9
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/_tnhelper.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3.4
+
+# Copyright 2016- The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Helper module for common telnet capability to communicate with AttenuatorInstrument(s).
+
+User code shouldn't need to directly access this class.
+"""
+
+
+import telnetlib
+from vts.utils.python.controllers import attenuator
+
+
+def _ascii_string(uc_string):
+ return str(uc_string).encode('ASCII')
+
+
+class _TNHelper():
+ #This is an internal helper class for Telnet+SCPI command-based instruments.
+ #It should only be used by those implemention control libraries and not by any user code
+ # directly
+
+ def __init__(self, tx_cmd_separator="\n", rx_cmd_separator="\n", prompt=""):
+ self._tn = None
+
+ self.tx_cmd_separator = tx_cmd_separator
+ self.rx_cmd_separator = rx_cmd_separator
+ self.prompt = prompt
+
+ def open(self, host, port=23):
+ if self._tn:
+ self._tn.close()
+
+ self._tn = telnetlib.Telnet()
+ self._tn.open(host, port, 10)
+
+ def is_open(self):
+ return bool(self._tn)
+
+ def close(self):
+ if self._tn:
+ self._tn.close()
+ self._tn = None
+
+ def cmd(self, cmd_str, wait_ret=True):
+ if not isinstance(cmd_str, str):
+ raise TypeError("Invalid command string", cmd_str)
+
+ if not self.is_open():
+ raise attenuator.InvalidOperationError("Telnet connection not open for commands")
+
+ cmd_str.strip(self.tx_cmd_separator)
+ self._tn.read_until(_ascii_string(self.prompt), 2)
+ self._tn.write(_ascii_string(cmd_str+self.tx_cmd_separator))
+
+ if wait_ret is False:
+ return None
+
+ match_idx, match_val, ret_text = \
+ self._tn.expect([_ascii_string("\S+"+self.rx_cmd_separator)], 1)
+
+ if match_idx == -1:
+ raise attenuator.InvalidDataError("Telnet command failed to return valid data")
+
+ ret_text = ret_text.decode()
+ ret_text = ret_text.strip(self.tx_cmd_separator + self.rx_cmd_separator + self.prompt)
+
+ return ret_text
diff --git a/utils/python/controllers/attenuator_lib/aeroflex/__init__.py b/utils/python/controllers/attenuator_lib/aeroflex/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/aeroflex/__init__.py
diff --git a/utils/python/controllers/attenuator_lib/aeroflex/telnet.py b/utils/python/controllers/attenuator_lib/aeroflex/telnet.py
new file mode 100644
index 0000000..4ce5612
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/aeroflex/telnet.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3.4
+
+# Copyright 2016- The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Class for Telnet control of Aeroflex 832X and 833X Series Attenuator Modules
+
+This class provides a wrapper to the Aeroflex attenuator modules for purposes
+of simplifying and abstracting control down to the basic necessities. It is
+not the intention of the module to expose all functionality, but to allow
+interchangeable HW to be used.
+
+See http://www.aeroflex.com/ams/weinschel/PDFILES/IM-608-Models-8320-&-8321-preliminary.pdf
+"""
+
+
+from vts.utils.python.controllers import attenuator
+from vts.utils.python.controllers.attenuator_lib import _tnhelper
+
+
+class AttenuatorInstrument(attenuator.AttenuatorInstrument):
+
+ def __init__(self, num_atten=0):
+ super().__init__(num_atten)
+
+ self._tnhelper = _tnhelper._TNHelper(tx_cmd_separator="\r\n",
+ rx_cmd_separator="\r\n",
+ prompt=">")
+ self.properties = None
+
+ def open(self, host, port=23):
+ r"""Opens a telnet connection to the desired AttenuatorInstrument and queries basic
+ information.
+
+ Parameters
+ ----------
+ host : A valid hostname (IP address or DNS-resolvable name) to an MC-DAT attenuator
+ instrument.
+ port : An optional port number (defaults to telnet default 23)
+ """
+ self._tnhelper.open(host, port)
+
+ # work around a bug in IO, but this is a good thing to do anyway
+ self._tnhelper.cmd("*CLS", False)
+
+ if self.num_atten == 0:
+ self.num_atten = int(self._tnhelper.cmd("RFCONFIG? CHAN"))
+
+ configstr = self._tnhelper.cmd("RFCONFIG? ATTN 1")
+
+ self.properties = dict(zip(['model', 'max_atten', 'min_step',
+ 'unknown', 'unknown2', 'cfg_str'],
+ configstr.split(", ", 5)))
+
+ self.max_atten = float(self.properties['max_atten'])
+
+ def is_open(self):
+ r"""This function returns the state of the telnet connection to the underlying
+ AttenuatorInstrument.
+
+ Returns
+ -------
+ Bool
+ True if there is a successfully open connection to the AttenuatorInstrument
+ """
+
+ return bool(self._tnhelper.is_open())
+
+ def close(self):
+ r"""Closes a telnet connection to the desired AttenuatorInstrument.
+
+ This should be called as part of any teardown procedure prior to the attenuator
+ instrument leaving scope.
+ """
+
+ self._tnhelper.close()
+
+ def set_atten(self, idx, value):
+ r"""This function sets the attenuation of an attenuator given its index in the instrument.
+
+ Parameters
+ ----------
+ idx : This zero-based index is the identifier for a particular attenuator in an
+ instrument.
+ value : This is a floating point value for nominal attenuation to be set.
+
+ Raises
+ ------
+ InvalidOperationError
+ This error occurs if the underlying telnet connection to the instrument is not open.
+ IndexError
+ If the index of the attenuator is greater than the maximum index of the underlying
+ instrument, this error will be thrown. Do not count on this check programmatically.
+ ValueError
+ If the requested set value is greater than the maximum attenuation value, this error
+ will be thrown. Do not count on this check programmatically.
+ """
+
+
+ if not self.is_open():
+ raise attenuator.InvalidOperationError("Connection not open!")
+
+ if idx >= self.num_atten:
+ raise IndexError("Attenuator index out of range!", self.num_atten, idx)
+
+ if value > self.max_atten:
+ raise ValueError("Attenuator value out of range!", self.max_atten, value)
+
+ self._tnhelper.cmd("ATTN " + str(idx+1) + " " + str(value), False)
+
+ def get_atten(self, idx):
+ r"""This function returns the current attenuation from an attenuator at a given index in
+ the instrument.
+
+ Parameters
+ ----------
+ idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+
+ Raises
+ ------
+ InvalidOperationError
+ This error occurs if the underlying telnet connection to the instrument is not open.
+
+ Returns
+ -------
+ float
+ Returns a the current attenuation value
+ """
+ if not self.is_open():
+ raise attenuator.InvalidOperationError("Connection not open!")
+
+# Potentially redundant safety check removed for the moment
+# if idx >= self.num_atten:
+# raise IndexError("Attenuator index out of range!", self.num_atten, idx)
+
+ atten_val = self._tnhelper.cmd("ATTN? " + str(idx+1))
+
+ return float(atten_val)
diff --git a/utils/python/controllers/attenuator_lib/minicircuits/__init__.py b/utils/python/controllers/attenuator_lib/minicircuits/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/minicircuits/__init__.py
diff --git a/utils/python/controllers/attenuator_lib/minicircuits/telnet.py b/utils/python/controllers/attenuator_lib/minicircuits/telnet.py
new file mode 100644
index 0000000..1bcb98b
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/minicircuits/telnet.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3.4
+
+# Copyright 2016- The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Class for Telnet control of Mini-Circuits RCDAT series attenuators
+
+This class provides a wrapper to the MC-RCDAT attenuator modules for purposes
+of simplifying and abstracting control down to the basic necessities. It is
+not the intention of the module to expose all functionality, but to allow
+interchangeable HW to be used.
+
+See http://www.minicircuits.com/softwaredownload/Prog_Manual-6-Programmable_Attenuator.pdf
+"""
+
+
+from vts.utils.python.controllers import attenuator
+from vts.utils.python.controllers.attenuator_lib import _tnhelper
+
+
+class AttenuatorInstrument(attenuator.AttenuatorInstrument):
+ r"""This provides a specific telnet-controlled implementation of AttenuatorInstrument for
+ Mini-Circuits RC-DAT attenuators.
+
+ With the exception of telnet-specific commands, all functionality is defined by the
+ AttenuatorInstrument class. Because telnet is a stateful protocol, the functionality of
+ AttenuatorInstrument is contingent upon a telnet connection being established.
+ """
+
+ def __init__(self, num_atten=0):
+ super().__init__(num_atten)
+ self._tnhelper = _tnhelper._TNHelper(tx_cmd_separator="\r\n",
+ rx_cmd_separator="\n\r",
+ prompt="")
+
+ def __del__(self):
+ if self.is_open():
+ self.close()
+
+ def open(self, host, port=23):
+ r"""Opens a telnet connection to the desired AttenuatorInstrument and queries basic
+ information.
+
+ Parameters
+ ----------
+ host : A valid hostname (IP address or DNS-resolvable name) to an MC-DAT attenuator
+ instrument.
+ port : An optional port number (defaults to telnet default 23)
+ """
+
+ self._tnhelper.open(host, port)
+
+ if self.num_atten == 0:
+ self.num_atten = 1
+
+ config_str = self._tnhelper.cmd("MN?")
+
+ if config_str.startswith("MN="):
+ config_str = config_str[len("MN="):]
+
+ self.properties = dict(zip(['model', 'max_freq', 'max_atten'], config_str.split("-", 2)))
+ self.max_atten = float(self.properties['max_atten'])
+
+ def is_open(self):
+ r"""This function returns the state of the telnet connection to the underlying
+ AttenuatorInstrument.
+
+ Returns
+ -------
+ Bool
+ True if there is a successfully open connection to the AttenuatorInstrument
+ """
+
+ return bool(self._tnhelper.is_open())
+
+ def close(self):
+ r"""Closes a telnet connection to the desired AttenuatorInstrument.
+
+ This should be called as part of any teardown procedure prior to the attenuator
+ instrument leaving scope.
+ """
+
+ self._tnhelper.close()
+
+ def set_atten(self, idx, value):
+ r"""This function sets the attenuation of an attenuator given its index in the instrument.
+
+ Parameters
+ ----------
+ idx : This zero-based index is the identifier for a particular attenuator in an
+ instrument.
+ value : This is a floating point value for nominal attenuation to be set.
+
+ Raises
+ ------
+ InvalidOperationError
+ This error occurs if the underlying telnet connection to the instrument is not open.
+ IndexError
+ If the index of the attenuator is greater than the maximum index of the underlying
+ instrument, this error will be thrown. Do not count on this check programmatically.
+ ValueError
+ If the requested set value is greater than the maximum attenuation value, this error
+ will be thrown. Do not count on this check programmatically.
+ """
+
+ if not self.is_open():
+ raise attenuator.InvalidOperationError("Connection not open!")
+
+ if idx >= self.num_atten:
+ raise IndexError("Attenuator index out of range!", self.num_atten, idx)
+
+ if value > self.max_atten:
+ raise ValueError("Attenuator value out of range!", self.max_atten, value)
+
+ self._tnhelper.cmd("SETATT=" + str(value))
+
+ def get_atten(self, idx):
+ r"""This function returns the current attenuation from an attenuator at a given index in
+ the instrument.
+
+ Parameters
+ ----------
+ idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+
+ Raises
+ ------
+ InvalidOperationError
+ This error occurs if the underlying telnet connection to the instrument is not open.
+
+ Returns
+ -------
+ float
+ Returns a the current attenuation value
+ """
+
+ if not self.is_open():
+ raise attenuator.InvalidOperationError("Connection not open!")
+
+ atten_val = self._tnhelper.cmd("ATT?")
+
+ return float(atten_val)
diff --git a/utils/python/controllers/event_dispatcher.py b/utils/python/controllers/event_dispatcher.py
new file mode 100644
index 0000000..92b9c65
--- /dev/null
+++ b/utils/python/controllers/event_dispatcher.py
@@ -0,0 +1,423 @@
+#!/usr/bin/env python3.4
+#
+# Copyright 2016- The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from concurrent.futures import ThreadPoolExecutor
+import queue
+import re
+import socket
+import threading
+import time
+import traceback
+
+class EventDispatcherError(Exception):
+ pass
+
+class IllegalStateError(EventDispatcherError):
+ """Raise when user tries to put event_dispatcher into an illegal state.
+ """
+
+class DuplicateError(EventDispatcherError):
+ """Raise when a duplicate is being created and it shouldn't.
+ """
+
+class EventDispatcher:
+ """Class managing events for an sl4a connection.
+ """
+
+ DEFAULT_TIMEOUT = 60
+
+ def __init__(self, droid):
+ self.droid = droid
+ self.started = False
+ self.executor = None
+ self.poller = None
+ self.event_dict = {}
+ self.handlers = {}
+ self.lock = threading.RLock()
+
+ def poll_events(self):
+ """Continuously polls all types of events from sl4a.
+
+ Events are sorted by name and store in separate queues.
+ If there are registered handlers, the handlers will be called with
+ corresponding event immediately upon event discovery, and the event
+ won't be stored. If exceptions occur, stop the dispatcher and return
+ """
+ while self.started:
+ event_obj = None
+ event_name = None
+ try:
+ event_obj = self.droid.eventWait(50000)
+ except:
+ if self.started:
+ print("Exception happened during polling.")
+ print(traceback.format_exc())
+ raise
+ if not event_obj:
+ continue
+ elif 'name' not in event_obj:
+ print("Received Malformed event {}".format(event_obj))
+ continue
+ else:
+ event_name = event_obj['name']
+ # if handler registered, process event
+ if event_name in self.handlers:
+ self.handle_subscribed_event(event_obj, event_name)
+ if event_name == "EventDispatcherShutdown":
+ self.droid.closeSl4aSession()
+ break
+ else:
+ self.lock.acquire()
+ if event_name in self.event_dict: # otherwise, cache event
+ self.event_dict[event_name].put(event_obj)
+ else:
+ q = queue.Queue()
+ q.put(event_obj)
+ self.event_dict[event_name] = q
+ self.lock.release()
+
+ def register_handler(self, handler, event_name, args):
+ """Registers an event handler.
+
+ One type of event can only have one event handler associated with it.
+
+ Args:
+ handler: The event handler function to be registered.
+ event_name: Name of the event the handler is for.
+ args: User arguments to be passed to the handler when it's called.
+
+ Raises:
+ IllegalStateError: Raised if attempts to register a handler after
+ the dispatcher starts running.
+ DuplicateError: Raised if attempts to register more than one
+ handler for one type of event.
+ """
+ if self.started:
+ raise IllegalStateError(("Can't register service after polling is"
+ " started"))
+ self.lock.acquire()
+ try:
+ if event_name in self.handlers:
+ raise DuplicateError(
+ 'A handler for {} already exists'.format(event_name))
+ self.handlers[event_name] = (handler, args)
+ finally:
+ self.lock.release()
+
+ def start(self):
+ """Starts the event dispatcher.
+
+ Initiates executor and start polling events.
+
+ Raises:
+ IllegalStateError: Can't start a dispatcher again when it's already
+ running.
+ """
+ if not self.started:
+ self.started = True
+ self.executor = ThreadPoolExecutor(max_workers=32)
+ self.poller = self.executor.submit(self.poll_events)
+ else:
+ raise IllegalStateError("Dispatcher is already started.")
+
+ def clean_up(self):
+ """Clean up and release resources after the event dispatcher polling
+ loop has been broken.
+
+ The following things happen:
+ 1. Clear all events and flags.
+ 2. Close the sl4a client the event_dispatcher object holds.
+ 3. Shut down executor without waiting.
+ """
+ uid = self.droid.uid
+ if not self.started:
+ return
+ self.started = False
+ self.clear_all_events()
+ self.droid.close()
+ self.poller.set_result("Done")
+ # The polling thread is guaranteed to finish after a max of 60 seconds,
+ # so we don't wait here.
+ self.executor.shutdown(wait=False)
+
+ def pop_event(self, event_name, timeout=DEFAULT_TIMEOUT):
+ """Pop an event from its queue.
+
+ Return and remove the oldest entry of an event.
+ Block until an event of specified name is available or
+ times out if timeout is set.
+
+ Args:
+ event_name: Name of the event to be popped.
+ timeout: Number of seconds to wait when event is not present.
+ Never times out if None.
+
+ Returns:
+ event: The oldest entry of the specified event. None if timed out.
+
+ Raises:
+ IllegalStateError: Raised if pop is called before the dispatcher
+ starts polling.
+ """
+ if not self.started:
+ raise IllegalStateError(
+ "Dispatcher needs to be started before popping.")
+
+ e_queue = self.get_event_q(event_name)
+
+ if not e_queue:
+ raise TypeError(
+ "Failed to get an event queue for {}".format(event_name))
+
+ try:
+ # Block for timeout
+ if timeout:
+ return e_queue.get(True, timeout)
+ # Non-blocking poll for event
+ elif timeout == 0:
+ return e_queue.get(False)
+ else:
+ # Block forever on event wait
+ return e_queue.get(True)
+ except queue.Empty:
+ raise queue.Empty(
+ 'Timeout after {}s waiting for event: {}'.format(
+ timeout, event_name))
+
+ def wait_for_event(self, event_name, predicate,
+ timeout=DEFAULT_TIMEOUT, *args, **kwargs):
+ """Wait for an event that satisfies a predicate to appear.
+
+ Continuously pop events of a particular name and check against the
+ predicate until an event that satisfies the predicate is popped or
+ timed out. Note this will remove all the events of the same name that
+ do not satisfy the predicate in the process.
+
+ Args:
+ event_name: Name of the event to be popped.
+ predicate: A function that takes an event and returns True if the
+ predicate is satisfied, False otherwise.
+ timeout: Number of seconds to wait.
+ *args: Optional positional args passed to predicate().
+ **kwargs: Optional keyword args passed to predicate().
+
+ Returns:
+ The event that satisfies the predicate.
+
+ Raises:
+ queue.Empty: Raised if no event that satisfies the predicate was
+ found before time out.
+ """
+ deadline = time.time() + timeout
+
+ while True:
+ event = None
+ try:
+ event = self.pop_event(event_name, 1)
+ except queue.Empty:
+ pass
+
+ if event and predicate(event, *args, **kwargs):
+ return event
+
+ if time.time() > deadline:
+ raise queue.Empty(
+ 'Timeout after {}s waiting for event: {}'.format(
+ timeout, event_name))
+
+ def pop_events(self, regex_pattern, timeout):
+ """Pop events whose names match a regex pattern.
+
+ If such event(s) exist, pop one event from each event queue that
+ satisfies the condition. Otherwise, wait for an event that satisfies
+ the condition to occur, with timeout.
+
+ Results are sorted by timestamp in ascending order.
+
+ Args:
+ regex_pattern: The regular expression pattern that an event name
+ should match in order to be popped.
+ timeout: Number of seconds to wait for events in case no event
+ matching the condition exits when the function is called.
+
+ Returns:
+ results: Pop events whose names match a regex pattern.
+ Empty if none exist and the wait timed out.
+
+ Raises:
+ IllegalStateError: Raised if pop is called before the dispatcher
+ starts polling.
+ queue.Empty: Raised if no event was found before time out.
+ """
+ if not self.started:
+ raise IllegalStateError(
+ "Dispatcher needs to be started before popping.")
+ deadline = time.time() + timeout
+ while True:
+ #TODO: fix the sleep loop
+ results = self._match_and_pop(regex_pattern)
+ if len(results) != 0 or time.time() > deadline:
+ break
+ time.sleep(1)
+ if len(results) == 0:
+ raise queue.Empty(
+ 'Timeout after {}s waiting for event: {}'.format(
+ timeout, regex_pattern))
+
+ return sorted(results, key=lambda event : event['time'])
+
+ def _match_and_pop(self, regex_pattern):
+ """Pop one event from each of the event queues whose names
+ match (in a sense of regular expression) regex_pattern.
+ """
+ results = []
+ self.lock.acquire()
+ for name in self.event_dict.keys():
+ if re.match(regex_pattern, name):
+ q = self.event_dict[name]
+ if q:
+ try:
+ results.append(q.get(False))
+ except:
+ pass
+ self.lock.release()
+ return results
+
+ def get_event_q(self, event_name):
+ """Obtain the queue storing events of the specified name.
+
+ If no event of this name has been polled, wait for one to.
+
+ Returns:
+ queue: A queue storing all the events of the specified name.
+ None if timed out.
+ timeout: Number of seconds to wait for the operation.
+
+ Raises:
+ queue.Empty: Raised if the queue does not exist and timeout has
+ passed.
+ """
+ self.lock.acquire()
+ if not event_name in self.event_dict or self.event_dict[event_name] is None:
+ self.event_dict[event_name] = queue.Queue()
+ self.lock.release()
+
+ event_queue = self.event_dict[event_name]
+ return event_queue
+
+ def handle_subscribed_event(self, event_obj, event_name):
+ """Execute the registered handler of an event.
+
+ Retrieve the handler and its arguments, and execute the handler in a
+ new thread.
+
+ Args:
+ event_obj: Json object of the event.
+ event_name: Name of the event to call handler for.
+ """
+ handler, args = self.handlers[event_name]
+ self.executor.submit(handler, event_obj, *args)
+
+
+ def _handle(self, event_handler, event_name, user_args, event_timeout,
+ cond, cond_timeout):
+ """Pop an event of specified type and calls its handler on it. If
+ condition is not None, block until condition is met or timeout.
+ """
+ if cond:
+ cond.wait(cond_timeout)
+ event = self.pop_event(event_name, event_timeout)
+ return event_handler(event, *user_args)
+
+ def handle_event(self, event_handler, event_name, user_args,
+ event_timeout=None, cond=None, cond_timeout=None):
+ """Handle events that don't have registered handlers
+
+ In a new thread, poll one event of specified type from its queue and
+ execute its handler. If no such event exists, the thread waits until
+ one appears.
+
+ Args:
+ event_handler: Handler for the event, which should take at least
+ one argument - the event json object.
+ event_name: Name of the event to be handled.
+ user_args: User arguments for the handler; to be passed in after
+ the event json.
+ event_timeout: Number of seconds to wait for the event to come.
+ cond: A condition to wait on before executing the handler. Should
+ be a threading.Event object.
+ cond_timeout: Number of seconds to wait before the condition times
+ out. Never times out if None.
+
+ Returns:
+ worker: A concurrent.Future object associated with the handler.
+ If blocking call worker.result() is triggered, the handler
+ needs to return something to unblock.
+ """
+ worker = self.executor.submit(self._handle, event_handler, event_name,
+ user_args, event_timeout, cond, cond_timeout)
+ return worker
+
+ def pop_all(self, event_name):
+ """Return and remove all stored events of a specified name.
+
+ Pops all events from their queue. May miss the latest ones.
+ If no event is available, return immediately.
+
+ Args:
+ event_name: Name of the events to be popped.
+
+ Returns:
+ results: List of the desired events.
+
+ Raises:
+ IllegalStateError: Raised if pop is called before the dispatcher
+ starts polling.
+ """
+ if not self.started:
+ raise IllegalStateError(("Dispatcher needs to be started before "
+ "popping."))
+ results = []
+ try:
+ self.lock.acquire()
+ while True:
+ e = self.event_dict[event_name].get(block=False)
+ results.append(e)
+ except (queue.Empty, KeyError):
+ return results
+ finally:
+ self.lock.release()
+
+ def clear_events(self, event_name):
+ """Clear all events of a particular name.
+
+ Args:
+ event_name: Name of the events to be popped.
+ """
+ self.lock.acquire()
+ try:
+ q = self.get_event_q(event_name)
+ q.queue.clear()
+ except queue.Empty:
+ return
+ finally:
+ self.lock.release()
+
+ def clear_all_events(self):
+ """Clear all event queues and their cached events."""
+ self.lock.acquire()
+ self.event_dict.clear()
+ self.lock.release()
diff --git a/utils/python/controllers/fastboot.py b/utils/python/controllers/fastboot.py
new file mode 100644
index 0000000..096dfae
--- /dev/null
+++ b/utils/python/controllers/fastboot.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3.4
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from subprocess import Popen, PIPE
+
+def exe_cmd(*cmds):
+ """Executes commands in a new shell. Directing stderr to PIPE.
+
+ This is fastboot's own exe_cmd because of its peculiar way of writing
+ non-error info to stderr.
+
+ Args:
+ cmds: A sequence of commands and arguments.
+
+ Returns:
+ The output of the command run.
+
+ Raises:
+ Exception is raised if an error occurred during the command execution.
+ """
+ cmd = ' '.join(cmds)
+ proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
+ (out, err) = proc.communicate()
+ if not err:
+ return out
+ return err
+
+class FastbootError(Exception):
+ """Raised when there is an error in fastboot operations."""
+
+class FastbootProxy():
+ """Proxy class for fastboot.
+
+ For syntactic reasons, the '-' in fastboot commands need to be replaced
+ with '_'. Can directly execute fastboot commands on an object:
+ >> fb = FastbootProxy(<serial>)
+ >> fb.devices() # will return the console output of "fastboot devices".
+ """
+ def __init__(self, serial=""):
+ self.serial = serial
+ if serial:
+ self.fastboot_str = "fastboot -s {}".format(serial)
+ else:
+ self.fastboot_str = "fastboot"
+
+ def _exec_fastboot_cmd(self, name, arg_str):
+ return exe_cmd(' '.join((self.fastboot_str, name, arg_str)))
+
+ def args(self, *args):
+ return exe_cmd(' '.join((self.fastboot_str,) + args))
+
+ def __getattr__(self, name):
+ def fastboot_call(*args):
+ clean_name = name.replace('_', '-')
+ arg_str = ' '.join(str(elem) for elem in args)
+ return self._exec_fastboot_cmd(clean_name, arg_str)
+ return fastboot_call