xref: /aosp_15_r20/external/angle/build/fuchsia/test/browser_runner.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1# Copyright 2024 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Executes a browser with devtools enabled on the target."""
5
6import os
7import re
8import subprocess
9import tempfile
10import time
11from typing import List, Optional
12from urllib.parse import urlparse
13
14from common import run_continuous_ffx_command, ssh_run, REPO_ALIAS
15from ffx_integration import run_symbolizer
16
17WEB_ENGINE_SHELL = 'web-engine-shell'
18CAST_STREAMING_SHELL = 'cast-streaming-shell'
19
20
21class BrowserRunner:
22    """Manages the browser process on the target."""
23
24    def __init__(self,
25                 browser_type: str,
26                 target_id: Optional[str] = None,
27                 output_dir: Optional[str] = None):
28        self._browser_type = browser_type
29        assert self._browser_type in [WEB_ENGINE_SHELL, CAST_STREAMING_SHELL]
30        self._target_id = target_id
31        self._output_dir = output_dir or os.environ['CHROMIUM_OUTPUT_DIR']
32        assert self._output_dir
33        self._browser_proc = None
34        self._symbolizer_proc = None
35        self._devtools_port = None
36        self._log_fs = None
37
38        output_root = os.path.join(self._output_dir, 'gen', 'fuchsia_web')
39        if self._browser_type == WEB_ENGINE_SHELL:
40            self._id_files = [
41                os.path.join(output_root, 'shell', 'web_engine_shell',
42                             'ids.txt'),
43                os.path.join(output_root, 'webengine', 'web_engine_with_webui',
44                             'ids.txt'),
45            ]
46        else:  # self._browser_type == CAST_STREAMING_SHELL:
47            self._id_files = [
48                os.path.join(output_root, 'shell', 'cast_streaming_shell',
49                             'ids.txt'),
50                os.path.join(output_root, 'webengine', 'web_engine',
51                             'ids.txt'),
52            ]
53
54    @property
55    def browser_type(self) -> str:
56        """Returns the type of the browser for the tests."""
57        return self._browser_type
58
59    @property
60    def devtools_port(self) -> int:
61        """Returns the randomly assigned devtools-port, shouldn't be called
62        before executing the start."""
63        assert self._devtools_port
64        return self._devtools_port
65
66    @property
67    def log_file(self) -> str:
68        """Returns the log file of the browser instance, shouldn't be called
69        before executing the start."""
70        assert self._log_fs
71        return self._log_fs.name
72
73    @property
74    def browser_pid(self) -> int:
75        """Returns the process id of the ffx instance which starts the browser
76        on the test device, shouldn't be called before executing the start."""
77        assert self._browser_proc
78        return self._browser_proc.pid
79
80    def _read_devtools_port(self):
81        search_regex = r'DevTools listening on (.+)'
82
83        # The ipaddress of the emulator or device is preferred over the address
84        # reported by the devtools, former one is usually more accurate.
85        def try_reading_port(log_file) -> int:
86            for line in log_file:
87                tokens = re.search(search_regex, line)
88                if tokens:
89                    url = urlparse(tokens.group(1))
90                    assert url.scheme == 'ws'
91                    assert url.port is not None
92                    return url.port
93            return None
94
95        with open(self.log_file, encoding='utf-8') as log_file:
96            start = time.time()
97            while time.time() - start < 180:
98                port = try_reading_port(log_file)
99                if port:
100                    return port
101                self._browser_proc.poll()
102                assert not self._browser_proc.returncode, 'Browser stopped.'
103                time.sleep(1)
104            assert False, 'Failed to wait for the devtools port.'
105
106    def start(self, extra_args: List[str] = None) -> None:
107        """Starts the selected browser, |extra_args| are attached to the command
108        line."""
109        browser_cmd = ['test', 'run']
110        if self.browser_type == WEB_ENGINE_SHELL:
111            browser_cmd.extend([
112                f'fuchsia-pkg://{REPO_ALIAS}/web_engine_shell#meta/'
113                f'web_engine_shell.cm',
114                '--',
115                '--web-engine-package-name=web_engine_with_webui',
116                '--remote-debugging-port=0',
117                '--enable-web-instance-tmp',
118                '--with-webui',
119                'about:blank',
120            ])
121        else:  # if self.browser_type == CAST_STREAMING_SHELL:
122            browser_cmd.extend([
123                f'fuchsia-pkg://{REPO_ALIAS}/cast_streaming_shell#meta/'
124                f'cast_streaming_shell.cm',
125                '--',
126                '--remote-debugging-port=0',
127            ])
128        # Use flags used on WebEngine in production devices.
129        browser_cmd.extend([
130            '--',
131            '--enable-low-end-device-mode',
132            '--force-gpu-mem-available-mb=64',
133            '--force-gpu-mem-discardable-limit-mb=32',
134            '--force-max-texture-size=2048',
135            '--gpu-rasterization-msaa-sample-count=0',
136            '--min-height-for-gpu-raster-tile=128',
137            '--webgl-msaa-sample-count=0',
138            '--max-decoded-image-size-mb=10',
139        ])
140        if extra_args:
141            browser_cmd.extend(extra_args)
142        self._browser_proc = run_continuous_ffx_command(
143            cmd=browser_cmd,
144            stdout=subprocess.PIPE,
145            stderr=subprocess.STDOUT,
146            target_id=self._target_id)
147        # The stdout will be forwarded to the symbolizer, then to the _log_fs.
148        self._log_fs = tempfile.NamedTemporaryFile()
149        self._symbolizer_proc = run_symbolizer(self._id_files,
150                                               self._browser_proc.stdout,
151                                               self._log_fs)
152        self._devtools_port = self._read_devtools_port()
153
154    def stop_browser(self) -> None:
155        """Stops the browser on the target, as well as the local symbolizer, the
156        _log_fs is preserved. Calling this function for a second time won't have
157        any effect."""
158        if not self.is_browser_running():
159            return
160        self._browser_proc.kill()
161        self._browser_proc = None
162        self._symbolizer_proc.kill()
163        self._symbolizer_proc = None
164        self._devtools_port = None
165        # The process may be stopped already, ignoring the no process found
166        # error.
167        ssh_run(['killall', 'web_instance.cmx'], self._target_id, check=False)
168
169    def is_browser_running(self) -> bool:
170        """Checks if the browser is still running."""
171        if self._browser_proc:
172            assert self._symbolizer_proc
173            assert self._devtools_port
174            return True
175        assert not self._symbolizer_proc
176        assert not self._devtools_port
177        return False
178
179    def close(self) -> None:
180        """Cleans up everything."""
181        self.stop_browser()
182        self._log_fs.close()
183        self._log_fs = None
184