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