""" Playwright browser on steroids. """ import asyncio import logging from dataclasses import dataclass, field from playwright.async_api import Browser as PlaywrightBrowser from playwright.async_api import ( Playwright, async_playwright, ) from browser_use.browser.context import BrowserContext, BrowserContextConfig logger = logging.getLogger(__name__) @dataclass class BrowserConfig: """ Configuration for the Browser. Default values: headless: True Whether to run browser in headless mode disable_security: False Disable browser security features extra_chromium_args: [] Extra arguments to pass to the browser wss_url: None Connect to a browser instance via WebSocket chrome_instance_path: None Path to a Chrome instance to use to connect to your normal browser e.g. '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome' """ headless: bool = False disable_security: bool = True extra_chromium_args: list[str] = field(default_factory=list) chrome_instance_path: str | None = None wss_url: str | None = None new_context_config: BrowserContextConfig = field(default_factory=BrowserContextConfig) # @singleton: TODO - think about id singleton makes sense here # @dev By default this is a singleton, but you can create multiple instances if you need to. class Browser: """ Playwright browser on steroids. This is persistant browser factory that can spawn multiple browser contexts. It is recommended to use only one instance of Browser per your application (RAM usage will grow otherwise). """ def __init__( self, config: BrowserConfig = BrowserConfig(), ): logger.debug('Initializing new browser') self.config = config self.playwright: Playwright | None = None self.playwright_browser: PlaywrightBrowser | None = None async def new_context( self, config: BrowserContextConfig = BrowserContextConfig() ) -> BrowserContext: """Create a browser context""" return BrowserContext(config=config, browser=self) async def get_playwright_browser(self) -> PlaywrightBrowser: """Get a browser context""" if self.playwright_browser is None: return await self._init() return self.playwright_browser async def _init(self): """Initialize the browser session""" playwright = await async_playwright().start() browser = await self._setup_browser(playwright) self.playwright = playwright self.playwright_browser = browser return self.playwright_browser async def _setup_browser(self, playwright: Playwright) -> PlaywrightBrowser: """Sets up and returns a Playwright Browser instance with anti-detection measures.""" if self.config.wss_url: browser = await playwright.chromium.connect(self.config.wss_url) return browser elif self.config.chrome_instance_path: import subprocess import requests try: # Check if browser is already running response = requests.get('http://localhost:9222/json/version', timeout=2) if response.status_code == 200: logger.info('Reusing existing Chrome instance') browser = await playwright.chromium.connect_over_cdp( endpoint_url='http://localhost:9222', timeout=20000, # 20 second timeout for connection ) return browser except requests.ConnectionError: logger.debug('No existing Chrome instance found, starting a new one') # Start a new Chrome instance subprocess.Popen( [ self.config.chrome_instance_path, '--remote-debugging-port=9222', ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # Attempt to connect again after starting a new instance try: browser = await playwright.chromium.connect_over_cdp( endpoint_url='http://localhost:9222', timeout=20000, # 20 second timeout for connection ) return browser except Exception as e: logger.error(f'Failed to start a new Chrome instance.: {str(e)}') raise RuntimeError( ' To start chrome in Debug mode, you need to close all existing Chrome instances and try again otherwise we can not connect to the instance.' ) else: try: disable_security_args = [] if self.config.disable_security: disable_security_args = [ '--disable-web-security', '--disable-site-isolation-trials', '--disable-features=IsolateOrigins,site-per-process', ] browser = await playwright.chromium.launch( headless=self.config.headless, args=[ '--no-sandbox', '--disable-blink-features=AutomationControlled', '--disable-infobars', '--disable-background-timer-throttling', '--disable-popup-blocking', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-window-activation', '--disable-focus-on-load', '--no-first-run', '--no-default-browser-check', '--no-startup-window', '--window-position=0,0', '--window-size=3000,3000', ] + disable_security_args + self.config.extra_chromium_args, ) return browser except Exception as e: logger.error(f'Failed to initialize Playwright browser: {str(e)}') raise async def close(self): """Close the browser instance""" if self.playwright_browser: await self.playwright_browser.close() if self.playwright: await self.playwright.stop() def __del__(self): """Async cleanup when object is destroyed""" try: loop = asyncio.get_running_loop() if loop.is_running(): loop.create_task(self.close()) else: asyncio.run(self.close()) except RuntimeError: asyncio.run(self.close())