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