xref: /aosp_15_r20/external/autotest/server/cros/tradefed/tradefed_chromelogin.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2018 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
6import contextlib
7import logging
8import os
9
10from autotest_lib.client.common_lib import error
11from autotest_lib.server import autotest
12from autotest_lib.server.cros.tradefed import tradefed_constants as constants
13
14
15class ChromeLogin(object):
16    """Context manager to handle Chrome login state."""
17
18    def need_reboot(self, hard_reboot=False):
19        """Marks state as "dirty" - reboot needed during/after test."""
20        logging.info('Will reboot DUT when Chrome stops.')
21        self._need_reboot = True
22        if hard_reboot and self._host.servo:
23            self._hard_reboot_on_failure = True
24
25    def __init__(self,
26                 host,
27                 board=None,
28                 dont_override_profile=False,
29                 enable_default_apps=False,
30                 toggle_ndk=False,
31                 log_dir=None):
32        """Initializes the ChromeLogin object.
33
34        @param board: optional parameter to extend timeout for login for slow
35                      DUTs. Used in particular for virtual machines.
36        @param dont_override_profile: reuses the existing test profile if any
37        @param enable_default_apps: enables default apps (like Files app)
38        @param toggle_ndk: toggles native bridge engine switch.
39        @param log_dir: Any log files for this Chrome session is written to this
40               directory.
41        """
42        self._host = host
43        self._timeout = constants.LOGIN_BOARD_TIMEOUT.get(
44            board, constants.LOGIN_DEFAULT_TIMEOUT)
45        self._dont_override_profile = dont_override_profile
46        self._enable_default_apps = enable_default_apps
47        self._need_reboot = False
48        self._hard_reboot_on_failure = False
49        self._toggle_ndk = toggle_ndk
50        self._log_dir = log_dir
51
52    def _cmd_builder(self, verbose=False):
53        """Gets remote command to start browser with ARC enabled."""
54        # If autotest is not installed on the host, as with moblab at times,
55        # getting the autodir will raise an exception.
56        cmd = autotest.Autotest.get_installed_autodir(self._host)
57        cmd += '/bin/autologin.py --arc'
58
59        # We want to suppress the Google doodle as it is not part of the image
60        # and can be different content every day interacting with testing.
61        cmd += ' --no-startup-window'
62        # Disable several forms of auto-installation to stablize the tests.
63        cmd += ' --no-arc-syncs'
64        # TODO(b/196460968) delete after M96 branch, or after finishing the
65        # experiment.
66        cmd += ' --feature=NotificationsRefresh'
67        # Toggle the translation from houdini to ndk
68        if self._toggle_ndk:
69            cmd += ' --toggle_ndk'
70        if self._dont_override_profile:
71            logging.info('Starting Chrome with a possibly reused profile.')
72            cmd += ' --dont_override_profile'
73        else:
74            logging.info('Starting Chrome with a fresh profile.')
75        if self._enable_default_apps:
76            logging.info('Using --enable_default_apps to start Chrome.')
77            cmd += ' --enable_default_apps'
78        if not verbose:
79            cmd += ' > /dev/null 2>&1'
80        return cmd
81
82    def _login_by_script(self, timeout, verbose):
83        """Runs the autologin.py script on the DUT to log in."""
84        self._host.run(
85            self._cmd_builder(verbose=verbose),
86            ignore_status=False,
87            verbose=verbose,
88            timeout=timeout)
89
90    def _login(self, timeout, verbose=False, install_autotest=False):
91        """Logs into Chrome. Raises an exception on failure."""
92        if not install_autotest:
93            try:
94                # Assume autotest to be already installed.
95                self._login_by_script(timeout=timeout, verbose=verbose)
96            except autotest.AutodirNotFoundError:
97                logging.warning('Autotest not installed, forcing install...')
98                install_autotest = True
99
100        if install_autotest:
101            # Installs the autotest client to the DUT by running a no-op test.
102            autotest.Autotest(self._host).run_timed_test(
103                    'stub_Pass', timeout=2 * timeout, check_client_result=True)
104            # The (re)run the login script.
105            self._login_by_script(timeout=timeout, verbose=verbose)
106
107        # Quick check if Android has really started. When autotest client
108        # installed on the DUT was partially broken, the script may succeed
109        # without actually logging into Chrome/Android. See b/129382439.
110        self._host.run(
111            # "/data/anr" is an arbitrary directory accessible only after
112            # proper login and data mount.
113            'android-sh -c "ls /data/anr"',
114            ignore_status=False, timeout=9)
115
116    def enter(self):
117        """Logs into Chrome with retry."""
118        timeout = self._timeout
119        try:
120            logging.info('Ensure Android is running (timeout=%d)...', timeout)
121            self._login(timeout=timeout)
122        except Exception as e:
123            logging.error('Login failed.', exc_info=e)
124            # Retry with more time, with refreshed client autotest installation,
125            # and the DUT cleanup by rebooting. This can hide some failures.
126            self._reboot()
127            timeout *= 2
128            logging.info('Retrying failed login (timeout=%d)...', timeout)
129            try:
130                self._login(timeout=timeout,
131                            verbose=True,
132                            install_autotest=True)
133            except Exception as e2:
134                logging.error('Failed to login to Chrome', exc_info=e2)
135                raise error.TestError('Failed to login to Chrome')
136
137    def exit(self):
138        """On exit restart the browser or reboot the machine.
139
140        If self._log_dir is set, the VM kernel log is written
141        to a file.
142
143        """
144        if self._log_dir:
145            self._write_kernel_log()
146
147        if not self._need_reboot:
148            try:
149                self._restart()
150            except:
151                logging.error('Restarting browser has failed.')
152                self.need_reboot()
153        if self._need_reboot:
154            self._reboot()
155
156    def _write_kernel_log(self):
157        """Writes ARCVM kernel logs."""
158        if not os.path.exists(self._log_dir):
159            os.makedirs(self._log_dir)
160
161        output_path = os.path.join(
162                self._log_dir, '%s_vm_pstore_dump.txt' % self._host.hostname)
163
164        with open(output_path, 'w') as f:
165            try:
166                logging.info('Getting VM kernel logs.')
167                self._host.run('/usr/bin/vm_pstore_dump', stdout_tee=f)
168            except Exception as e:
169                logging.error('vm_pstore_dump command failed: %s', e)
170            else:
171                logging.info('Wrote VM kernel logs.')
172
173    def _restart(self):
174        """Restart Chrome browser."""
175        # We clean up /tmp (which is memory backed) from crashes and
176        # other files. A reboot would have cleaned /tmp as well.
177        # TODO(ihf): Remove "start ui" which is a nicety to non-ARC tests (i.e.
178        # now we wait on login screen, but login() above will 'stop ui' again
179        # before launching Chrome with ARC enabled).
180        logging.info('Skipping reboot, restarting browser.')
181        script = 'stop ui'
182        script += '&& find /tmp/ -mindepth 1 -delete '
183        script += '&& start ui'
184        self._host.run(script, ignore_status=False, verbose=False, timeout=120)
185
186    def _reboot(self):
187        """Reboot the machine."""
188        if self._hard_reboot_on_failure:
189            logging.info('Powering OFF the DUT: %s', self._host)
190            self._host.servo.get_power_state_controller().power_off()
191            logging.info('Powering ON the DUT: %s', self._host)
192            self._host.servo.get_power_state_controller().power_on()
193        else:
194            logging.info('Rebooting...')
195            self._host.reboot()
196
197
198@contextlib.contextmanager
199def login_chrome(hosts, **kwargs):
200    """Returns Chrome log-in context manager for multiple hosts. """
201    # TODO(pwang): Chromelogin takes 10+ seconds for it to successfully
202    #              enter. Parallelize if this becomes a bottleneck.
203    instances = [ChromeLogin(host, **kwargs) for host in hosts]
204    for instance in instances:
205        instance.enter()
206    yield instances
207    for instance in instances:
208        instance.exit()
209