1#!/usr/bin/python2.7
2
3# Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import argparse
8import contextlib2
9import errno
10import logging
11import Queue
12import select
13import shutil
14import signal
15import subprocess
16import threading
17import time
18
19from SimpleXMLRPCServer import SimpleXMLRPCServer
20
21from acts import logger
22from acts import utils
23from acts.controllers import android_device
24from acts.controllers import attenuator
25from acts.test_utils.wifi import wifi_test_utils as wutils
26
27
28class Map(dict):
29    """A convenience class that makes dictionary values accessible via dot
30    operator.
31
32    Example:
33        >> m = Map({"SSID": "GoogleGuest"})
34        >> m.SSID
35        GoogleGuest
36    """
37    def __init__(self, *args, **kwargs):
38        super(Map, self).__init__(*args, **kwargs)
39        for arg in args:
40            if isinstance(arg, dict):
41                for k, v in arg.items():
42                    self[k] = v
43        if kwargs:
44            for k, v in kwargs.items():
45                self[k] = v
46
47
48    def __getattr__(self, attr):
49        return self.get(attr)
50
51
52    def __setattr__(self, key, value):
53        self.__setitem__(key, value)
54
55
56# This is copied over from client/cros/xmlrpc_server.py so that this
57# daemon has no autotest dependencies.
58class XmlRpcServer(threading.Thread):
59    """Simple XMLRPC server implementation.
60
61    In theory, Python should provide a sane XMLRPC server implementation as
62    part of its standard library.  In practice the provided implementation
63    doesn't handle signals, not even EINTR.  As a result, we have this class.
64
65    Usage:
66
67    server = XmlRpcServer(('localhost', 43212))
68    server.register_delegate(my_delegate_instance)
69    server.run()
70
71    """
72
73    def __init__(self, host, port):
74        """Construct an XmlRpcServer.
75
76        @param host string hostname to bind to.
77        @param port int port number to bind to.
78
79        """
80        super(XmlRpcServer, self).__init__()
81        logging.info('Binding server to %s:%d', host, port)
82        self._server = SimpleXMLRPCServer((host, port), allow_none=True)
83        self._server.register_introspection_functions()
84        self._keep_running = True
85        self._delegates = []
86        # Gracefully shut down on signals.  This is how we expect to be shut
87        # down by autotest.
88        signal.signal(signal.SIGTERM, self._handle_signal)
89        signal.signal(signal.SIGINT, self._handle_signal)
90
91
92    def register_delegate(self, delegate):
93        """Register delegate objects with the server.
94
95        The server will automagically look up all methods not prefixed with an
96        underscore and treat them as potential RPC calls.  These methods may
97        only take basic Python objects as parameters, as noted by the
98        SimpleXMLRPCServer documentation.  The state of the delegate is
99        persisted across calls.
100
101        @param delegate object Python object to be exposed via RPC.
102
103        """
104        self._server.register_instance(delegate)
105        self._delegates.append(delegate)
106
107
108    def run(self):
109        """Block and handle many XmlRpc requests."""
110        logging.info('XmlRpcServer starting...')
111        with contextlib2.ExitStack() as stack:
112            for delegate in self._delegates:
113                stack.enter_context(delegate)
114            while self._keep_running:
115                try:
116                    self._server.handle_request()
117                except select.error as v:
118                    # In a cruel twist of fate, the python library doesn't
119                    # handle this kind of error.
120                    if v[0] != errno.EINTR:
121                        raise
122                except Exception as e:
123                    logging.error("Error in handle request: %s", e)
124        logging.info('XmlRpcServer exited.')
125
126
127    def _handle_signal(self, _signum, _frame):
128        """Handle a process signal by gracefully quitting.
129
130        SimpleXMLRPCServer helpfully exposes a method called shutdown() which
131        clears a flag similar to _keep_running, and then blocks until it sees
132        the server shut down.  Unfortunately, if you call that function from
133        a signal handler, the server will just hang, since the process is
134        paused for the signal, causing a deadlock.  Thus we are reinventing the
135        wheel with our own event loop.
136
137        """
138        self._server.server_close()
139        self._keep_running = False
140
141
142class XmlRpcServerError(Exception):
143    """Raised when an error is encountered in the XmlRpcServer."""
144
145
146class AndroidXmlRpcDelegate(object):
147    """Exposes methods called remotely during WiFi autotests.
148
149    All instance methods of this object without a preceding '_' are exposed via
150    an XMLRPC server.
151    """
152
153    WEP40_HEX_KEY_LEN = 10
154    WEP104_HEX_KEY_LEN = 26
155    SHILL_DISCONNECTED_STATES = ['idle']
156    SHILL_CONNECTED_STATES =  ['portal', 'online', 'ready']
157    DISCONNECTED_SSID = '0x'
158    DISCOVERY_POLLING_INTERVAL = 1
159    NUM_ATTEN = 4
160
161
162    def __init__(self, serial_number, log_dir, test_station):
163        """Initializes the ACTS library components.
164
165        @test_station string represting teststation's hostname.
166        @param serial_number Serial number of the android device to be tested,
167               None if there is only one device connected to the host.
168        @param log_dir Path to store output logs of this run.
169
170        """
171        # Cleanup all existing logs for this device when starting.
172        shutil.rmtree(log_dir, ignore_errors=True)
173        logger.setup_test_logger(log_path=log_dir, prefix="ANDROID_XMLRPC")
174        if not serial_number:
175            ads = android_device.get_all_instances()
176            if not ads:
177                msg = "No android device found, abort!"
178                logging.error(msg)
179                raise XmlRpcServerError(msg)
180            self.ad = ads[0]
181        elif serial_number in android_device.list_adb_devices():
182            self.ad = android_device.AndroidDevice(serial_number)
183        else:
184            msg = ("Specified Android device %s can't be found, abort!"
185                   ) % serial_number
186            logging.error(msg)
187            raise XmlRpcServerError(msg)
188        # Even if we find one attenuator assume the rig has attenuators for now.
189        # With the single IP attenuator, this will be a easy check.
190        rig_has_attenuator = False
191        count = 0
192        for i in range(1, self.NUM_ATTEN + 1):
193            atten_addr = test_station+'-attenuator-'+'%d' %i
194            if subprocess.Popen(['ping', '-c', '2', atten_addr],
195                                 stdout=subprocess.PIPE).communicate()[0]:
196                rig_has_attenuator = True
197                count = count + 1
198        if rig_has_attenuator and count == self.NUM_ATTEN:
199            atten = attenuator.create([{"Address":test_station+'-attenuator-1',
200                                        "Port":23,
201                                        "Model":"minicircuits",
202                                        "InstrumentCount": 1,
203                                        "Paths":["Attenuator-1"]},
204                                        {"Address":test_station+'-attenuator-2',
205                                        "Port":23,
206                                        "Model":"minicircuits",
207                                        "InstrumentCount": 1,
208                                        "Paths":["Attenuator-2"]},
209                                        {"Address":test_station+'-attenuator-3',
210                                        "Port":23,
211                                        "Model":"minicircuits",
212                                        "InstrumentCount": 1,
213                                        "Paths":["Attenuator-3"]},
214                                        {"Address":test_station+'-attenuator-4',
215                                        "Port":23,
216                                        "Model":"minicircuits",
217                                        "InstrumentCount": 1,
218                                        "Paths":["Attenuator-4"]}])
219            device = 0
220            # Set attenuation on all attenuators to 0.
221            for device in range(len(atten)):
222               atten[device].set_atten(0)
223            attenuator.destroy(atten)
224        elif rig_has_attenuator and count < self.NUM_ATTEN:
225            msg = 'One or more attenuators are down.'
226            logging.error(msg)
227            raise XmlRpcServerError(msg)
228
229
230    def __enter__(self):
231        logging.debug('Bringing up AndroidXmlRpcDelegate.')
232        self.ad.get_droid()
233        self.ad.ed.start()
234        self.ad.start_adb_logcat()
235        return self
236
237
238    def __exit__(self, exception, value, traceback):
239        logging.debug('Tearing down AndroidXmlRpcDelegate.')
240        self.ad.terminate_all_sessions()
241        self.ad.stop_adb_logcat()
242
243
244    # Commands start.
245    def ready(self):
246        """Confirm that the XMLRPC server is up and ready to serve.
247
248        @return True (always).
249
250        """
251        logging.debug('ready()')
252        return True
253
254
255    def collect_debug_info(self, test_name):
256        """Collects appropriate debug information on DUT.
257
258        @param test_name: string name of the test to collect debug information
259                          for.
260        """
261        self.ad.cat_adb_log(test_name, self.test_begin_time)
262        self.ad.take_bug_report(test_name, self.test_begin_time)
263
264
265    def list_controlled_wifi_interfaces(self):
266        """List all controlled wifi interfaces (just wlan0 for Android). """
267        return ['wlan0']
268
269
270    def set_device_enabled(self, wifi_interface, enabled):
271        """Enable or disable the WiFi device.
272
273        @param wifi_interface: string name of interface being modified.
274        @param enabled: boolean; true if this device should be enabled,
275                false if this device should be disabled.
276        @return True if it worked; false, otherwise
277
278        """
279        return wutils.wifi_toggle_state(self.ad, enabled)
280
281
282    def sync_time_to(self, epoch_seconds):
283        """Sync time on the DUT to |epoch_seconds| from the epoch.
284
285        @param epoch_seconds: float number of seconds from the epoch.
286
287        """
288        # The adb_host is already doing this; just return True.
289        return True
290
291
292    def clean_profiles(self):
293        """ Not applicable for Android.
294        @param profile_name: Ignored.
295        """
296        return True
297
298
299    def create_profile(self, profile_name):
300        """ Not applicable for Android.
301        @param profile_name: Ignored.
302        """
303        return True
304
305
306    def push_profile(self, profile_name):
307        """ Not applicable for Android.
308        @param profile_name: Ignored.
309        """
310        return True
311
312
313    def remove_profile(self, profile_name):
314        """ Not applicable for Android.
315        @param profile_name: Ignored.
316        """
317        return True
318
319
320    def pop_profile(self, profile_name):
321        """ Not applicable for Android.
322        @param profile_name: Ignored.
323        """
324        return True
325
326
327    def disconnect(self, ssid):
328        """Attempt to disconnect from the given ssid.
329
330        Blocks until disconnected or operation has timed out.  Returns True iff
331        disconnect was successful.
332
333        @param ssid string network to disconnect from.
334        @return bool True on success, False otherwise.
335
336        """
337        # Android had no explicit disconnect, so let's just forget the network.
338        return self.delete_entries_for_ssid(ssid)
339
340
341    def get_active_wifi_SSIDs(self):
342        """Get the list of all SSIDs in the current scan results.
343
344        @return list of string SSIDs with at least one BSS we've scanned.
345
346        """
347        ssids = []
348        try:
349            self.ad.droid.wifiStartScan()
350            self.ad.ed.pop_event('WifiManagerScanResultsAvailable')
351            scan_results = self.ad.droid.wifiGetScanResults()
352            for result in scan_results:
353                if wutils.WifiEnums.SSID_KEY in result:
354                    ssids.append(result[wutils.WifiEnums.SSID_KEY])
355        except Queue.Empty:
356            logging.error("Scan results available event timed out!")
357        except Exception as e:
358            logging.error("Scan results error: %s", e)
359        finally:
360            logging.debug("Scan Results: %r", ssids)
361            return ssids
362
363
364    def wait_for_service_states(self, ssid, states, timeout_seconds):
365        """Wait for SSID to reach one state out of a list of states.
366
367        @param ssid string the network to connect to (e.g. 'GoogleGuest').
368        @param states tuple the states for which to wait
369        @param timeout_seconds int seconds to wait for a state
370
371        @return (result, final_state, wait_time) tuple of the result for the
372                wait.
373        """
374        current_con = self.ad.droid.wifiGetConnectionInfo()
375        # Check the current state to see if we're connected/disconnected.
376        if set(states).intersection(set(self.SHILL_CONNECTED_STATES)):
377            if current_con[wutils.WifiEnums.SSID_KEY] == ssid:
378                return True, '', 0
379            wait_event = 'WifiNetworkConnected'
380        elif set(states).intersection(set(self.SHILL_DISCONNECTED_STATES)):
381            if current_con[wutils.WifiEnums.SSID_KEY] == self.DISCONNECTED_SSID:
382                return True, '', 0
383            wait_event = 'WifiNetworkDisconnected'
384        else:
385            assert 0, "Unhandled wait states received: %r" % states
386        final_state = ""
387        wait_time = -1
388        result = False
389        logging.debug(current_con)
390        try:
391            self.ad.droid.wifiStartTrackingStateChange()
392            start_time = utils.get_current_epoch_time()
393            wait_result = self.ad.ed.pop_event(wait_event, timeout_seconds)
394            end_time = utils.get_current_epoch_time()
395            wait_time = (end_time - start_time) / 1000
396            if wait_event == 'WifiNetworkConnected':
397                actual_ssid = wait_result['data'][wutils.WifiEnums.SSID_KEY]
398                assert actual_ssid == ssid, ("Expected to connect to %s, but "
399                        "connected to %s") % (ssid, actual_ssid)
400            result = True
401        except Queue.Empty:
402            logging.error("No state change available yet!")
403        except Exception as e:
404            logging.error("State change error: %s", e)
405        finally:
406            logging.debug((result, final_state, wait_time))
407            self.ad.droid.wifiStopTrackingStateChange()
408            return result, final_state, wait_time
409
410
411    def delete_entries_for_ssid(self, ssid):
412        """Delete all saved entries for an SSID.
413
414        @param ssid string of SSID for which to delete entries.
415        @return True on success, False otherwise.
416
417        """
418        try:
419            wutils.wifi_forget_network(self.ad, ssid)
420        except Exception as e:
421            logging.error(e)
422            return False
423        return True
424
425
426    def connect_wifi(self, raw_params):
427        """Block and attempt to connect to wifi network.
428
429        @param raw_params serialized AssociationParameters.
430        @return serialized AssociationResult
431
432        """
433        # Prepare data objects.
434        params = Map(raw_params)
435        params.security_config = Map(raw_params['security_config'])
436        params.bgscan_config = Map(raw_params['bgscan_config'])
437        logging.debug('connect_wifi(). Params: %r', params)
438        network_config = {
439            "SSID": params.ssid,
440            "hiddenSSID":  True if params.is_hidden else False
441        }
442        assoc_result = {
443            "discovery_time" : 0,
444            "association_time" : 0,
445            "configuration_time" : 0,
446            "failure_reason" : "None",
447            "xmlrpc_struct_type_key" : "AssociationResult"
448        }
449        duration = lambda: (utils.get_current_epoch_time() - start_time) / 1000
450        try:
451            # Verify that the network was found, if the SSID is not hidden.
452            if not params.is_hidden:
453                start_time = utils.get_current_epoch_time()
454                found = False
455                while duration() < params.discovery_timeout and not found:
456                    active_ssids = self.get_active_wifi_SSIDs()
457                    found = params.ssid in active_ssids
458                    if not found:
459                        time.sleep(self.DISCOVERY_POLLING_INTERVAL)
460                assoc_result["discovery_time"] = duration()
461                assert found, ("Could not find %s in scan results: %r") % (
462                        params.ssid, active_ssids)
463            result = False
464            if params.security_config.security == "psk":
465                network_config["password"] = params.security_config.psk
466            elif params.security_config.security == "wep":
467                network_config["wepTxKeyIndex"] = params.security_config.wep_default_key
468                # Convert all ASCII keys to Hex
469                wep_hex_keys = []
470                for key in params.security_config.wep_keys:
471                    if len(key) == self.WEP40_HEX_KEY_LEN or \
472                       len(key) == self.WEP104_HEX_KEY_LEN:
473                        wep_hex_keys.append(key)
474                    else:
475                        hex_key = ""
476                        for byte in bytearray(key, 'utf-8'):
477                            hex_key += '%x' % byte
478                        wep_hex_keys.append(hex_key)
479                network_config["wepKeys"] = wep_hex_keys
480            # Associate to the network.
481            self.ad.droid.wifiStartTrackingStateChange()
482            start_time = utils.get_current_epoch_time()
483            result = self.ad.droid.wifiConnect(network_config)
484            assert result, "wifiConnect call failed."
485            # Verify connection successful and correct.
486            logging.debug('wifiConnect result: %s. Waiting for connection', result);
487            timeout = params.association_timeout + params.configuration_timeout
488            connect_result = self.ad.ed.pop_event(
489                wutils.WifiEventNames.WIFI_CONNECTED, timeout)
490            assoc_result["association_time"] = duration()
491            actual_ssid = connect_result['data'][wutils.WifiEnums.SSID_KEY]
492            logging.debug('Connected to SSID: %s', actual_ssid);
493            assert actual_ssid == params.ssid, ("Expected to connect to %s, "
494                "connected to %s") % (params.ssid, actual_ssid)
495            result = True
496        except Queue.Empty:
497            msg = "Failed to connect to %s with %s" % (params.ssid,
498                params.security_config.security)
499            logging.error(msg)
500            assoc_result["failure_reason"] = msg
501            result = False
502        except Exception as e:
503            msg = e
504            logging.error(msg)
505            assoc_result["failure_reason"] = msg
506            result = False
507        finally:
508            assoc_result["success"] = result
509            logging.debug(assoc_result)
510            self.ad.droid.wifiStopTrackingStateChange()
511            return assoc_result
512
513
514    def init_test_network_state(self):
515        """Create a clean slate for tests with respect to remembered networks.
516
517        @return True iff operation succeeded, False otherwise.
518        """
519        self.test_begin_time = logger.get_log_line_timestamp()
520        try:
521            wutils.wifi_test_device_init(self.ad)
522            self.ad.ed.clear_all_events()
523        except AssertionError as e:
524            logging.error(e)
525            return False
526        return True
527
528
529if __name__ == '__main__':
530    parser = argparse.ArgumentParser(description='Cros Wifi Xml RPC server.')
531    parser.add_argument('-s', '--serial-number', action='store', default=None,
532                         help='Serial Number of the device to test.')
533    parser.add_argument('-l', '--log-dir', action='store', default=None,
534                         help='Path to store output logs.')
535    parser.add_argument('-t', '--test-station', action='store', default=None,
536                         help='The accompaning teststion hostname.')
537    parser.add_argument('-p', '--port', action='store', default=9989,
538                        type=int, help='The port number to listen on.')
539    args = parser.parse_args()
540    listen_port = args.port
541    logging.basicConfig(level=logging.DEBUG)
542    logging.debug("android_xmlrpc_server main...")
543    logging.debug('xmlrpc instance on port %d' % listen_port)
544    server = XmlRpcServer('localhost', listen_port)
545    server.register_delegate(
546            AndroidXmlRpcDelegate(args.serial_number, args.log_dir,
547                                  args.test_station))
548    server.run()
549