• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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