普通文本  |  549行  |  20.22 KB

#!/usr/bin/python2.7

# Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import contextlib2
import errno
import logging
import Queue
import select
import shutil
import signal
import subprocess
import threading
import time

from SimpleXMLRPCServer import SimpleXMLRPCServer

from acts import logger
from acts import utils
from acts.controllers import android_device
from acts.controllers import attenuator
from acts.test_utils.wifi import wifi_test_utils as wutils


class Map(dict):
    """A convenience class that makes dictionary values accessible via dot
    operator.

    Example:
        >> m = Map({"SSID": "GoogleGuest"})
        >> m.SSID
        GoogleGuest
    """
    def __init__(self, *args, **kwargs):
        super(Map, self).__init__(*args, **kwargs)
        for arg in args:
            if isinstance(arg, dict):
                for k, v in arg.items():
                    self[k] = v
        if kwargs:
            for k, v in kwargs.items():
                self[k] = v


    def __getattr__(self, attr):
        return self.get(attr)


    def __setattr__(self, key, value):
        self.__setitem__(key, value)


# This is copied over from client/cros/xmlrpc_server.py so that this
# daemon has no autotest dependencies.
class XmlRpcServer(threading.Thread):
    """Simple XMLRPC server implementation.

    In theory, Python should provide a sane XMLRPC server implementation as
    part of its standard library.  In practice the provided implementation
    doesn't handle signals, not even EINTR.  As a result, we have this class.

    Usage:

    server = XmlRpcServer(('localhost', 43212))
    server.register_delegate(my_delegate_instance)
    server.run()

    """

    def __init__(self, host, port):
        """Construct an XmlRpcServer.

        @param host string hostname to bind to.
        @param port int port number to bind to.

        """
        super(XmlRpcServer, self).__init__()
        logging.info('Binding server to %s:%d', host, port)
        self._server = SimpleXMLRPCServer((host, port), allow_none=True)
        self._server.register_introspection_functions()
        self._keep_running = True
        self._delegates = []
        # Gracefully shut down on signals.  This is how we expect to be shut
        # down by autotest.
        signal.signal(signal.SIGTERM, self._handle_signal)
        signal.signal(signal.SIGINT, self._handle_signal)


    def register_delegate(self, delegate):
        """Register delegate objects with the server.

        The server will automagically look up all methods not prefixed with an
        underscore and treat them as potential RPC calls.  These methods may
        only take basic Python objects as parameters, as noted by the
        SimpleXMLRPCServer documentation.  The state of the delegate is
        persisted across calls.

        @param delegate object Python object to be exposed via RPC.

        """
        self._server.register_instance(delegate)
        self._delegates.append(delegate)


    def run(self):
        """Block and handle many XmlRpc requests."""
        logging.info('XmlRpcServer starting...')
        with contextlib2.ExitStack() as stack:
            for delegate in self._delegates:
                stack.enter_context(delegate)
            while self._keep_running:
                try:
                    self._server.handle_request()
                except select.error as v:
                    # In a cruel twist of fate, the python library doesn't
                    # handle this kind of error.
                    if v[0] != errno.EINTR:
                        raise
                except Exception as e:
                    logging.error("Error in handle request: %s", e)
        logging.info('XmlRpcServer exited.')


    def _handle_signal(self, _signum, _frame):
        """Handle a process signal by gracefully quitting.

        SimpleXMLRPCServer helpfully exposes a method called shutdown() which
        clears a flag similar to _keep_running, and then blocks until it sees
        the server shut down.  Unfortunately, if you call that function from
        a signal handler, the server will just hang, since the process is
        paused for the signal, causing a deadlock.  Thus we are reinventing the
        wheel with our own event loop.

        """
        self._server.server_close()
        self._keep_running = False


class XmlRpcServerError(Exception):
    """Raised when an error is encountered in the XmlRpcServer."""


