1# Copyright 2016 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#
5# arc_util.py is supposed to be called from chrome.py for ARC specific logic.
6# It should not import arc.py since it will create a import loop.
7
8import collections
9import logging
10import os
11import tempfile
12
13from autotest_lib.client.bin import utils
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib import file_utils
16from autotest_lib.client.common_lib.cros import arc_common
17from telemetry.core import exceptions
18from telemetry.internal.browser import extension_page
19
20_ARC_SUPPORT_HOST_URL = 'chrome-extension://cnbgggchhmkkdmeppjobngjoejnihlei/'
21_ARC_SUPPORT_HOST_PAGENAME = '_generated_background_page.html'
22_DUMPSTATE_PATH = '/var/log/arc-dumpstate.log'
23_USERNAME = 'crosarcplusplustest@gmail.com'
24_ARCP_URL = 'https://sites.google.com/a/chromium.org/dev/chromium-os' \
25                '/testing/arcplusplus-testing/arcp'
26_OPT_IN_BEGIN = 'Initializing ARC opt-in flow.'
27_OPT_IN_SKIP = 'Skip waiting provisioning completed.'
28_OPT_IN_FINISH = 'ARC opt-in flow complete.'
29
30_SIGN_IN_TIMEOUT = 120
31_SIGN_IN_CHECK_INTERVAL = 1
32
33
34# Describes ARC session state.
35# - provisioned is set to True once ARC is provisioned.
36# - tos_needed is set to True in case ARC Terms of Service need to be shown.
37#              This depends on ARC Terms of Service acceptance and policy for
38#              ARC preferences, such as backup and restore and use of location
39#              services.
40ArcSessionState = collections.namedtuple(
41    'ArcSessionState', ['provisioned', 'tos_needed'])
42
43
44def get_arc_session_state(autotest_ext):
45    """Returns the runtime state of ARC session.
46
47    @param autotest_ext private autotest API.
48
49    @raises error.TestFail in case autotest API failure or if ARC is not
50                           allowed for the device.
51
52    @return ArcSessionState that describes the state of current ARC session.
53
54    """
55    get_arc_state = '''
56        new Promise((resolve, reject) => {
57            chrome.autotestPrivate.getArcState((state) => {
58                if (chrome.runtime.lastError) {
59                    reject(new Error(chrome.runtime.lastError.message));
60                    return;
61                }
62                resolve(state);
63            });
64        })
65    '''
66    try:
67        state = autotest_ext.EvaluateJavaScript(get_arc_state, promise=True)
68        return ArcSessionState(state['provisioned'], state['tosNeeded'])
69    except exceptions.EvaluateException as e:
70        raise error.TestFail('Could not get ARC session state "%s".' % e)
71
72
73def _wait_arc_provisioned(autotest_ext):
74    """Waits until ARC provisioning is completed.
75
76    @param autotest_ext private autotest API.
77
78    @raises: error.TimeoutException if provisioning is not complete.
79
80    """
81    utils.poll_for_condition(
82            condition=lambda: get_arc_session_state(autotest_ext).provisioned,
83            desc='Wait for ARC is provisioned',
84            timeout=_SIGN_IN_TIMEOUT,
85            sleep_interval=_SIGN_IN_CHECK_INTERVAL)
86
87
88def should_start_arc(arc_mode):
89    """
90    Determines whether ARC should be started.
91
92    @param arc_mode: mode as defined in arc_common.
93
94    @returns: True or False.
95
96    """
97    logging.debug('ARC is enabled in mode %s', arc_mode)
98    assert arc_mode is None or arc_mode in arc_common.ARC_MODES
99    return arc_mode in [arc_common.ARC_MODE_ENABLED,
100                        arc_common.ARC_MODE_ENABLED_ASYNC]
101
102
103def post_processing_after_browser(chrome, timeout):
104    """
105    Called when a new browser instance has been initialized.
106
107    Note that this hook function is called regardless of arc_mode.
108
109    @param chrome: Chrome object.
110
111    """
112    # Remove any stale dumpstate files.
113    if os.path.isfile(_DUMPSTATE_PATH):
114        os.unlink(_DUMPSTATE_PATH)
115
116    # Wait for Android container ready if ARC is enabled.
117    if chrome.arc_mode == arc_common.ARC_MODE_ENABLED:
118        try:
119            arc_common.wait_for_android_boot(timeout)
120        except Exception:
121            # Save dumpstate so that we can figure out why boot does not
122            # complete.
123            _save_android_dumpstate()
124            raise
125
126
127def pre_processing_before_close(chrome):
128    """
129    Called when the browser instance is being closed.
130
131    Note that this hook function is called regardless of arc_mode.
132
133    @param chrome: Chrome object.
134
135    """
136    if not should_start_arc(chrome.arc_mode):
137        return
138    # TODO(b/29341443): Implement stopping of adb logcat when we start adb
139    # logcat for all tests
140
141    # Save dumpstate just before logout.
142    _save_android_dumpstate()
143
144
145def _save_android_dumpstate():
146    """
147    Triggers a dumpstate and saves its contents to to /var/log/arc-dumpstate.log
148    with logging.
149
150    Exception thrown while doing dumpstate will be ignored.
151    """
152
153    try:
154        logging.info('Saving Android dumpstate.')
155        with open(_DUMPSTATE_PATH, 'w') as out:
156            log = utils.system_output('android-sh -c arc-bugreport')
157            out.write(log)
158        logging.info('Android dumpstate successfully saved.')
159    except Exception:
160        # Dumpstate is nice-to-have stuff. Do not make it as a fatal error.
161        logging.exception('Failed to save Android dumpstate.')
162
163
164def get_test_account_info():
165    """Retrieve test account information."""
166    with tempfile.NamedTemporaryFile() as pltp:
167        file_utils.download_file(_ARCP_URL, pltp.name)
168        password = pltp.read().rstrip()
169    return (_USERNAME, password)
170
171
172def set_browser_options_for_opt_in(b_options):
173    """
174    Setup Chrome for gaia login and opt_in.
175
176    @param b_options: browser options object used by chrome.Chrome.
177
178    """
179    b_options.username, b_options.password = get_test_account_info()
180    b_options.disable_default_apps = False
181    b_options.disable_component_extensions_with_background_pages = False
182    b_options.gaia_login = True
183
184
185def enable_play_store(autotest_ext, enabled, enable_managed_policy=True):
186    """
187    Enable ARC++ Play Store
188
189    Do nothing if the account is managed and enable_managed_policy is set to
190    True.
191
192    @param autotest_ext: autotest extension object.
193
194    @param enabled: if True then perform opt-in, otherwise opt-out.
195
196    @param enable_managed_policy: If False then policy check is ignored for
197            managed user case and ARC++ is forced enabled or disabled.
198
199    @returns: True if the opt-in should continue; else False.
200
201    """
202
203    if autotest_ext is None:
204         raise error.TestFail(
205                 'Could not change the Play Store enabled state because '
206                 'autotest API does not exist')
207
208    # Skip enabling for managed users, since value is policy enforced.
209    # Return early if a managed user has ArcEnabled set to false.
210    get_play_store_state = '''
211            new Promise((resolve, reject) => {
212                chrome.autotestPrivate.getPlayStoreState((state) => {
213                    if (chrome.runtime.lastError) {
214                        reject(new Error(chrome.runtime.lastError.message));
215                        return;
216                    }
217                    resolve(state);
218                });
219            })
220    '''
221    try:
222        state = autotest_ext.EvaluateJavaScript(get_play_store_state,
223                                                promise=True)
224        if state['managed']:
225            # Handle managed case.
226            logging.info('Determined that ARC is managed by user policy.')
227            if enable_managed_policy:
228                if enabled == state['enabled']:
229                    return True
230                logging.info('Returning early since ARC is policy-enforced.')
231                return False
232            logging.info('Forcing ARC %s, ignore managed state.',
233                    ('enabled' if enabled else 'disabled'))
234
235        autotest_ext.ExecuteJavaScript('''
236                chrome.autotestPrivate.setPlayStoreEnabled(
237                    %s, function(enabled) {});
238            ''' % ('true' if enabled else 'false'))
239    except exceptions.EvaluateException as e:
240        raise error.TestFail('Could not change the Play Store enabled state '
241                             ' via autotest API. "%s".' % e)
242
243    return True
244
245
246def find_opt_in_extension_page(browser):
247    """
248    Find and verify the opt-in extension extension page.
249
250    @param browser: chrome.Chrome broswer object.
251
252    @returns: the extension page.
253
254    @raises: error.TestFail if extension is not found or is mal-formed.
255
256    """
257    opt_in_extension_id = extension_page.UrlToExtensionId(_ARC_SUPPORT_HOST_URL)
258    try:
259        extension_pages = browser.extensions.GetByExtensionId(
260            opt_in_extension_id)
261    except Exception, e:
262        raise error.TestFail('Could not locate extension for arc opt-in. '
263                             'Make sure disable_default_apps is False. '
264                             '"%s".' % e)
265
266    extension_main_page = None
267    for page in extension_pages:
268        url = page.EvaluateJavaScript('location.href;')
269        if url.endswith(_ARC_SUPPORT_HOST_PAGENAME):
270            extension_main_page = page
271            break
272    if not extension_main_page:
273        raise error.TestError('Found opt-in extension but not correct page!')
274    extension_main_page.WaitForDocumentReadyStateToBeComplete()
275
276    js_code_did_start_conditions = ['termsPage != null',
277            '(termsPage.isManaged_ || termsPage.state_ == LoadState.LOADED)']
278    try:
279        for condition in js_code_did_start_conditions:
280            extension_main_page.WaitForJavaScriptCondition(condition,
281                                                           timeout=60)
282    except Exception, e:
283        raise error.TestError('Error waiting for "%s": "%s".' % (condition, e))
284
285    return extension_main_page
286
287
288def opt_in_and_wait_for_completion(extension_main_page):
289    """
290    Step through the user input of the opt-in extension and wait for completion.
291
292    @param extension_main_page: opt-in extension object.
293
294    @raises error.TestFail if opt-in doesn't complete after timeout.
295
296    """
297    extension_main_page.ExecuteJavaScript('termsPage.onAgree()')
298
299    try:
300        extension_main_page.WaitForJavaScriptCondition('!appWindow',
301                                                       timeout=_SIGN_IN_TIMEOUT)
302    except Exception, e:
303        js_read_error_message = """
304            err = appWindow.contentWindow.document.getElementById(
305                    "error-message");
306            if (err) {
307                err.innerText;
308            }
309        """
310        err_msg = extension_main_page.EvaluateJavaScript(js_read_error_message)
311        err_msg = err_msg.strip()
312        logging.error('Error: %r', err_msg)
313        if err_msg:
314            raise error.TestFail('Opt-in app error: %r' % err_msg)
315        else:
316            raise error.TestFail('Opt-in app did not finish running after %s '
317                                 'seconds!' % _SIGN_IN_TIMEOUT)
318    # Reset termsPage to be able to reuse OptIn page and wait condition for ToS
319    # are loaded.
320    extension_main_page.ExecuteJavaScript('termsPage = null')
321
322
323def opt_in(browser,
324           autotest_ext,
325           enable_managed_policy=True,
326           wait_for_provisioning=True):
327    """
328    Step through opt in and wait for it to complete.
329
330    Return early if the arc_setting cannot be set True.
331
332    @param browser: chrome.Chrome browser object.
333    @param autotest_ext: autotest extension object.
334    @param enable_managed_policy: If False then policy check is ignored for
335            managed user case and ARC++ is forced enabled.
336    @param wait_for_provisioning: True in case we have to wait for provisioning
337                                       to complete.
338
339    @raises: error.TestFail if opt in fails.
340
341    """
342
343    logging.info(_OPT_IN_BEGIN)
344
345    # TODO(b/124391451): Enterprise tests have race, when ARC policy appears
346    # after profile prefs sync completed. That means they may appear as
347    # non-managed first, treated like this but finally turns as managed. This
348    # leads to test crash. Solution is to wait until prefs are synced or, if
349    # possible tune test accounts to wait sync is completed before starting the
350    # Chrome session.
351
352    # Enabling Play Store may affect this flag, capture it before.
353    tos_needed = get_arc_session_state(autotest_ext).tos_needed
354
355    if not enable_play_store(autotest_ext, True, enable_managed_policy):
356        return
357
358    if not wait_for_provisioning:
359        logging.info(_OPT_IN_SKIP)
360        return
361
362    if tos_needed:
363        extension_main_page = find_opt_in_extension_page(browser)
364        opt_in_and_wait_for_completion(extension_main_page)
365    else:
366        _wait_arc_provisioned(autotest_ext)
367
368    logging.info(_OPT_IN_FINISH)
369