Files
ShawnPana 4d8c4e5e48 refactor(cli): consolidate tunnel code into single tunnel.py
- Rename tunnel_manager.py to tunnel.py
- Remove dead code: commands/tunnel.py (session-scoped implementation never reached)
- Remove TunnelInfo dataclass and tunnels field from sessions.py
- Remove tunnel routing from server.py (unreachable code path)
- Update imports in main.py, doctor.py, setup.py, and tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 02:09:55 -08:00

157 lines
4.1 KiB
Python

"""Session registry - manages BrowserSession instances."""
import logging
from dataclasses import dataclass, field
from typing import Any
from browser_use.browser.session import BrowserSession
from browser_use.skill_cli.python_session import PythonSession
logger = logging.getLogger(__name__)
@dataclass
class SessionInfo:
"""Information about a browser session."""
name: str
browser_mode: str
headed: bool
profile: str | None
browser_session: BrowserSession
python_session: PythonSession = field(default_factory=PythonSession)
class SessionRegistry:
"""Registry of active browser sessions.
Sessions are created on-demand when first accessed. Each named session
is isolated with its own BrowserSession and Python namespace.
"""
def __init__(self) -> None:
self._sessions: dict[str, SessionInfo] = {}
async def get_or_create(
self,
name: str,
browser_mode: str,
headed: bool,
profile: str | None,
) -> SessionInfo:
"""Get existing session or create new one."""
if name in self._sessions:
return self._sessions[name]
logger.info(f'Creating new session: {name} (mode={browser_mode}, headed={headed})')
browser_session = await create_browser_session(browser_mode, headed, profile)
await browser_session.start()
session_info = SessionInfo(
name=name,
browser_mode=browser_mode,
headed=headed,
profile=profile,
browser_session=browser_session,
)
self._sessions[name] = session_info
return session_info
def get(self, name: str) -> SessionInfo | None:
"""Get session by name."""
return self._sessions.get(name)
def list_sessions(self) -> list[dict[str, Any]]:
"""List all active sessions."""
return [
{
'name': s.name,
'browser_mode': s.browser_mode,
'headed': s.headed,
'profile': s.profile,
}
for s in self._sessions.values()
]
async def close_session(self, name: str) -> bool:
"""Close and remove a session."""
if name not in self._sessions:
return False
session = self._sessions.pop(name)
logger.info(f'Closing session: {name}')
# Note: Tunnels are managed independently via tunnel.py
# They persist across session close/open cycles
try:
await session.browser_session.kill()
except Exception as e:
logger.warning(f'Error closing session {name}: {e}')
return True
async def close_all(self) -> None:
"""Close all sessions."""
for name in list(self._sessions.keys()):
await self.close_session(name)
async def create_browser_session(
mode: str,
headed: bool,
profile: str | None,
) -> BrowserSession:
"""Create BrowserSession based on mode.
Modes:
- chromium: Playwright-managed Chromium (default)
- real: User's Chrome with profile
- remote: Browser-Use Cloud (requires API key)
Raises:
RuntimeError: If the requested mode is not available based on installation config
"""
from browser_use.skill_cli.install_config import get_mode_unavailable_error, is_mode_available
# Validate mode is available based on installation config
if not is_mode_available(mode):
raise RuntimeError(get_mode_unavailable_error(mode))
if mode == 'chromium':
return BrowserSession(
headless=not headed,
)
elif mode == 'real':
from browser_use.skill_cli.utils import find_chrome_executable, get_chrome_profile_path
chrome_path = find_chrome_executable()
if not chrome_path:
raise RuntimeError('Could not find Chrome executable. Please install Chrome or specify --browser chromium')
# Always get the Chrome user data directory (not the profile subdirectory)
user_data_dir = get_chrome_profile_path(None)
# Profile directory defaults to 'Default', or use the specified profile name
profile_directory = profile if profile else 'Default'
return BrowserSession(
executable_path=chrome_path,
user_data_dir=user_data_dir,
profile_directory=profile_directory,
headless=not headed, # Headless by default, --headed for visible
)
elif mode == 'remote':
from browser_use.skill_cli.api_key import require_api_key
require_api_key('Remote browser')
# Profile is used as cloud_profile_id for remote mode
return BrowserSession(
use_cloud=True,
cloud_profile_id=profile,
)
else:
raise ValueError(f'Unknown browser mode: {mode}')