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