Spaces:
Sleeping
Sleeping
| import { chromium, Browser, BrowserContext } from 'playwright'; | |
| import { config } from '../config'; | |
| import { logger } from '../utils/logger'; | |
| let browser: Browser | null = null; | |
| let browserLaunchPromise: Promise<Browser> | null = null; | |
| /** | |
| * Get or launch the shared browser instance. | |
| * Multiple callers waiting concurrently will share the same launch promise. | |
| */ | |
| export async function getBrowser(): Promise<Browser> { | |
| if (browser && browser.isConnected()) { | |
| return browser; | |
| } | |
| if (browserLaunchPromise) { | |
| return browserLaunchPromise; | |
| } | |
| browserLaunchPromise = launchBrowser(); | |
| try { | |
| browser = await browserLaunchPromise; | |
| return browser; | |
| } finally { | |
| browserLaunchPromise = null; | |
| } | |
| } | |
| async function launchBrowser(): Promise<Browser> { | |
| logger.info('Launching Chromium browser', { headless: config.headless }); | |
| const b = await chromium.launch({ | |
| headless: config.headless, | |
| args: [ | |
| '--no-sandbox', | |
| '--disable-setuid-sandbox', | |
| '--disable-dev-shm-usage', | |
| '--disable-gpu', | |
| ], | |
| }); | |
| b.on('disconnected', () => { | |
| logger.warn('Browser disconnected'); | |
| browser = null; | |
| }); | |
| logger.info('Browser launched successfully'); | |
| return b; | |
| } | |
| /** | |
| * Create a new isolated browser context for a job. | |
| * Optionally restore storage state from a previous session. | |
| */ | |
| export async function createContext( | |
| storageState?: string | |
| ): Promise<BrowserContext> { | |
| const b = await getBrowser(); | |
| const contextOptions: Parameters<Browser['newContext']>[0] = { | |
| viewport: { width: 1280, height: 720 }, | |
| userAgent: | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| acceptDownloads: true, | |
| }; | |
| if (storageState) { | |
| try { | |
| contextOptions.storageState = JSON.parse(storageState); | |
| } catch { | |
| logger.warn('Failed to parse storage state, starting fresh'); | |
| } | |
| } | |
| const context = await b.newContext(contextOptions); | |
| // Set reasonable timeouts | |
| context.setDefaultTimeout(30000); | |
| context.setDefaultNavigationTimeout(60000); | |
| return context; | |
| } | |
| /** | |
| * Gracefully close the shared browser. | |
| */ | |
| export async function closeBrowser(): Promise<void> { | |
| if (browser) { | |
| try { | |
| await browser.close(); | |
| } catch { | |
| // Ignore close errors | |
| } | |
| browser = null; | |
| logger.info('Browser closed'); | |
| } | |
| } | |
| /** | |
| * Get count of active contexts (approximation of active jobs). | |
| */ | |
| export async function getActiveContextCount(): Promise<number> { | |
| if (!browser || !browser.isConnected()) return 0; | |
| return browser.contexts().length; | |
| } | |