# Copyright 2021 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import abc import logging import os import plistlib import subprocess import time import typing import utils class BrowserDriver(abc.ABC): """Abstract Base Class encapsulating browser setup and tear down. """ def __init__(self, browser_name: str, process_name: str): self.name = browser_name self.process_name = process_name self.browser_process = None # LaunchServices can get confused when an application is launched from # more than one location and break AppleScript commands. Always launch # browsers from /Applications to avoid such problems. self.executable_path = (os.path.join("/Applications", f"{self.process_name}.app")) if not os.path.exists(self.executable_path): raise ValueError(f"Application doesn't exist for {browser_name}.") @abc.abstractmethod def Launch(self): """Starts the browser and ensures it is started before returning. """ pass def TearDown(self): """Terminates the browser and ensures it's cleaned up before returning. """ logging.debug(f"Tearing down {self.process_name}") if self.browser_process: utils.TerminateProcess(self.browser_process) def GetApplicationInfo(self) -> typing.Dict: """ Returns the Info.plist data in the application folder. """ plist_path = os.path.join(self.executable_path, "Contents", "Info.plist") with open(plist_path, 'rb') as plist_file: return plistlib.load(plist_file) def Summary(self): """Returns a dictionary describing the browser. """ info = self.GetApplicationInfo() return { 'name': self.name, 'version': info['CFBundleShortVersionString'], 'identifier': info['CFBundleIdentifier'] } def _EnsureStarted(self): """Waits until a browser with the given `process_name` is running. """ while not self.browser_process: self.browser_process = utils.FindProcess(self.process_name) time.sleep(0.100) logging.debug(f"Waiting for {self.process_name} to start") logging.debug(f"{self.process_name} started") class SafariDriver(BrowserDriver): def __init__(self, extra_args=[]): super().__init__("safari", "Safari") self.extra_args = extra_args def Launch(self): subprocess.call(["open", "-a", "Safari"]) # Call prep_safari.scpt to make sure the run starts clean. See file # comment for details. subprocess.call([ "osascript", os.path.join(os.path.dirname(__file__), "driver_scripts_templates", "prep_safari.scpt") ]) subprocess.call(["open", "-a", "Safari", "--args"] + self.extra_args) self._EnsureStarted() class ChromiumDriver(BrowserDriver): def __init__(self, browser_name: str, variation: str, process_name: str, extra_args=[]): if variation != "": browser_name += f"_{variation}" super().__init__(browser_name, process_name) self.extra_args = extra_args def Launch(self): open_args = ["-a", self.process_name] subprocess.call(["open"] + open_args + ["--args"] + [ "--enable-benchmarking", "--disable-stack-profiler", "--no-first-run", "--no-default-browser-check" ] + self.extra_args) self._EnsureStarted() def Summary(self): """Returns a dictionary describing the browser. """ info = self.GetApplicationInfo() return { 'name': self.name, 'identifier': info['CFBundleIdentifier'], 'version': info['CFBundleShortVersionString'], 'commit': info['SCMRevision'], 'extra_args': self.extra_args } # Helper functions to get default browser configurations. def Safari(): return SafariDriver() def Chrome(variation, extra_args=[]): return ChromiumDriver("chrome", variation, "Google Chrome", extra_args=extra_args) def Canary(variation, extra_args=[]): return ChromiumDriver("canary", variation, "Google Chrome Canary", extra_args=extra_args) def Chromium(variation, extra_args=[]): return ChromiumDriver("chromium", variation, "Chromium", extra_args=extra_args) def Edge(variation, extra_args=[]): return ChromiumDriver("edge", variation, "Microsoft Edge", extra_args=extra_args) PROCESS_NAMES = [ "Safari", "Google Chrome", "Google Chrome Canary", "Chromium", "Microsoft Edge" ] def MakeBrowserDriver(browser_name: str, variation: str, chrome_user_dir=None, output_dir=None, tracing_mode=False, extra_command_line=None) -> BrowserDriver: """Creates browser driver by name. Args: browser_name: Identifier for the browser to create. Supported browsers are: safari, chrome and chromium. chrome_user_dir: Optional user directory path to use for chrome. """ if "safari" == browser_name: return Safari() if browser_name in ["chrome", "chromium", "canary", "edge"]: if chrome_user_dir: chrome_extra_arg = [f"--user-data-dir={chrome_user_dir}"] else: chrome_extra_arg = ["--guest"] if variation == 'AlignWakeUps': chrome_extra_arg += ['--enable-features=AlignWakeUps'] if tracing_mode: chrome_extra_arg += [ '--enable-tracing=toplevel,toplevel.flow,mojom,navigation' ] trace_path = os.path.join(output_dir, "chrometrace.log") chrome_extra_arg += [ f'--trace-startup-file={os.path.abspath(trace_path)}' ] if extra_command_line: for command in extra_command_line: # Quotes are needed to avoid to avoid cli replacement. command = command.replace('"', '') chrome_extra_arg += [command] if browser_name == "chrome": return Chrome(variation, extra_args=chrome_extra_arg) if browser_name == "canary": return Canary(variation, extra_args=chrome_extra_arg) elif browser_name == "chromium": return Chromium(variation, extra_args=chrome_extra_arg) elif browser_name == "edge": return Edge(variation, extra_args=chrome_extra_arg) return None