const { Logger } = require("./logger"); const { connect } = require("puppeteer-real-browser"); const os = require("os"); class BrowserService { constructor() { this.logger = new Logger("Browser"); this.browser = null; this.browserContexts = new Set(); this.cleanupTimer = null; this.isShuttingDown = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.cleanupInterval = 30000; this.contextTimeout = 300000; this.contextCreationTimes = new Map(); this.stats = { totalContexts: 0, activeContexts: 0, memoryUsage: 0, cpuUsage: 0, lastCleanup: Date.now(), }; const cpuCores = os.cpus().length; this.contextLimit = Math.max(cpuCores * 4, 16); this.logger.info(`Browser service initialized with context limit: ${this.contextLimit}`); this.setupGracefulShutdown(); this.startPeriodicCleanup(); } async initialize(options = {}) { if (this.isShuttingDown) return; try { await this.closeBrowser(); this.logger.info("Launching browser..."); const defaultWidth = 1024; const defaultHeight = 768; const width = options.width || defaultWidth; const height = options.height || defaultHeight; const { browser } = await connect({ headless: false, turnstile: true, connectOption: { defaultViewport: { width, height }, timeout: 120000, protocolTimeout: 300000, args: [ `--window-size=${width},${height}`, '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--disable-background-networking', '--disable-default-apps', '--disable-extensions', '--disable-sync', '--disable-translate', '--disable-web-security', '--disable-features=VizDisplayCompositor', '--single-process', '--no-zygote', '--no-first-run' ], }, disableXvfb: false, }); if (!browser) throw new Error("Failed to connect to browser"); this.browser = browser; this.reconnectAttempts = 0; this.setupBrowserEventHandlers(); this.wrapBrowserMethods(); this.logger.success("Browser launched successfully"); } catch (error) { this.logger.error("Browser initialization failed:", error); if (this.reconnectAttempts < this.maxReconnectAttempts && !this.isShuttingDown) { this.reconnectAttempts++; this.logger.warn(`Retrying browser initialization (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); await new Promise((resolve) => setTimeout(resolve, 5000 * this.reconnectAttempts)); return this.initialize(options); } throw error; } } setupBrowserEventHandlers() { if (!this.browser) return; this.browser.on("disconnected", async () => { if (this.isShuttingDown) return; this.logger.warn("Browser disconnected, attempting to reconnect..."); await this.handleBrowserDisconnection(); }); this.browser.on("targetcreated", () => this.updateStats()); this.browser.on("targetdestroyed", () => this.updateStats()); } wrapBrowserMethods() { if (!this.browser) return; const originalCreateContext = this.browser.createBrowserContext.bind(this.browser); this.browser.createBrowserContext = async (...args) => { if (this.browserContexts.size >= this.contextLimit) { await this.forceCleanupOldContexts(); if (this.browserContexts.size >= this.contextLimit) { throw new Error(`Browser context limit reached (${this.contextLimit})`); } } const context = await originalCreateContext(...args); if (context) { this.browserContexts.add(context); this.contextCreationTimes.set(context, Date.now()); this.stats.totalContexts++; const originalClose = context.close.bind(context); context.close = async () => { try { await originalClose(); } catch (error) { this.logger.warn("Error closing context:", error.message); } finally { this.browserContexts.delete(context); this.contextCreationTimes.delete(context); this.updateStats(); } }; setTimeout(async () => { if (this.browserContexts.has(context)) { this.logger.debug("Force closing expired context"); try { await context.close(); } catch { } } }, this.contextTimeout); } this.updateStats(); return context; }; } async handleBrowserDisconnection() { try { const cleanupPromises = Array.from(this.browserContexts).map((context) => context.close().catch(() => { })); await Promise.allSettled(cleanupPromises); this.browserContexts.clear(); this.contextCreationTimes.clear(); if (this.reconnectAttempts < this.maxReconnectAttempts) { await new Promise((resolve) => setTimeout(resolve, 5000)); await this.initialize(); } else { this.logger.error("Max reconnection attempts reached"); } } catch (error) { this.logger.error("Error handling browser disconnection:", error); } } startPeriodicCleanup() { this.cleanupTimer = setInterval(async () => { if (this.isShuttingDown) return; try { await this.performCleanup(); this.updateStats(); } catch (error) { this.logger.error("Periodic cleanup error:", error); } }, this.cleanupInterval); } async performCleanup() { const now = Date.now(); const contextsToCleanup = []; for (const [context, creationTime] of this.contextCreationTimes.entries()) { if (now - creationTime > this.contextTimeout) { contextsToCleanup.push(context); } } if (contextsToCleanup.length > 0) { this.logger.debug(`Cleaning up ${contextsToCleanup.length} expired contexts`); const cleanupPromises = contextsToCleanup.map((context) => context.close().catch(() => { })); await Promise.allSettled(cleanupPromises); } if (this.browserContexts.size > this.contextLimit * 0.8) await this.forceCleanupOldContexts(); this.stats.lastCleanup = now; } async forceCleanupOldContexts() { const contextsArray = Array.from(this.browserContexts); const sortedContexts = contextsArray.sort((a, b) => { const timeA = this.contextCreationTimes.get(a) || 0; const timeB = this.contextCreationTimes.get(b) || 0; return timeA - timeB; }); const toCleanup = sortedContexts.slice(0, Math.floor(sortedContexts.length * 0.3)); if (toCleanup.length > 0) { this.logger.warn(`Force cleaning up ${toCleanup.length} contexts due to limit`); const cleanupPromises = toCleanup.map((context) => context.close().catch(() => { })); await Promise.allSettled(cleanupPromises); } } updateStats() { this.stats.activeContexts = this.browserContexts.size; this.stats.memoryUsage = process.memoryUsage().heapUsed; const usage = process.cpuUsage(); this.stats.cpuUsage = (usage.user + usage.system) / 1000000; } async createContext(options = {}) { if (!this.browser) await this.initialize(); if (!this.browser) throw new Error("Browser not available"); return await this.browser.createBrowserContext({ ...options, ignoreHTTPSErrors: true, }); } async withBrowserContext(callback) { let context = null; try { context = await this.createContext(); return await callback(context); } finally { if (context) { try { await context.close(); } catch (error) { this.logger.warn(`Failed to close context: ${error.message}`); } } } } getBrowserStats() { return { ...this.stats }; } isReady() { return this.browser !== null && !this.isShuttingDown; } async closeBrowser() { if (this.browser) { try { const cleanupPromises = Array.from(this.browserContexts).map((context) => context.close().catch(() => { })); await Promise.allSettled(cleanupPromises); this.browserContexts.clear(); this.contextCreationTimes.clear(); await this.browser.close(); this.logger.info("Browser closed successfully"); } catch (error) { this.logger.error("Error closing browser:", error); } finally { this.browser = null; } } } setupGracefulShutdown() { const gracefulShutdown = async (signal) => { this.logger.warn(`Received ${signal}, shutting down browser service...`); this.isShuttingDown = true; if (this.cleanupTimer) clearInterval(this.cleanupTimer); await this.closeBrowser(); this.logger.success("Browser service shutdown complete"); }; process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); process.on("SIGINT", () => gracefulShutdown("SIGINT")); } async shutdown() { this.isShuttingDown = true; if (this.cleanupTimer) clearInterval(this.cleanupTimer); await this.closeBrowser(); } } module.exports = { BrowserService };