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.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