Files
browser-use/browser_use/browser/browser.py

193 lines
5.4 KiB
Python

"""
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())