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