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