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