android_xmlrpc_server.py revision db7736fec56257309bb215bb5165dce7759194f7
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        logging.info('XmlRpcServer exited.')
119
120
121    def _handle_signal(self, _signum, _frame):
122        """Handle a process signal by gracefully quitting.
123
124        SimpleXMLRPCServer helpfully exposes a method called shutdown() which
125        clears a flag similar to _keep_running, and then blocks until it sees
126        the server shut down.  Unfortunately, if you call that function from
127        a signal handler, the server will just hang, since the process is
128        paused for the signal, causing a deadlock.  Thus we are reinventing the
129        wheel with our own event loop.
130
131        """
132        self._server.server_close()
133        self._keep_running = False
134
135
136class XmlRpcServerError(Exception):
137    """Raised when an error is encountered in the XmlRpcServer."""
138
139
140class AndroidXmlRpcDelegate(object):
141    """Exposes methods called remotely during WiFi autotests.
142
143    All instance methods of this object without a preceding '_' are exposed via
144    an XMLRPC server.
145    """
146
147    WEP40_HEX_KEY_LEN = 10
148    WEP104_HEX_KEY_LEN = 26
149    SHILL_DISCONNECTED_STATES = ['idle']
150    SHILL_CONNECTED_STATES =  ['portal', 'online', 'ready']
151    DISCONNECTED_SSID = '0x'
152    DISCOVERY_POLLING_INTERVAL = 1
153
154
155    def __init__(self, serial_number):
156        """Initializes the ACTS library components.
157
158        @param serial_number Serial number of the android device to be tested,
159               None if there is only one device connected to the host.
160
161        """
162        if not serial_number:
163            ads = android_device.get_all_instances()
164            if not ads:
165                msg = "No android device found, abort!"
166                logging.error(msg)
167                raise XmlRpcServerError(msg)
168            self.ad = ads[0]
169        elif serial_number in android_device.list_adb_devices():
170            self.ad = android_device.AndroidDevice(serial_number)
171        else:
172            msg = ("Specified Android device %s can't be found, abort!"
173                   ) % serial_number
174            logging.error(msg)
175            raise XmlRpcServerError(msg)
176
177
178    def __enter__(self):
179        logging.debug('Bringing up AndroidXmlRpcDelegate.')
180        self.ad.get_droid()
181        self.ad.ed.start()
182        return self
183
184
185    def __exit__(self, exception, value, traceback):
186        logging.debug('Tearing down AndroidXmlRpcDelegate.')
187        self.ad.terminate_all_sessions()
188
189
190    # Commands start.
191    def ready(self):
192        """Confirm that the XMLRPC server is up and ready to serve.
193
194        @return True (always).
195
196        """
197        logging.debug('ready()')
198        return True
199
200
201    def list_controlled_wifi_interfaces(self):
202        return ['wlan0']
203
204
205    def set_device_enabled(self, wifi_interface, enabled):
206        """Enable or disable the WiFi device.
207
208        @param wifi_interface: string name of interface being modified.
209        @param enabled: boolean; true if this device should be enabled,
210                false if this device should be disabled.
211        @return True if it worked; false, otherwise
212
213        """
214        return utils.wifi_toggle_state(self.ad.droid, self.ad.ed, enabled)
215
216
217    def sync_time_to(self, epoch_seconds):
218        """Sync time on the DUT to |epoch_seconds| from the epoch.
219
220        @param epoch_seconds: float number of seconds from the epoch.
221
222        """
223        self.ad.droid.setTime(epoch_seconds)
224        return True
225
226
227    def clean_profiles(self):
228        return True
229
230
231    def create_profile(self, profile_name):
232        return True
233
234
235    def push_profile(self, profile_name):
236        return True
237
238
239    def remove_profile(self, profile_name):
240        return True
241
242
243    def pop_profile(self, profile_name):
244        return True
245
246
247    def disconnect(self, ssid):
248        """Attempt to disconnect from the given ssid.
249
250        Blocks until disconnected or operation has timed out.  Returns True iff
251        disconnect was successful.
252
253        @param ssid string network to disconnect from.
254        @return bool True on success, False otherwise.
255
256        """
257        # Android had no explicit disconnect, so let's just forget the network.
258        return self.delete_entries_for_ssid(ssid)
259
260
261    def get_active_wifi_SSIDs(self):
262        """Get the list of all SSIDs in the current scan results.
263
264        @return list of string SSIDs with at least one BSS we've scanned.
265
266        """
267        ssids = []
268        try:
269            self.ad.droid.wifiStartScan()
270            self.ad.ed.pop_event('WifiManagerScanResultsAvailable')
271            scan_results = self.ad.droid.wifiGetScanResults()
272            for result in scan_results:
273                if utils.WifiEnums.SSID_KEY in result:
274                    ssids.append(result[utils.WifiEnums.SSID_KEY])
275        except queue.Empty:
276            logging.error("Scan results available event timed out!")
277        except Exception as e:
278            logging.error("Scan results error: %s" % str(e))
279        finally:
280            logging.debug(ssids)
281            return ssids
282
283
284    def wait_for_service_states(self, ssid, states, timeout_seconds):
285        """Wait for SSID to reach one state out of a list of states.
286
287        @param ssid string the network to connect to (e.g. 'GoogleGuest').
288        @param states tuple the states for which to wait
289        @param timeout_seconds int seconds to wait for a state
290
291        @return (result, final_state, wait_time) tuple of the result for the
292                wait.
293        """
294        current_con = self.ad.droid.wifiGetConnectionInfo()
295        # Check the current state to see if we're connected/disconnected.
296        if set(states).intersection(set(self.SHILL_CONNECTED_STATES)):
297            if current_con[utils.WifiEnums.SSID_KEY] == ssid:
298                return True, '', 0
299            wait_event = 'WifiNetworkConnected'
300        elif set(states).intersection(set(self.SHILL_DISCONNECTED_STATES)):
301            if current_con[utils.WifiEnums.SSID_KEY] == self.DISCONNECTED_SSID:
302                return True, '', 0
303            wait_event = 'WifiNetworkDisconnected'
304        else:
305            assert 0, "Unhandled wait states received: %r" % states
306        final_state = ""
307        wait_time = -1
308        result = False
309        logging.debug(current_con)
310        try:
311            self.ad.droid.wifiStartTrackingStateChange()
312            start_time = get_current_epoch_time()
313            wait_result = self.ad.ed.pop_event(wait_event, timeout_seconds)
314            end_time = get_current_epoch_time()
315            wait_time = (end_time - start_time) / 1000
316            if wait_event == 'WifiNetworkConnected':
317                actual_ssid = wait_result['data'][utils.WifiEnums.SSID_KEY]
318                assert actual_ssid == ssid, ("Expected to connect to %s, but "
319                        "connected to %s") % (ssid, actual_ssid)
320            result = True
321        except queue.Empty:
322            logging.error("No state change available yet!")
323        except Exception as e:
324            logging.error("State change error: %s" % str(e))
325        finally:
326            logging.debug((result, final_state, wait_time))
327            self.ad.droid.wifiStopTrackingStateChange()
328            return result, final_state, wait_time
329
330
331    def delete_entries_for_ssid(self, ssid):
332        """Delete all saved entries for an SSID.
333
334        @param ssid string of SSID for which to delete entries.
335        @return True on success, False otherwise.
336
337        """
338        try:
339            utils.wifi_forget_network(self.ad, ssid)
340        except Exception as e:
341            logging.error(str(e))
342            return False
343        return True
344
345
346    def connect_wifi(self, raw_params):
347        """Block and attempt to connect to wifi network.
348
349        @param raw_params serialized AssociationParameters.
350        @return serialized AssociationResult
351
352        """
353        # Prepare data objects.
354        params = Map(raw_params)
355        params.security_config = Map(raw_params['security_config'])
356        params.bgscan_config = Map(raw_params['bgscan_config'])
357        logging.debug('connect_wifi(). Params: %r' % params)
358        network_config = {
359            "SSID": params.ssid,
360            "hiddenSSID":  True if params.is_hidden else False
361        }
362        assoc_result = {
363            "discovery_time" : 0,
364            "association_time" : 0,
365            "configuration_time" : 0,
366            "failure_reason" : "Oops!",
367            "xmlrpc_struct_type_key" : "AssociationResult"
368        }
369        duration = lambda: (get_current_epoch_time() - start_time) / 1000
370        try:
371            # Verify that the network was found, if the SSID is not hidden.
372            if not params.is_hidden:
373                start_time = get_current_epoch_time()
374                found = False
375                while duration() < params.discovery_timeout and not found:
376                    active_ssids = self.get_active_wifi_SSIDs()
377                    found = params.ssid in active_ssids
378                    if not found:
379                        time.sleep(self.DISCOVERY_POLLING_INTERVAL)
380                assoc_result["discovery_time"] = duration()
381                assert found, ("Could not find %s in scan results: %r") % (
382                        params.ssid, active_ssids)
383            result = False
384            if params.security_config.security == "psk":
385                network_config["password"] = params.security_config.psk
386            elif params.security_config.security == "wep":
387                network_config["wepTxKeyIndex"] = params.security_config.wep_default_key
388                # Convert all ASCII keys to Hex
389                wep_hex_keys = []
390                for key in params.security_config.wep_keys:
391                    if len(key) == self.WEP40_HEX_KEY_LEN or \
392                       len(key) == self.WEP104_HEX_KEY_LEN:
393                        wep_hex_keys.append(key)
394                    else:
395                        hex_key = ""
396                        for byte in bytes(key, 'utf-8'):
397                            hex_key += '%x' % byte
398                        wep_hex_keys.append(hex_key)
399                network_config["wepKeys"] = wep_hex_keys
400            # Associate to the network.
401            self.ad.droid.wifiStartTrackingStateChange()
402            start_time = get_current_epoch_time()
403            result = self.ad.droid.wifiConnect(network_config)
404            assert result, "wifiConnect call failed."
405            # Verify connection successful and correct.
406            logging.debug('wifiConnect result: %s. Waiting for connection' % result);
407            timeout = params.association_timeout + params.configuration_timeout
408            connect_result = self.ad.ed.pop_event(
409                utils.WifiEventNames.WIFI_CONNECTED, timeout)
410            assoc_result["association_time"] = duration()
411            actual_ssid = connect_result['data'][utils.WifiEnums.SSID_KEY]
412            logging.debug('Connected to SSID: %s' % params.ssid);
413            assert actual_ssid == params.ssid, ("Expected to connect to %s, "
414                "connected to %s") % (params.ssid, actual_ssid)
415            result = True
416        except queue.Empty:
417            msg = "Failed to connect to %s with %s" % (params.ssid,
418                params.security_config.security)
419            logging.error(msg)
420            assoc_result["failure_reason"] = msg
421            result = False
422        except Exception as e:
423            msg = str(e)
424            logging.error(msg)
425            assoc_result["failure_reason"] = msg
426            result = False
427        finally:
428            assoc_result["success"] = result
429            logging.debug(assoc_result)
430            self.ad.droid.wifiStopTrackingStateChange()
431            return assoc_result
432
433
434    def init_test_network_state(self):
435        """Create a clean slate for tests with respect to remembered networks.
436
437        @return True iff operation succeeded, False otherwise.
438        """
439        try:
440            utils.wifi_test_device_init(self.ad)
441        except AssertionError as e:
442            logging.error(str(e))
443            return False
444        return True
445
446
447if __name__ == '__main__':
448    parser = argparse.ArgumentParser(description='Cros Wifi Xml RPC server.')
449    parser.add_argument('-s', '--serial-number', action='store', default=None,
450                         help='Serial Number of the device to test.')
451    args = parser.parse_args()
452    logging.basicConfig(level=logging.DEBUG)
453    logging.debug("android_xmlrpc_server main...")
454    server = XmlRpcServer('localhost', 9989)
455    server.register_delegate(AndroidXmlRpcDelegate(args.serial_number))
456    server.run()
457