mirror of
https://github.com/browser-use/browser-use
synced 2026-05-06 17:52:15 +02:00
improve BrowserSession auto-restart on disconnection logic
This commit is contained in:
@@ -73,22 +73,36 @@ def require_initialization(func):
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
if not self.initialized:
|
||||
# raise RuntimeError('BrowserSession(...).start() must be called first to launch or connect to the browser')
|
||||
await self.start() # just start it automatically if not already started
|
||||
try:
|
||||
if not self.initialized:
|
||||
# raise RuntimeError('BrowserSession(...).start() must be called first to launch or connect to the browser')
|
||||
await self.start() # just start it automatically if not already started
|
||||
|
||||
if not self.agent_current_page or self.agent_current_page.is_closed():
|
||||
self.agent_current_page = self.browser_context.pages[0] if self.browser_context.pages else None
|
||||
if not self.agent_current_page or self.agent_current_page.is_closed():
|
||||
self.agent_current_page = (
|
||||
self.browser_context.pages[0] if (self.browser_context and self.browser_context.pages) else None
|
||||
)
|
||||
|
||||
if not self.agent_current_page or self.agent_current_page.is_closed():
|
||||
await self.create_new_tab()
|
||||
if not self.agent_current_page or self.agent_current_page.is_closed():
|
||||
await self.create_new_tab()
|
||||
|
||||
assert self.agent_current_page and not self.agent_current_page.is_closed()
|
||||
assert self.agent_current_page and not self.agent_current_page.is_closed()
|
||||
|
||||
if not hasattr(self, '_cached_browser_state_summary'):
|
||||
raise RuntimeError('BrowserSession(...).start() must be called first to initialize the browser session')
|
||||
if not hasattr(self, '_cached_browser_state_summary'):
|
||||
raise RuntimeError('BrowserSession(...).start() must be called first to initialize the browser session')
|
||||
|
||||
return await func(self, *args, **kwargs)
|
||||
return await func(self, *args, **kwargs)
|
||||
|
||||
except Exception as e:
|
||||
# Check if this is a TargetClosedError or similar connection error
|
||||
if 'TargetClosedError' in str(type(e)) or 'context or browser has been closed' in str(e):
|
||||
logger.debug(f'Detected closed browser connection in {func.__name__}, resetting connection state')
|
||||
self._reset_connection_state()
|
||||
# Re-raise the error so the caller can handle it appropriately
|
||||
raise
|
||||
else:
|
||||
# Re-raise other exceptions unchanged
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -186,6 +200,7 @@ class BrowserSession(BaseModel):
|
||||
|
||||
_cached_browser_state_summary: BrowserStateSummary | None = PrivateAttr(default=None)
|
||||
_cached_clickable_element_hashes: CachedClickableElementHashes | None = PrivateAttr(default=None)
|
||||
_start_lock: asyncio.Lock = PrivateAttr(default_factory=asyncio.Lock)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def apply_session_overrides_to_profile(self) -> Self:
|
||||
@@ -230,28 +245,39 @@ class BrowserSession(BaseModel):
|
||||
7. playwright=Playwright object, will use its chromium instance to launch a new browser
|
||||
"""
|
||||
|
||||
# apply last-minute runtime-computed options to the the browser_profile, validate profile, set up folders on disk
|
||||
assert isinstance(self.browser_profile, BrowserProfile)
|
||||
self.browser_profile.prepare_user_data_dir() # create/unlock the <user_data_dir>/SingletonLock
|
||||
self.browser_profile.detect_display_configuration() # adjusts config values, must come before launch/connect
|
||||
async with self._start_lock:
|
||||
# if we're already initialized and the connection is still valid, return the existing session state and start from scratch
|
||||
if self.initialized and self.is_connected():
|
||||
return self
|
||||
self._reset_connection_state()
|
||||
|
||||
# launch/connect to the browser:
|
||||
# setup playwright library client, Browser, and BrowserContext objects
|
||||
await self.setup_playwright()
|
||||
await self.setup_browser_via_passed_objects()
|
||||
await self.setup_browser_via_browser_pid()
|
||||
await self.setup_browser_via_wss_url()
|
||||
await self.setup_browser_via_cdp_url()
|
||||
await self.setup_new_browser_context() # creates a new context in existing browser or launches a new persistent context
|
||||
assert self.browser_context, f'Failed to connect to or create a new BrowserContext for browser={self.browser}'
|
||||
self.initialized = True # set this first to ensure two parallel calls to start() don't clash with each other
|
||||
try:
|
||||
# apply last-minute runtime-computed options to the the browser_profile, validate profile, set up folders on disk
|
||||
assert isinstance(self.browser_profile, BrowserProfile)
|
||||
self.browser_profile.prepare_user_data_dir() # create/unlock the <user_data_dir>/SingletonLock
|
||||
self.browser_profile.detect_display_configuration() # adjusts config values, must come before launch/connect
|
||||
|
||||
# resize the existing pages and set up foreground tab detection
|
||||
await self._setup_viewports()
|
||||
await self._setup_current_page_change_listeners()
|
||||
# launch/connect to the browser:
|
||||
# setup playwright library client, Browser, and BrowserContext objects
|
||||
await self.setup_playwright()
|
||||
await self.setup_browser_via_passed_objects()
|
||||
await self.setup_browser_via_browser_pid()
|
||||
await self.setup_browser_via_wss_url()
|
||||
await self.setup_browser_via_cdp_url()
|
||||
await (
|
||||
self.setup_new_browser_context()
|
||||
) # creates a new context in existing browser or launches a new persistent context
|
||||
assert self.browser_context, f'Failed to connect to or create a new BrowserContext for browser={self.browser}'
|
||||
|
||||
self.initialized = True
|
||||
# resize the existing pages and set up foreground tab detection
|
||||
await self._setup_viewports()
|
||||
await self._setup_current_page_change_listeners()
|
||||
except Exception:
|
||||
self.initialized = False
|
||||
raise
|
||||
|
||||
return self
|
||||
return self
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Shuts down the BrowserSession, killing the browser process if keep_alive=False"""
|
||||
@@ -317,8 +343,28 @@ class BrowserSession(BaseModel):
|
||||
# 1. check for a passed Page object, if present, it always takes priority, set browser_context = page.context
|
||||
self.browser_context = (self.agent_current_page and self.agent_current_page.context) or self.browser_context or None
|
||||
|
||||
# 2. if we have a context now, it always takes precedence, set browser = context.browser, otherwise use the passed browser
|
||||
self.browser = (self.browser_context and self.browser_context.browser) or self.browser or None
|
||||
# 2. Check if the current browser connection is valid, if not clear the invalid objects
|
||||
if self.browser_context:
|
||||
try:
|
||||
# Try to access a property that would fail if the context is closed
|
||||
_ = self.browser_context.pages
|
||||
# Additional check: verify the browser is still connected
|
||||
if self.browser_context.browser and not self.browser_context.browser.is_connected():
|
||||
self.browser_context = None
|
||||
except Exception:
|
||||
# Context is closed, clear it
|
||||
self.browser_context = None
|
||||
|
||||
# 3. if we have a browser object but it's disconnected, clear it and the context because we cant use either
|
||||
if self.browser and not self.browser.is_connected():
|
||||
if self.browser_context and (self.browser_context.browser is self.browser):
|
||||
self.browser_context = None
|
||||
self.browser = None
|
||||
|
||||
# 4. if we have a context now, it always takes precedence, set browser = context.browser, otherwise use the passed browser
|
||||
browser_from_context = self.browser_context and self.browser_context.browser
|
||||
if browser_from_context and browser_from_context.is_connected():
|
||||
self.browser = browser_from_context
|
||||
|
||||
if self.browser or self.browser_context:
|
||||
logger.info(f'🌎 Connected to existing user-provided browser_context: {self.browser_context}')
|
||||
@@ -433,8 +479,11 @@ class BrowserSession(BaseModel):
|
||||
**self.browser_profile.kwargs_for_launch_persistent_context().model_dump()
|
||||
)
|
||||
|
||||
self.browser = (self.browser_context and self.browser_context.browser) or self.browser
|
||||
# ^ this can unfortunately still be None at the end ^
|
||||
# Only restore browser from context if it's connected, otherwise keep it None to force new launch
|
||||
browser_from_context = self.browser_context and self.browser_context.browser
|
||||
if browser_from_context and browser_from_context.is_connected():
|
||||
self.browser = browser_from_context
|
||||
# ^ self.browser can unfortunately still be None at the end ^
|
||||
# playwright does not give us a browser object at all when we use launch_persistent_context()!
|
||||
|
||||
# Detect any new child chrome processes that we might have launched above
|
||||
@@ -719,6 +768,46 @@ class BrowserSession(BaseModel):
|
||||
if self.browser_profile.keep_alive is None:
|
||||
self.browser_profile.keep_alive = keep_alive
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""
|
||||
Check if the browser session has valid, connected browser and context objects.
|
||||
Returns False if any of the following conditions are met:
|
||||
- No browser_context exists
|
||||
- Browser exists but is disconnected
|
||||
- Browser_context's browser exists but is disconnected
|
||||
- Browser_context itself is closed/unusable
|
||||
"""
|
||||
# Check if browser_context is missing
|
||||
if not self.browser_context:
|
||||
return False
|
||||
|
||||
# Check if browser exists but is disconnected
|
||||
if self.browser and not self.browser.is_connected():
|
||||
return False
|
||||
|
||||
# Check if browser_context's browser exists but is disconnected
|
||||
if self.browser_context.browser and not self.browser_context.browser.is_connected():
|
||||
return False
|
||||
|
||||
# Check if the browser_context itself is closed/unusable
|
||||
try:
|
||||
# Try to access a property that would fail if the context is closed
|
||||
_ = self.browser_context.pages
|
||||
# Additional check: try to access the browser property which might fail if context is closed
|
||||
if self.browser_context.browser and not self.browser_context.browser.is_connected():
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _reset_connection_state(self) -> None:
|
||||
"""Reset the browser connection state when disconnection is detected"""
|
||||
self.initialized = False
|
||||
self.browser = None
|
||||
self.browser_context = None
|
||||
# Also clear browser_pid since the process may no longer exist
|
||||
self.browser_pid = None
|
||||
|
||||
# --- Tab management ---
|
||||
async def get_current_page(self) -> Page:
|
||||
"""Get the current page + ensure it's not None / closed"""
|
||||
@@ -821,6 +910,7 @@ class BrowserSession(BaseModel):
|
||||
selector_map = await self.get_selector_map()
|
||||
return selector_map.get(index)
|
||||
|
||||
@require_initialization
|
||||
@time_execution_async('--click_element_node')
|
||||
async def _click_element_node(self, element_node: DOMElementNode) -> str | None:
|
||||
"""
|
||||
@@ -883,6 +973,7 @@ class BrowserSession(BaseModel):
|
||||
except Exception as e:
|
||||
raise Exception(f'Failed to click element: {repr(element_node)}. Error: {str(e)}')
|
||||
|
||||
@require_initialization
|
||||
@time_execution_async('--get_tabs_info')
|
||||
async def get_tabs_info(self) -> list[TabInfo]:
|
||||
"""Get information about all tabs"""
|
||||
|
||||
Reference in New Issue
Block a user