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