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