tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_runner.py (7078B)


      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 
      6 import os
      7 import re
      8 import subprocess
      9 import tempfile
     10 import time
     11 from typing import List, Optional
     12 from urllib.parse import urlparse
     13 
     14 from common import run_continuous_ffx_command, ssh_run, REPO_ALIAS
     15 from ffx_integration import run_symbolizer
     16 
     17 WEB_ENGINE_SHELL = 'web-engine-shell'
     18 CAST_STREAMING_SHELL = 'cast-streaming-shell'
     19 
     20 
     21 class 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