# Copyright 2024 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Executes a browser with devtools enabled on the target.""" import os import re import subprocess import tempfile import time from typing import List, Optional from urllib.parse import urlparse from common import run_continuous_ffx_command, ssh_run, REPO_ALIAS from ffx_integration import run_symbolizer WEB_ENGINE_SHELL = 'web-engine-shell' CAST_STREAMING_SHELL = 'cast-streaming-shell' class BrowserRunner: """Manages the browser process on the target.""" def __init__(self, browser_type: str, target_id: Optional[str] = None, output_dir: Optional[str] = None): self._browser_type = browser_type assert self._browser_type in [WEB_ENGINE_SHELL, CAST_STREAMING_SHELL] self._target_id = target_id self._output_dir = output_dir or os.environ['CHROMIUM_OUTPUT_DIR'] assert self._output_dir self._browser_proc = None self._symbolizer_proc = None self._devtools_port = None self._log_fs = None output_root = os.path.join(self._output_dir, 'gen', 'fuchsia_web') if self._browser_type == WEB_ENGINE_SHELL: self._id_files = [ os.path.join(output_root, 'shell', 'web_engine_shell', 'ids.txt'), os.path.join(output_root, 'webengine', 'web_engine_with_webui', 'ids.txt'), ] else: # self._browser_type == CAST_STREAMING_SHELL: self._id_files = [ os.path.join(output_root, 'shell', 'cast_streaming_shell', 'ids.txt'), os.path.join(output_root, 'webengine', 'web_engine', 'ids.txt'), ] @property def browser_type(self) -> str: """Returns the type of the browser for the tests.""" return self._browser_type @property def devtools_port(self) -> int: """Returns the randomly assigned devtools-port, shouldn't be called before executing the start.""" assert self._devtools_port return self._devtools_port @property def log_file(self) -> str: """Returns the log file of the browser instance, shouldn't be called before executing the start.""" assert self._log_fs return self._log_fs.name @property def browser_pid(self) -> int: """Returns the process id of the ffx instance which starts the browser on the test device, shouldn't be called before executing the start.""" assert self._browser_proc return self._browser_proc.pid def _read_devtools_port(self): search_regex = r'DevTools listening on (.+)' # The ipaddress of the emulator or device is preferred over the address # reported by the devtools, former one is usually more accurate. def try_reading_port(log_file) -> int: for line in log_file: tokens = re.search(search_regex, line) if tokens: url = urlparse(tokens.group(1)) assert url.scheme == 'ws' assert url.port is not None return url.port return None with open(self.log_file, encoding='utf-8') as log_file: start = time.time() while time.time() - start < 180: port = try_reading_port(log_file) if port: return port self._browser_proc.poll() assert not self._browser_proc.returncode, 'Browser stopped.' time.sleep(1) assert False, 'Failed to wait for the devtools port.' def start(self, extra_args: List[str] = None) -> None: """Starts the selected browser, |extra_args| are attached to the command line.""" browser_cmd = ['test', 'run'] if self.browser_type == WEB_ENGINE_SHELL: browser_cmd.extend([ f'fuchsia-pkg://{REPO_ALIAS}/web_engine_shell#meta/' f'web_engine_shell.cm', '--', '--web-engine-package-name=web_engine_with_webui', '--remote-debugging-port=0', '--enable-web-instance-tmp', '--with-webui', 'about:blank', ]) else: # if self.browser_type == CAST_STREAMING_SHELL: browser_cmd.extend([ f'fuchsia-pkg://{REPO_ALIAS}/cast_streaming_shell#meta/' f'cast_streaming_shell.cm', '--', '--remote-debugging-port=0', ]) # Use flags used on WebEngine in production devices. browser_cmd.extend([ '--', '--enable-low-end-device-mode', '--force-gpu-mem-available-mb=64', '--force-gpu-mem-discardable-limit-mb=32', '--force-max-texture-size=2048', '--gpu-rasterization-msaa-sample-count=0', '--min-height-for-gpu-raster-tile=128', '--webgl-msaa-sample-count=0', '--max-decoded-image-size-mb=10', ]) if extra_args: browser_cmd.extend(extra_args) self._browser_proc = run_continuous_ffx_command( cmd=browser_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, target_id=self._target_id) # The stdout will be forwarded to the symbolizer, then to the _log_fs. self._log_fs = tempfile.NamedTemporaryFile() self._symbolizer_proc = run_symbolizer(self._id_files, self._browser_proc.stdout, self._log_fs) self._devtools_port = self._read_devtools_port() def stop_browser(self) -> None: """Stops the browser on the target, as well as the local symbolizer, the _log_fs is preserved. Calling this function for a second time won't have any effect.""" if not self.is_browser_running(): return self._browser_proc.kill() self._browser_proc = None self._symbolizer_proc.kill() self._symbolizer_proc = None self._devtools_port = None # The process may be stopped already, ignoring the no process found # error. ssh_run(['killall', 'web_instance.cmx'], self._target_id, check=False) def is_browser_running(self) -> bool: """Checks if the browser is still running.""" if self._browser_proc: assert self._symbolizer_proc assert self._devtools_port return True assert not self._symbolizer_proc assert not self._devtools_port return False def close(self) -> None: """Cleans up everything.""" self.stop_browser() self._log_fs.close() self._log_fs = None