1# Copyright 2022 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 5from __future__ import annotations 6 7import argparse 8import logging 9import re 10from typing import TYPE_CHECKING, Optional, TextIO, Tuple, cast 11 12from crossbench import path as pth 13from crossbench import plt 14from crossbench.browsers.attributes import BrowserAttributes 15from crossbench.browsers.browser import Browser 16from crossbench.browsers.browser_helper import convert_flags_to_label 17from crossbench.browsers.viewport import Viewport 18from crossbench.flags.chrome import ChromeFeatures, ChromeFlags 19from crossbench.types import JsonDict 20 21if TYPE_CHECKING: 22 from crossbench.browsers.settings import Settings 23 from crossbench.flags.base import Flags, FlagsData 24 from crossbench.flags.js_flags import JSFlags 25 from crossbench.runner.groups.session import BrowserSessionRunGroup 26 27 28class Chromium(Browser): 29 MIN_HEADLESS_NEW_VERSION: int = 112 30 DEFAULT_FLAGS: Tuple[str, ...] = ( 31 "--no-default-browser-check", 32 "--disable-component-update", 33 "--disable-sync", 34 "--disable-extensions", 35 "--no-first-run", 36 # This could be enabled via feature-flags as well. 37 "--disable-search-engine-choice-screen", 38 ) 39 FLAGS_FOR_DISABLING_BACKGROUND_INTERVENTIONS: Tuple[str, ...] = ( 40 "--disable-background-timer-throttling", 41 "--disable-renderer-backgrounding", 42 ) 43 # All flags that might affect how finch / field-trials are loaded. 44 FIELD_TRIAL_FLAGS: Tuple[str, ...] = ( 45 "--force-fieldtrials", 46 "--variations-server-url", 47 "--variations-insecure-server-url", 48 "--variations-test-seed-path", 49 "--enable-field-trial-config", 50 "--disable-variations-safe-mode", 51 ) 52 NO_EXPERIMENTS_FLAGS: Tuple[str, ...] = ( 53 "--no-experiments", 54 "--enable-benchmarking", 55 "--disable-field-trial-config", 56 ) 57 58 @classmethod 59 def default_path(cls, platform: plt.Platform) -> pth.AnyPath: 60 return platform.search_app_or_executable( 61 "Chromium", 62 macos=["Chromium.app"], 63 linux=["google-chromium", "chromium"], 64 win=["Google/Chromium/Application/chromium.exe"]) 65 66 @classmethod 67 def default_flags(cls, initial_data: FlagsData = None) -> ChromeFlags: 68 return ChromeFlags(initial_data) 69 70 def __init__(self, 71 label: str, 72 path: pth.AnyPath, 73 settings: Optional[Settings] = None): 74 super().__init__(label, path, settings=settings) 75 self._stdout_log_file: Optional[TextIO] = None 76 assert isinstance(self._flags, ChromeFlags) 77 78 def _setup_flags(self, settings: Settings) -> ChromeFlags: 79 flags: Flags = settings.flags 80 js_flags: Flags = settings.js_flags 81 self._flags = self.default_flags(self.DEFAULT_FLAGS) 82 self._flags.update(flags) 83 84 if "--allow-background-interventions" in self._flags.data: 85 # The --allow-background-interventions flag should have no value. 86 assert self._flags.get("--allow-background-interventions") is None 87 else: 88 self._flags.update(self.FLAGS_FOR_DISABLING_BACKGROUND_INTERVENTIONS) 89 90 # Explicitly disable field-trials by default on all chrome flavours: 91 # By default field-trials are enabled on non-Chrome branded builds, but 92 # are auto-enabled on everything else. This gives very confusing results 93 # when comparing local builds to official binaries. 94 field_trial_flags = [ 95 flag for flag in self.FIELD_TRIAL_FLAGS if flag in self._flags 96 ] 97 if not field_trial_flags: 98 logging.info("Disabling experiments/finch/field-trials for %s", self) 99 for flag in self.NO_EXPERIMENTS_FLAGS: 100 self._flags.set(flag) 101 else: 102 logging.warning("Running with field-trials or finch experiments.") 103 no_finch_flags = [ 104 flag for flag in self.NO_EXPERIMENTS_FLAGS if flag in self._flags 105 ] 106 if no_finch_flags: 107 raise argparse.ArgumentTypeError( 108 "Conflicting flag groups set: " 109 f"{field_trial_flags} vs {no_finch_flags}.\n" 110 "Cannot enable and disable finch / field-trials at the same time.") 111 112 self.js_flags.update(js_flags) 113 self._maybe_disable_gpu_compositing() 114 return self._flags 115 116 def _maybe_disable_gpu_compositing(self) -> None: 117 # Chrome Remote Desktop provide no GPU and older chrome versions 118 # don't handle this well. 119 if self.major_version > 92 or ("CHROME_REMOTE_DESKTOP_SESSION" 120 not in self.platform.environ): 121 return 122 self.flags.set("--disable-gpu-compositing") 123 self.flags.set("--no-sandbox") 124 125 def _setup_cache_dir(self, settings: Settings) -> None: 126 cache_dir = settings.cache_dir 127 if cache_dir is None: 128 maybe_cache_dir = self._flags.get("--user-data-dir", None) 129 if maybe_cache_dir: 130 cache_dir = pth.AnyPath(maybe_cache_dir) 131 if cache_dir is None: 132 self.cache_dir = self.platform.mkdtemp(prefix=self.type_name) 133 self.clear_cache_dir = True 134 else: 135 self.cache_dir = cache_dir 136 self.clear_cache_dir = False 137 138 def _extract_version(self) -> str: 139 assert self.path 140 version_string = self.platform.app_version(self.path) 141 # Sample output: "Chromium 90.0.4430.212 dev" => "90.0.4430.212" 142 matches = re.findall(r"[\d\.]+", version_string) 143 if not matches: 144 raise ValueError( 145 f"Could not extract version number from '{version_string}' " 146 f"for '{self.path}'") 147 return str(matches[0]) 148 149 @property 150 def type_name(self) -> str: 151 return "chromium" 152 153 @property 154 def attributes(self) -> BrowserAttributes: 155 return BrowserAttributes.CHROMIUM | BrowserAttributes.CHROMIUM_BASED 156 157 @property 158 def is_headless(self) -> bool: 159 return "--headless" in self._flags 160 161 @property 162 def chrome_log_file(self) -> pth.AnyPath: 163 assert self.log_file 164 return self.log_file.with_suffix(f".{self.type_name}.log") 165 166 @property 167 def flags(self) -> ChromeFlags: 168 return cast(ChromeFlags, self._flags) 169 170 @property 171 def js_flags(self) -> JSFlags: 172 return cast(ChromeFlags, self._flags).js_flags 173 174 @property 175 def features(self) -> ChromeFeatures: 176 return cast(ChromeFlags, self._flags).features 177 178 def details_json(self) -> JsonDict: 179 details: JsonDict = super().details_json() 180 if self.log_file: 181 log = cast(JsonDict, details["log"]) 182 log[self.type_name] = str(self.chrome_log_file) 183 log["stdout"] = str(self.stdout_log_file) 184 details["js_flags"] = tuple(self.js_flags) 185 return details 186 187 def _get_browser_flags_for_session( 188 self, session: BrowserSessionRunGroup) -> Tuple[str, ...]: 189 js_flags_copy = self.js_flags.copy() 190 js_flags_copy.update(session.extra_js_flags) 191 192 flags_copy = self.flags.copy() 193 flags_copy.update(session.extra_flags) 194 flags_copy.update(self.network.extra_flags(self.attributes)) 195 self._handle_viewport_flags(flags_copy) 196 197 if len(js_flags_copy): 198 flags_copy["--js-flags"] = str(js_flags_copy) 199 if user_data_dir := self.flags.get("--user-data-dir"): 200 assert user_data_dir == str( 201 self.cache_dir), (f"--user-data-dir path: {user_data_dir} was passed " 202 f"but does not match cache-dir: {self.cache_dir}") 203 if self.cache_dir: 204 flags_copy["--user-data-dir"] = str(self.cache_dir) 205 if self.log_file: 206 flags_copy.set("--enable-logging") 207 flags_copy["--log-file"] = str(self.chrome_log_file) 208 209 flags_copy = self._filter_flags_for_run(flags_copy) 210 211 return tuple(flags_copy) 212 213 def _handle_viewport_flags(self, flags: Flags) -> None: 214 self._sync_viewport_flag(flags, "--start-fullscreen", 215 self.viewport.is_fullscreen, Viewport.FULLSCREEN) 216 self._sync_viewport_flag(flags, "--start-maximized", 217 self.viewport.is_maximized, Viewport.MAXIMIZED) 218 self._sync_viewport_flag(flags, "--headless", self.viewport.is_headless, 219 Viewport.HEADLESS) 220 # M112 added --headless=new as replacement for --headless 221 if "--headless" in flags and (self.major_version >= 222 self.MIN_HEADLESS_NEW_VERSION): 223 if flags["--headless"] is None: 224 logging.info("Replacing --headless with --headless=new") 225 flags.set("--headless", "new", override=True) 226 227 if self.viewport.is_default: 228 update_viewport = False 229 width, height = self.viewport.size 230 x, y = self.viewport.position 231 if "--window-size" in flags: 232 update_viewport = True 233 width, height = map(int, flags["--window-size"].split(",")) 234 if "--window-position" in flags: 235 update_viewport = True 236 x, y = map(int, flags["--window-position"].split(",")) 237 if update_viewport: 238 self.viewport = Viewport(width, height, x, y) 239 if self.viewport.has_size: 240 flags["--window-size"] = f"{self.viewport.width},{self.viewport.height}" 241 flags["--window-position"] = f"{self.viewport.x},{self.viewport.y}" 242 else: 243 for flag in ("--window-position", "--window-size"): 244 if flag in flags: 245 flag_value = flags[flag] 246 raise ValueError(f"Viewport {self.viewport} conflicts with flag " 247 f"{flag}={flag_value}") 248 249 def get_label_from_flags(self) -> str: 250 return convert_flags_to_label(*self.flags, *self.js_flags) 251 252 def quit(self) -> None: 253 super().quit() 254 if self._stdout_log_file: 255 self._stdout_log_file.close() 256 self._stdout_log_file = None 257