1# Lint as: python2, python3
2# Copyright 2015 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
6"""A module providing common resources for different facades."""
7
8import exceptions
9import logging
10import time
11
12from autotest_lib.client.bin import utils
13from autotest_lib.client.common_lib.cros import chrome
14from autotest_lib.client.common_lib.cros import retry
15from autotest_lib.client.cros import constants
16
17import py_utils
18
19_FLAKY_CALL_RETRY_TIMEOUT_SEC = 60
20_FLAKY_CHROME_CALL_RETRY_DELAY_SEC = 1
21
22retry_chrome_call = retry.retry(
23        (chrome.Error, exceptions.IndexError, exceptions.Exception),
24        timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0,
25        delay_sec=_FLAKY_CHROME_CALL_RETRY_DELAY_SEC)
26
27
28class FacadeResoureError(Exception):
29    """Error in FacadeResource."""
30    pass
31
32
33_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC = 120
34_FLAKY_CHROME_START_RETRY_DELAY_SEC = 10
35
36
37# Telemetry sometimes fails to start Chrome.
38retry_start_chrome = retry.retry(
39        (Exception,),
40        timeout_min=_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC / 60.0,
41        delay_sec=_FLAKY_CHROME_START_RETRY_DELAY_SEC,
42        exception_to_raise=FacadeResoureError,
43        label='Start Chrome')
44
45
46class FacadeResource(object):
47    """This class provides access to telemetry chrome wrapper."""
48
49    ARC_DISABLED = 'disabled'
50    ARC_ENABLED = 'enabled'
51    ARC_VERSION = 'CHROMEOS_ARC_VERSION'
52    EXTRA_BROWSER_ARGS = ['--enable-gpu-benchmarking', '--use-fake-ui-for-media-stream']
53
54    def __init__(self, chrome_object=None, restart=False):
55        """Initializes a FacadeResource.
56
57        @param chrome_object: A chrome.Chrome object or None.
58        @param restart: Preserve the previous browser state.
59
60        """
61        self._chrome = chrome_object
62
63    @property
64    def _browser(self):
65        """Gets the browser object from Chrome."""
66        return self._chrome.browser
67
68
69    @retry_start_chrome
70    def _start_chrome(self, kwargs):
71        """Start a Chrome with given arguments.
72
73        @param kwargs: A dict of keyword arguments passed to Chrome.
74
75        @return: A chrome.Chrome object.
76
77        """
78        logging.debug('Try to start Chrome with kwargs: %s', kwargs)
79        return chrome.Chrome(**kwargs)
80
81
82    def start_custom_chrome(self, kwargs):
83        """Start a custom Chrome with given arguments.
84
85        @param kwargs: A dict of keyword arguments passed to Chrome.
86
87        @return: True on success, False otherwise.
88
89        """
90        # Close the previous Chrome.
91        if self._chrome:
92            self._chrome.close()
93
94        # Start the new Chrome.
95        try:
96            self._chrome = self._start_chrome(kwargs)
97        except FacadeResoureError:
98            logging.error('Failed to start Chrome after retries')
99            return False
100        else:
101            logging.info('Chrome started successfully')
102
103        # The opened tabs are stored by tab descriptors.
104        # Key is the tab descriptor string.
105        # We use string as the key because of RPC Call. Client can use the
106        # string to locate the tab object.
107        # Value is the tab object.
108        self._tabs = dict()
109
110        # Workaround for issue crbug.com/588579.
111        # On daisy, Chrome freezes about 30 seconds after login because of
112        # TPM error. Avoid test accessing Chrome during this time.
113        # Check issue crbug.com/588579 and crbug.com/591646.
114        if utils.get_board() == 'daisy':
115            logging.warning('Delay 30s for issue 588579 on daisy')
116            time.sleep(30)
117
118        return True
119
120
121    def start_default_chrome(self, restart=False, extra_browser_args=None,
122                             disable_arc=False):
123        """Start the default Chrome.
124
125        @param restart: True to start Chrome without clearing previous state.
126        @param extra_browser_args: A list containing extra browser args passed
127                                   to Chrome. This list will be appened to
128                                   default EXTRA_BROWSER_ARGS.
129        @param disable_arc: True to disable ARC++.
130
131        @return: True on success, False otherwise.
132
133        """
134        # TODO: (crbug.com/618111) Add test driven switch for
135        # supporting arc_mode enabled or disabled. At this time
136        # if ARC build is tested, arc_mode is always enabled.
137        if not disable_arc and utils.get_board_property(self.ARC_VERSION):
138            arc_mode = self.ARC_ENABLED
139        else:
140            arc_mode = self.ARC_DISABLED
141        kwargs = {
142            'extension_paths': [constants.AUDIO_TEST_EXTENSION,
143                                constants.DISPLAY_TEST_EXTENSION],
144            'extra_browser_args': self.EXTRA_BROWSER_ARGS,
145            'clear_enterprise_policy': not restart,
146            'arc_mode': arc_mode,
147            'autotest_ext': True
148        }
149        if extra_browser_args:
150            kwargs['extra_browser_args'] += extra_browser_args
151        return self.start_custom_chrome(kwargs)
152
153
154    def __enter__(self):
155        return self
156
157
158    def __exit__(self, *args):
159        if self._chrome:
160            self._chrome.close()
161            self._chrome = None
162
163
164    @staticmethod
165    def _generate_tab_descriptor(tab):
166        """Generate tab descriptor by tab object.
167
168        @param tab: the tab object.
169        @return a str, the tab descriptor of the tab.
170
171        """
172        return hex(id(tab))
173
174
175    def clean_unexpected_tabs(self):
176        """Clean all tabs that are not opened by facade_resource
177
178        It is used to make sure our chrome browser is clean.
179
180        """
181        # If they have the same length we can assume there is no unexpected
182        # tabs.
183        browser_tabs = self.get_tabs()
184        if len(browser_tabs) == len(self._tabs):
185            return
186
187        for tab in browser_tabs:
188            if self._generate_tab_descriptor(tab) not in self._tabs:
189                # TODO(mojahsu): Reevaluate this code. crbug.com/719592
190                try:
191                    tab.Close()
192                except py_utils.TimeoutException:
193                    logging.warn('close tab timeout %r, %s', tab, tab.url)
194
195
196    @retry_chrome_call
197    def get_extension(self, extension_path=None):
198        """Gets the extension from the indicated path.
199
200        @param extension_path: the path of the target extension.
201                               Set to None to get autotest extension.
202                               Defaults to None.
203        @return an extension object.
204
205        @raise RuntimeError if the extension is not found.
206        @raise chrome.Error if the found extension has not yet been
207               retrieved succesfully.
208
209        """
210        try:
211            if extension_path is None:
212                extension = self._chrome.autotest_ext
213            else:
214                extension = self._chrome.get_extension(extension_path)
215        except KeyError as errmsg:
216            # Trigger retry_chrome_call to retry to retrieve the
217            # found extension.
218            raise chrome.Error(errmsg)
219        if not extension:
220            if extension_path is None:
221                raise RuntimeError('Autotest extension not found')
222            else:
223                raise RuntimeError('Extension not found in %r'
224                                    % extension_path)
225        return extension
226
227
228    def get_visible_notifications(self):
229        """Gets the visible notifications
230
231        @return: Returns all visible notifications in list format. Ex:
232                [{title:'', message:'', prority:'', id:''}]
233        """
234        return self._chrome.get_visible_notifications()
235
236
237    @retry_chrome_call
238    def load_url(self, url):
239        """Loads the given url in a new tab. The new tab will be active.
240
241        @param url: The url to load as a string.
242        @return a str, the tab descriptor of the opened tab.
243
244        """
245        tab = self._browser.tabs.New()
246        tab.Navigate(url)
247        tab.Activate()
248        tab.WaitForDocumentReadyStateToBeComplete()
249        tab_descriptor = self._generate_tab_descriptor(tab)
250        self._tabs[tab_descriptor] = tab
251        self.clean_unexpected_tabs()
252        return tab_descriptor
253
254
255    def set_http_server_directories(self, directories):
256        """Starts an HTTP server.
257
258        @param directories: Directories to start serving.
259
260        @return True on success. False otherwise.
261
262        """
263        return self._chrome.browser.platform.SetHTTPServerDirectories(directories)
264
265
266    def http_server_url_of(self, fullpath):
267        """Converts a path to a URL.
268
269        @param fullpath: String containing the full path to the content.
270
271        @return the URL for the provided path.
272
273        """
274        return self._chrome.browser.platform.http_server.UrlOf(fullpath)
275
276
277    def get_tabs(self):
278        """Gets the tabs opened by browser.
279
280        @returns: The tabs attribute in telemetry browser object.
281
282        """
283        return self._browser.tabs
284
285
286    def get_tab_by_descriptor(self, tab_descriptor):
287        """Gets the tab by the tab descriptor.
288
289        @returns: The tab object indicated by the tab descriptor.
290
291        """
292        return self._tabs[tab_descriptor]
293
294
295    @retry_chrome_call
296    def close_tab(self, tab_descriptor):
297        """Closes the tab.
298
299        @param tab_descriptor: Indicate which tab to be closed.
300
301        """
302        if tab_descriptor not in self._tabs:
303            raise RuntimeError('There is no tab for %s' % tab_descriptor)
304        tab = self._tabs[tab_descriptor]
305        del self._tabs[tab_descriptor]
306        tab.Close()
307        self.clean_unexpected_tabs()
308
309
310    def wait_for_javascript_expression(
311            self, tab_descriptor, expression, timeout):
312        """Waits for the given JavaScript expression to be True on the given tab
313
314        @param tab_descriptor: Indicate on which tab to wait for the expression.
315        @param expression: Indiate for what expression to wait.
316        @param timeout: Indicate the timeout of the expression.
317        """
318        if tab_descriptor not in self._tabs:
319            raise RuntimeError('There is no tab for %s' % tab_descriptor)
320        self._tabs[tab_descriptor].WaitForJavaScriptCondition(
321                expression, timeout=timeout)
322
323
324    def execute_javascript(self, tab_descriptor, statement, timeout):
325        """Executes a JavaScript statement on the given tab.
326
327        @param tab_descriptor: Indicate on which tab to execute the statement.
328        @param statement: Indiate what statement to execute.
329        @param timeout: Indicate the timeout of the statement.
330        """
331        if tab_descriptor not in self._tabs:
332            raise RuntimeError('There is no tab for %s' % tab_descriptor)
333        self._tabs[tab_descriptor].ExecuteJavaScript(
334                statement, timeout=timeout)
335
336
337    def evaluate_javascript(self, tab_descriptor, expression, timeout):
338        """Evaluates a JavaScript expression on the given tab.
339
340        @param tab_descriptor: Indicate on which tab to evaluate the expression.
341        @param expression: Indiate what expression to evaluate.
342        @param timeout: Indicate the timeout of the expression.
343        @return the JSONized result of the given expression
344        """
345        if tab_descriptor not in self._tabs:
346            raise RuntimeError('There is no tab for %s' % tab_descriptor)
347        return self._tabs[tab_descriptor].EvaluateJavaScript(
348                expression, timeout=timeout)
349