1# Copyright 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import httplib 6import logging 7import os 8import re 9import socket 10import xmlrpclib 11import pprint 12import sys 13 14from autotest_lib.client.bin import utils 15from autotest_lib.client.common_lib import logging_manager 16from autotest_lib.client.common_lib import error 17from autotest_lib.client.common_lib.cros import retry 18from autotest_lib.client.cros import constants 19from autotest_lib.server import autotest 20from autotest_lib.server.cros.multimedia import assistant_facade_adapter 21from autotest_lib.server.cros.multimedia import audio_facade_adapter 22from autotest_lib.server.cros.multimedia import bluetooth_hid_facade_adapter 23from autotest_lib.server.cros.multimedia import browser_facade_adapter 24from autotest_lib.server.cros.multimedia import cfm_facade_adapter 25from autotest_lib.server.cros.multimedia import display_facade_adapter 26from autotest_lib.server.cros.multimedia import graphics_facade_adapter 27from autotest_lib.server.cros.multimedia import input_facade_adapter 28from autotest_lib.server.cros.multimedia import kiosk_facade_adapter 29from autotest_lib.server.cros.multimedia import system_facade_adapter 30from autotest_lib.server.cros.multimedia import usb_facade_adapter 31from autotest_lib.server.cros.multimedia import video_facade_adapter 32 33 34# Log the client messages in the DEBUG level, with the prefix [client]. 35CLIENT_LOG_STREAM = logging_manager.LoggingFile( 36 level=logging.DEBUG, 37 prefix='[client] ') 38 39 40class WebSocketConnectionClosedException(Exception): 41 """WebSocket is closed during Telemetry inspecting the backend.""" 42 pass 43 44 45class _Method: 46 """Class to save the name of the RPC method instead of the real object. 47 48 It keeps the name of the RPC method locally first such that the RPC method 49 can be evalulated to a real object while it is called. Its purpose is to 50 refer to the latest RPC proxy as the original previous-saved RPC proxy may 51 be lost due to reboot. 52 53 The call_method is the method which does refer to the latest RPC proxy. 54 """ 55 56 def __init__(self, call_method, name): 57 self.__call_method = call_method 58 self.__name = name 59 60 61 def __getattr__(self, name): 62 # Support a nested method. 63 return _Method(self.__call_method, "%s.%s" % (self.__name, name)) 64 65 66 def __call__(self, *args, **dargs): 67 return self.__call_method(self.__name, *args, **dargs) 68 69 70class RemoteFacadeProxy(object): 71 """An abstraction of XML RPC proxy to the DUT multimedia server. 72 73 The traditional XML RPC server proxy is static. It is lost when DUT 74 reboots. This class reconnects the server again when it finds the 75 connection is lost. 76 77 """ 78 79 XMLRPC_CONNECT_TIMEOUT = 90 80 XMLRPC_RETRY_TIMEOUT = 180 81 XMLRPC_RETRY_DELAY = 10 82 REBOOT_TIMEOUT = 60 83 84 def __init__(self, host, no_chrome, extra_browser_args=None, 85 disable_arc=False): 86 """Construct a RemoteFacadeProxy. 87 88 @param host: Host object representing a remote host. 89 @param no_chrome: Don't start Chrome by default. 90 @param extra_browser_args: A list containing extra browser args passed 91 to Chrome in addition to default ones. 92 @param disable_arc: True to disable ARC++. 93 94 """ 95 self._client = host 96 self._xmlrpc_proxy = None 97 self._log_saving_job = None 98 self._no_chrome = no_chrome 99 self._extra_browser_args = extra_browser_args 100 self._disable_arc = disable_arc 101 self.connect() 102 if not no_chrome: 103 self._start_chrome(reconnect=False, retry=True, 104 extra_browser_args=self._extra_browser_args, 105 disable_arc=self._disable_arc) 106 107 108 def __getattr__(self, name): 109 """Return a _Method object only, not its real object.""" 110 return _Method(self.__call_proxy, name) 111 112 113 def __call_proxy(self, name, *args, **dargs): 114 """Make the call on the latest RPC proxy object. 115 116 This method gets the internal method of the RPC proxy and calls it. 117 118 @param name: Name of the RPC method, a nested method supported. 119 @param args: The rest of arguments. 120 @param dargs: The rest of dict-type arguments. 121 @return: The return value of the RPC method. 122 """ 123 def process_log(): 124 """Process the log from client, i.e. showing the log messages.""" 125 if self._log_saving_job: 126 # final_read=True to process all data until the end 127 self._log_saving_job.process_output( 128 stdout=True, final_read=True) 129 self._log_saving_job.process_output( 130 stdout=False, final_read=True) 131 132 def parse_exception(message): 133 """Parse the given message and extract the exception line. 134 135 @return: A tuple of (keyword, reason); or None if not found. 136 """ 137 # Search the line containing the exception keyword, like: 138 # "TestFail: Not able to start session." 139 # "WebSocketException... Error message: socket is already closed." 140 EXCEPTION_PATTERNS = (r'(\w+): (.+)', 141 r'(.*)\. Error message: (.*)') 142 for line in reversed(message.split('\n')): 143 for pattern in EXCEPTION_PATTERNS: 144 m = re.match(pattern, line) 145 if m: 146 return (m.group(1), m.group(2)) 147 return None 148 149 def call_rpc_with_log(): 150 """Call the RPC with log.""" 151 value = getattr(self._xmlrpc_proxy, name)(*args, **dargs) 152 process_log() 153 154 # For debug, print the return value. 155 logging.debug('RPC %s returns %s.', rpc, pprint.pformat(value)) 156 157 # Raise some well-known client exceptions, like TestFail. 158 if type(value) is str and value.startswith('Traceback'): 159 exception_tuple = parse_exception(value) 160 if exception_tuple: 161 keyword, reason = exception_tuple 162 reason = reason + ' (RPC: %s)' % name 163 if keyword == 'TestFail': 164 raise error.TestFail(reason) 165 elif keyword == 'TestError': 166 raise error.TestError(reason) 167 elif 'WebSocketConnectionClosedException' in keyword: 168 raise WebSocketConnectionClosedException(reason) 169 170 # Raise the exception with the original exception keyword. 171 raise Exception('%s: %s' % (keyword, reason)) 172 173 # Raise the default exception with the original message. 174 raise Exception('Exception from client (RPC: %s)\n%s' % 175 (name, value)) 176 177 return value 178 179 try: 180 # TODO(ihf): This logs all traffic from server to client. Make 181 # the spew optional. 182 rpc = ( 183 '%s(%s, %s)' % 184 (pprint.pformat(name), pprint.pformat(args), 185 pprint.pformat(dargs))) 186 try: 187 return call_rpc_with_log() 188 except (socket.error, 189 xmlrpclib.ProtocolError, 190 httplib.BadStatusLine, 191 WebSocketConnectionClosedException): 192 # Reconnect the RPC server in case connection lost, e.g. reboot. 193 self.connect() 194 if not self._no_chrome: 195 self._start_chrome( 196 reconnect=True, retry=False, 197 extra_browser_args=self._extra_browser_args, 198 disable_arc=self._disable_arc) 199 # Try again. 200 logging.warning('Retrying RPC %s.', rpc) 201 return call_rpc_with_log() 202 except: 203 # Process the log if any. It is helpful for debug. 204 process_log() 205 logging.error( 206 'Failed RPC %s with status [%s].', rpc, sys.exc_info()[0]) 207 raise 208 209 210 def save_log_bg(self): 211 """Save the log from client in background.""" 212 # Run a tail command in background that keeps all the log messages from 213 # client. 214 command = 'tail -n0 -f %s' % constants.MULTIMEDIA_XMLRPC_SERVER_LOG_FILE 215 full_command = '%s "%s"' % (self._client.ssh_command(), command) 216 217 if self._log_saving_job: 218 # Kill and join the previous job, probably due to a DUT reboot. 219 # In this case, a new job will be recreated. 220 logging.info('Kill and join the previous log job.') 221 utils.nuke_subprocess(self._log_saving_job.sp) 222 utils.join_bg_jobs([self._log_saving_job]) 223 224 # Create the background job and pipe its stdout and stderr to the 225 # Autotest logging. 226 self._log_saving_job = utils.BgJob(full_command, 227 stdout_tee=CLIENT_LOG_STREAM, 228 stderr_tee=CLIENT_LOG_STREAM) 229 230 231 def connect(self): 232 """Connects the XML-RPC proxy on the client. 233 234 @return: True on success. Note that if autotest server fails to 235 connect to XMLRPC server on Cros host after timeout, 236 error.TimeoutException will be raised by retry.retry 237 decorator. 238 239 """ 240 @retry.retry((socket.error, 241 xmlrpclib.ProtocolError, 242 httplib.BadStatusLine), 243 timeout_min=self.XMLRPC_RETRY_TIMEOUT / 60.0, 244 delay_sec=self.XMLRPC_RETRY_DELAY) 245 def connect_with_retries(): 246 """Connects the XML-RPC proxy with retries.""" 247 self._xmlrpc_proxy = self._client.rpc_server_tracker.xmlrpc_connect( 248 constants.MULTIMEDIA_XMLRPC_SERVER_COMMAND, 249 constants.MULTIMEDIA_XMLRPC_SERVER_PORT, 250 command_name=( 251 constants.MULTIMEDIA_XMLRPC_SERVER_CLEANUP_PATTERN 252 ), 253 ready_test_name=( 254 constants.MULTIMEDIA_XMLRPC_SERVER_READY_METHOD), 255 timeout_seconds=self.XMLRPC_CONNECT_TIMEOUT, 256 logfile=constants.MULTIMEDIA_XMLRPC_SERVER_LOG_FILE, 257 request_timeout_seconds= 258 constants.MULTIMEDIA_XMLRPC_SERVER_REQUEST_TIMEOUT) 259 260 logging.info('Setup the connection to RPC server, with retries...') 261 connect_with_retries() 262 263 logging.info('Start a job to save the log from the client.') 264 self.save_log_bg() 265 266 return True 267 268 269 def _start_chrome(self, reconnect, retry=False, extra_browser_args=None, 270 disable_arc=False): 271 """Starts Chrome using browser facade on Cros host. 272 273 @param reconnect: True for reconnection, False for the first-time. 274 @param retry: True to retry using a reboot on host. 275 @param extra_browser_args: A list containing extra browser args passed 276 to Chrome in addition to default ones. 277 @param disable_arc: True to disable ARC++. 278 279 @raise: error.TestError: if fail to start Chrome after retry. 280 281 """ 282 logging.info( 283 'Start Chrome with default arguments and extra browser args %s...', 284 extra_browser_args) 285 success = self._xmlrpc_proxy.browser.start_default_chrome( 286 reconnect, extra_browser_args, disable_arc) 287 if not success and retry: 288 logging.warning('Can not start Chrome. Reboot host and try again') 289 # Reboot host and try again. 290 self._client.reboot() 291 # Wait until XMLRPC server can be reconnected. 292 utils.poll_for_condition(condition=self.connect, 293 timeout=self.REBOOT_TIMEOUT) 294 logging.info( 295 'Retry starting Chrome with default arguments and ' 296 'extra browser args %s...', extra_browser_args) 297 success = self._xmlrpc_proxy.browser.start_default_chrome( 298 reconnect, extra_browser_args, disable_arc) 299 300 if not success: 301 raise error.TestError( 302 'Failed to start Chrome on DUT. ' 303 'Check multimedia_xmlrpc_server.log in result folder.') 304 305 306 def __del__(self): 307 """Destructor of RemoteFacadeFactory.""" 308 self._client.rpc_server_tracker.disconnect( 309 constants.MULTIMEDIA_XMLRPC_SERVER_PORT) 310 311 312class RemoteFacadeFactory(object): 313 """A factory to generate remote multimedia facades. 314 315 The facade objects are remote-wrappers to access the DUT multimedia 316 functionality, like display, video, and audio. 317 318 """ 319 320 def __init__(self, host, no_chrome=False, install_autotest=True, 321 results_dir=None, extra_browser_args=None, disable_arc=False): 322 """Construct a RemoteFacadeFactory. 323 324 @param host: Host object representing a remote host. 325 @param no_chrome: Don't start Chrome by default. 326 @param install_autotest: Install autotest on host. 327 @param results_dir: A directory to store multimedia server init log. 328 @param extra_browser_args: A list containing extra browser args passed 329 to Chrome in addition to default ones. 330 @param disable_arc: True to disable ARC++. 331 If it is not None, we will get multimedia init log to the results_dir. 332 333 """ 334 self._client = host 335 if install_autotest: 336 # Make sure the client library is on the device so that 337 # the proxy code is there when we try to call it. 338 client_at = autotest.Autotest(self._client) 339 client_at.install() 340 try: 341 self._proxy = RemoteFacadeProxy( 342 host=self._client, 343 no_chrome=no_chrome, 344 extra_browser_args=extra_browser_args, 345 disable_arc=disable_arc) 346 finally: 347 if results_dir: 348 host.get_file(constants.MULTIMEDIA_XMLRPC_SERVER_LOG_FILE, 349 os.path.join(results_dir, 350 'multimedia_xmlrpc_server.log.init')) 351 352 353 def ready(self): 354 """Returns the proxy ready status""" 355 return self._proxy.ready() 356 357 def create_assistant_facade(self): 358 """Creates an assistant facade object.""" 359 return assistant_facade_adapter.AssistantFacadeRemoteAdapter( 360 self._client, self._proxy) 361 362 def create_audio_facade(self): 363 """Creates an audio facade object.""" 364 return audio_facade_adapter.AudioFacadeRemoteAdapter( 365 self._client, self._proxy) 366 367 368 def create_video_facade(self): 369 """Creates a video facade object.""" 370 return video_facade_adapter.VideoFacadeRemoteAdapter( 371 self._client, self._proxy) 372 373 374 def create_display_facade(self): 375 """Creates a display facade object.""" 376 return display_facade_adapter.DisplayFacadeRemoteAdapter( 377 self._client, self._proxy) 378 379 380 def create_system_facade(self): 381 """Creates a system facade object.""" 382 return system_facade_adapter.SystemFacadeRemoteAdapter( 383 self._client, self._proxy) 384 385 386 def create_usb_facade(self): 387 """"Creates a USB facade object.""" 388 return usb_facade_adapter.USBFacadeRemoteAdapter(self._proxy) 389 390 391 def create_browser_facade(self): 392 """"Creates a browser facade object.""" 393 return browser_facade_adapter.BrowserFacadeRemoteAdapter(self._proxy) 394 395 396 def create_bluetooth_hid_facade(self): 397 """"Creates a bluetooth hid facade object.""" 398 return bluetooth_hid_facade_adapter.BluetoothHIDFacadeRemoteAdapter( 399 self._client, self._proxy) 400 401 402 def create_input_facade(self): 403 """"Creates an input facade object.""" 404 return input_facade_adapter.InputFacadeRemoteAdapter(self._proxy) 405 406 407 def create_cfm_facade(self): 408 """"Creates a cfm facade object.""" 409 return cfm_facade_adapter.CFMFacadeRemoteAdapter( 410 self._client, self._proxy) 411 412 413 def create_kiosk_facade(self): 414 """"Creates a kiosk facade object.""" 415 return kiosk_facade_adapter.KioskFacadeRemoteAdapter( 416 self._client, self._proxy) 417 418 419 def create_graphics_facade(self): 420 """"Creates a graphics facade object.""" 421 return graphics_facade_adapter.GraphicsFacadeRemoteAdapter(self._proxy) 422