class AndroidXmlRpcDelegate(object):
    """Exposes methods called remotely during WiFi autotests.

    All instance methods of this object without a preceding '_' are exposed via
    an XMLRPC server.
    """

    WEP40_HEX_KEY_LEN = 10
    WEP104_HEX_KEY_LEN = 26
    SHILL_DISCONNECTED_STATES = ['idle']
    SHILL_CONNECTED_STATES =  ['portal', 'online', 'ready']
    DISCONNECTED_SSID = '0x'
    DISCOVERY_POLLING_INTERVAL = 1
    NUM_ATTEN = 4


    def __init__(self, serial_number, log_dir, test_station):
        """Initializes the ACTS library components.

        @test_station string represting teststation's hostname.
        @param serial_number Serial number of the android device to be tested,
               None if there is only one device connected to the host.
        @param log_dir Path to store output logs of this run.

        """
        # Cleanup all existing logs for this device when starting.
        shutil.rmtree(log_dir, ignore_errors=True)
        logger.setup_test_logger(log_path=log_dir, prefix="ANDROID_XMLRPC")
        if not serial_number:
            ads = android_device.get_all_instances()
            if not ads:
                msg = "No android device found, abort!"
                logging.error(msg)
                raise XmlRpcServerError(msg)
            self.ad = ads[0]
        elif serial_number in android_device.list_adb_devices():
            self.ad = android_device.AndroidDevice(serial_number)
        else:
            msg = ("Specified Android device %s can't be found, abort!"
                   ) % serial_number
            logging.error(msg)
            raise XmlRpcServerError(msg)
        # Even if we find one attenuator assume the rig has attenuators for now.
        # With the single IP attenuator, this will be a easy check.
        rig_has_attenuator = False
        count = 0
        for i in range(1, self.NUM_ATTEN + 1):
            atten_addr = test_station+'-attenuator-'+'%d' %i
            if subprocess.Popen(['ping', '-c', '2', atten_addr],
                                 stdout=subprocess.PIPE).communicate()[0]:
                rig_has_attenuator = True
                count = count + 1
        if rig_has_attenuator and count == self.NUM_ATTEN:
            atten = attenuator.create([{"Address":test_station+'-attenuator-1',
                                        "Port":23,
                                        "Model":"minicircuits",
                                        "InstrumentCount": 1,
                                        "Paths":["Attenuator-1"]},
                                        {"Address":test_station+'-attenuator-2',
                                        "Port":23,
                                        "Model":"minicircuits",
                                        "InstrumentCount": 1,
                                        "Paths":["Attenuator-2"]},
                                        {"Address":test_station+'-attenuator-3',
                                        "Port":23,
                                        "Model":"minicircuits",
                                        "InstrumentCount": 1,
                                        "Paths":["Attenuator-3"]},
                                        {"Address":test_station+'-attenuator-4',
                                        "Port":23,
                                        "Model":"minicircuits",
                                        "InstrumentCount": 1,
                                        "Paths":["Attenuator-4"]}])
            device = 0
            # Set attenuation on all attenuators to 0.
            for device in range(len(atten)):
               atten[device].set_atten(0)
            attenuator.destroy(atten)
        elif rig_has_attenuator and count < self.NUM_ATTEN:
            msg = 'One or more attenuators are down.'
            logging.error(msg)
            raise XmlRpcServerError(msg)


    def __enter__(self):
        logging.debug('Bringing up AndroidXmlRpcDelegate.')
        self.ad.get_droid()
        self.ad.ed.start()
        self.ad.start_adb_logcat()
        return self


    def __exit__(self, exception, value, traceback):
        logging.debug('Tearing down AndroidXmlRpcDelegate.')
        self.ad.terminate_all_sessions()
        self.ad.stop_adb_logcat()


    # Commands start.
    def ready(self):
        """Confirm that the XMLRPC server is up and ready to serve.

        @return True (always).

        """
        logging.debug('ready()')
        return True


    def collect_debug_info(self, test_name):
        """Collects appropriate debug information on DUT.

        @param test_name: string name of the test to collect debug information
                          for.
        """
        self.ad.cat_adb_log(test_name, self.test_begin_time)
        self.ad.take_bug_report(test_name, self.test_begin_time)


    def list_controlled_wifi_interfaces(self):
        """List all controlled wifi interfaces (just wlan0 for Android). """
        return ['wlan0']


    def set_device_enabled(self, wifi_interface, enabled):
        """Enable or disable the WiFi device.

        @param wifi_interface: string name of interface being modified.
        @param enabled: boolean; true if this device should be enabled,
                false if this device should be disabled.
        @return True if it worked; false, otherwise

        """
        return wutils.wifi_toggle_state(self.ad, enabled)


    def sync_time_to(self, epoch_seconds):
        """Sync time on the DUT to |epoch_seconds| from the epoch.

        @param epoch_seconds: float number of seconds from the epoch.

        """
        # The adb_host is already doing this; just return True.
        return True


    def clean_profiles(self):
        """ Not applicable for Android.
        @param profile_name: Ignored.
        """
        return True


    def create_profile(self, profile_name):
        """ Not applicable for Android.
        @param profile_name: Ignored.
        """
        return True


    def push_profile(self, profile_name):
        """ Not applicable for Android.
        @param profile_name: Ignored.
        """
        return True


    def remove_profile(self, profile_name):
        """ Not applicable for Android.
        @param profile_name: Ignored.
        """
        return True


    def pop_profile(self, profile_name):
        """ Not applicable for Android.
        @param profile_name: Ignored.
        """
        return True


    def disconnect(self, ssid):
        """Attempt to disconnect from the given ssid.

        Blocks until disconnected or operation has timed out.  Returns True iff
        disconnect was successful.

        @param ssid string network to disconnect from.
        @return bool True on success, False otherwise.

        """
        # Android had no explicit disconnect, so let's just forget the network.
        return self.delete_entries_for_ssid(ssid)


    def get_active_wifi_SSIDs(self):
        """Get the list of all SSIDs in the current scan results.

        @return list of string SSIDs with at least one BSS we've scanned.

        """
        ssids = []
        try:
            self.ad.droid.wifiStartScan()
            self.ad.ed.pop_event('WifiManagerScanResultsAvailable')
            scan_results = self.ad.droid.wifiGetScanResults()
            for result in scan_results:
                if wutils.WifiEnums.SSID_KEY in result:
                    ssids.append(result[wutils.WifiEnums.SSID_KEY])
        except Queue.Empty:
            logging.error("Scan results available event timed out!")
        except Exception as e:
            logging.error("Scan results error: %s", e)
        finally:
            logging.debug("Scan Results: %r", ssids)
            return ssids


    def wait_for_service_states(self, ssid, states, timeout_seconds):
        """Wait for SSID to reach one state out of a list of states.

        @param ssid string the network to connect to (e.g. 'GoogleGuest').
        @param states tuple the states for which to wait
        @param timeout_seconds int seconds to wait for a state

        @return (result, final_state, wait_time) tuple of the result for the
                wait.
        """
        current_con = self.ad.droid.wifiGetConnectionInfo()
        # Check the current state to see if we're connected/disconnected.
        if set(states).intersection(set(self.SHILL_CONNECTED_STATES)):
            if current_con[wutils.WifiEnums.SSID_KEY] == ssid:
                return True, '', 0
            wait_event = 'WifiNetworkConnected'
        elif set(states).intersection(set(self.SHILL_DISCONNECTED_STATES)):
            if current_con[wutils.WifiEnums.SSID_KEY] == self.DISCONNECTED_SSID:
                return True, '', 0
            wait_event = 'WifiNetworkDisconnected'
        else:
            assert 0, "Unhandled wait states received: %r" % states
        final_state = ""
        wait_time = -1
        result = False
        logging.debug(current_con)
        try:
            self.ad.droid.wifiStartTrackingStateChange()
            start_time = utils.get_current_epoch_time()
            wait_result = self.ad.ed.pop_event(wait_event, timeout_seconds)
            end_time = utils.get_current_epoch_time()
            wait_time = (end_time - start_time) / 1000
            if wait_event == 'WifiNetworkConnected':
                actual_ssid = wait_result['data'][wutils.WifiEnums.SSID_KEY]
                assert actual_ssid == ssid, ("Expected to connect to %s, but "
                        "connected to %s") % (ssid, actual_ssid)
            result = True
        except Queue.Empty:
            logging.error("No state change available yet!")
        except Exception as e:
            logging.error("State change error: %s", e)
        finally:
            logging.debug((result, final_state, wait_time))
            self.ad.droid.wifiStopTrackingStateChange()
            return result, final_state, wait_time


    def delete_entries_for_ssid(self, ssid):
        """Delete all saved entries for an SSID.

        @param ssid string of SSID for which to delete entries.
        @return True on success, False otherwise.

        """
        try:
            wutils.wifi_forget_network(self.ad, ssid)
        except Exception as e:
            logging.error(e)
            return False
        return True


    def connect_wifi(self, raw_params):
        """Block and attempt to connect to wifi network.

        @param raw_params serialized AssociationParameters.
        @return serialized AssociationResult

        """
        # Prepare data objects.
        params = Map(raw_params)
        params.security_config = Map(raw_params['security_config'])
        params.bgscan_config = Map(raw_params['bgscan_config'])
        logging.debug('connect_wifi(). Params: %r', params)
        network_config = {
            "SSID": params.ssid,
            "hiddenSSID":  True if params.is_hidden else False
        }
        assoc_result = {
            "discovery_time" : 0,
            "association_time" : 0,
            "configuration_time" : 0,
            "failure_reason" : "None",
            "xmlrpc_struct_type_key" : "AssociationResult"
        }
        duration = lambda: (utils.get_current_epoch_time() - start_time) / 1000
        try:
            # Verify that the network was found, if the SSID is not hidden.
            if not params.is_hidden:
                start_time = utils.get_current_epoch_time()
                found = False
                while duration() < params.discovery_timeout and not found:
                    active_ssids = self.get_active_wifi_SSIDs()
                    found = params.ssid in active_ssids
                    if not found:
                        time.sleep(self.DISCOVERY_POLLING_INTERVAL)
                assoc_result["discovery_time"] = duration()
                assert found, ("Could not find %s in scan results: %r") % (
                        params.ssid, active_ssids)
            result = False
            if params.security_config.security == "psk":
                network_config["password"] = params.security_config.psk
            elif params.security_config.security == "wep":
                network_config["wepTxKeyIndex"] = params.security_config.wep_default_key
                # Convert all ASCII keys to Hex
                wep_hex_keys = []
                for key in params.security_config.wep_keys:
                    if len(key) == self.WEP40_HEX_KEY_LEN or \
                       len(key) == self.WEP104_HEX_KEY_LEN:
                        wep_hex_keys.append(key)
                    else:
                        hex_key = ""
                        for byte in bytearray(key, 'utf-8'):
                            hex_key += '%x' % byte
                        wep_hex_keys.append(hex_key)
                network_config["wepKeys"] = wep_hex_keys
            # Associate to the network.
            self.ad.droid.wifiStartTrackingStateChange()
            start_time = utils.get_current_epoch_time()
            result = self.ad.droid.wifiConnect(network_config)
            assert result, "wifiConnect call failed."
            # Verify connection successful and correct.
            logging.debug('wifiConnect result: %s. Waiting for connection', result);
            timeout = params.association_timeout + params.configuration_timeout
            connect_result = self.ad.ed.pop_event(
                wutils.WifiEventNames.WIFI_CONNECTED, timeout)
            assoc_result["association_time"] = duration()
            actual_ssid = connect_result['data'][wutils.WifiEnums.SSID_KEY]
            logging.debug('Connected to SSID: %s', actual_ssid);
            assert actual_ssid == params.ssid, ("Expected to connect to %s, "
                "connected to %s") % (params.ssid, actual_ssid)
            result = True
        except Queue.Empty:
            msg = "Failed to connect to %s with %s" % (params.ssid,
                params.security_config.security)
            logging.error(msg)
            assoc_result["failure_reason"] = msg
            result = False
        except Exception as e:
            msg = e
            logging.error(msg)
            assoc_result["failure_reason"] = msg
            result = False
        finally:
            assoc_result["success"] = result
            logging.debug(assoc_result)
            self.ad.droid.wifiStopTrackingStateChange()
            return assoc_result


    def init_test_network_state(self):
        """Create a clean slate for tests with respect to remembered networks.

        @return True iff operation succeeded, False otherwise.
        """
        self.test_begin_time = logger.get_log_line_timestamp()
        try:
            wutils.wifi_test_device_init(self.ad)
            self.ad.ed.clear_all_events()
        except AssertionError as e:
            logging.error(e)
            return False
        return True


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Cros Wifi Xml RPC server.')
    parser.add_argument('-s', '--serial-number', action='store', default=None,
                         help='Serial Number of the device to test.')
    parser.add_argument('-l', '--log-dir', action='store', default=None,
                         help='Path to store output logs.')
    parser.add_argument('-t', '--test-station', action='store', default=None,
                         help='The accompaning teststion hostname.')
    parser.add_argument('-p', '--port', action='store', default=9989,
                        type=int, help='The port number to listen on.')
    args = parser.parse_args()
    listen_port = args.port
    logging.basicConfig(level=logging.DEBUG)
    logging.debug("android_xmlrpc_server main...")
    logging.debug('xmlrpc instance on port %d' % listen_port)
    server = XmlRpcServer('localhost', listen_port)
    server.register_delegate(
            AndroidXmlRpcDelegate(args.serial_number, args.log_dir,
                                  args.test_station))
    server.run()