import { EventEmitter } from 'events'; import { randomUUID } from 'crypto'; export default class BrowserPool extends EventEmitter { constructor(options = {}) { super(); this.chromium = options.chromium; if (!this.chromium) throw new Error('Chromium instance must be provided'); this.maxSize = options.maxSize || 5; this.minSize = options.minSize || 1; this.maxUsage = options.maxUsage || 100; this.maxIdleTime = options.maxIdleTime || 300000; this.createTimeout = options.createTimeout || 30000; this.validateInterval = options.validateInterval || 60000; this.getTimeout = options.getTimeout || 30000; this.availableInstances = []; this.busyInstances = new Set(); this.totalInstances = 0; this.waitingQueue = []; this.stats = { created: 0, destroyed: 0, borrowed: 0, returned: 0, timeouts: 0, errors: 0 }; this.isShuttingDown = false; this.healthCheckInterval = null; } async initialize() { console.log(`[BrowserPool] Initializing pool with min: ${this.minSize}, max: ${this.maxSize}`); const createPromises = Array.from({ length: this.minSize }, () => this.createInstance()); try { await Promise.all(createPromises); console.log(`[BrowserPool] Pool initialized with ${this.availableInstances.length} instances.`); } catch (error) { console.error('[BrowserPool] Failed to initialize pool:', error.message); await this.shutdown(); throw error; } this.startHealthCheck(); } async createInstance() { if (this.isShuttingDown) { throw new Error('Pool is shutting down'); } if (this.totalInstances >= this.maxSize) { return null; } const startTime = Date.now(); let browser = null; this.totalInstances++; try { console.log('[BrowserPool] Creating new browser instance...'); browser = await Promise.race([ this.chromium.launch({ headless: true, args: [ // '--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', // '--disable-web-security', '--autoplay-policy=no-user-gesture-required', // '--disable-dev-shm-usage', '--disable-gpu', '--single-process', '--no-zygote', // '--disable-infobars', '--disable-notifications', '--disable-extensions', '--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-web-security', '--autoplay-policy=no-user-gesture-required', '--disable-dev-shm-usage', // 避免共享内存问题 '--disable-gpu', // 减少资源使用 '--no-first-run', // 跳过首次运行设置 '--disable-default-apps' // 禁用默认应用 ] }), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser creation timeout')), this.createTimeout)) ]); const instance = { browser, id: `browser_${randomUUID().slice(0, 8)}`, createdAt: Date.now(), lastUsed: Date.now(), usageCount: 0, isHealthy: true }; browser.on('disconnected', () => { console.log(`[BrowserPool] Browser ${instance.id} disconnected unexpectedly.`); this.destroyInstance(instance, true); }); this.stats.created++; console.log(`[BrowserPool] Browser instance created: ${instance.id} (${Date.now() - startTime}ms)`); this.emit('instanceCreated', instance); this.availableInstances.push(instance); this.processWaitingQueue(); return instance; } catch (error) { this.totalInstances--; this.stats.errors++; console.error('[BrowserPool] Failed to create browser instance:', error.message); if (browser) await browser.close().catch(e => console.error(`Error closing failed browser: ${e.message}`)); throw error; } } get() { if (this.isShuttingDown) return Promise.reject(new Error('Pool is shutting down')); return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.waitingQueue = this.waitingQueue.filter(w => w.resolve !== resolve); this.stats.timeouts++; reject(new Error(`Browser acquisition timeout after ${this.getTimeout}ms`)); }, this.getTimeout); const tryAcquire = async () => { while (this.availableInstances.length > 0) { const instance = this.availableInstances.shift(); if (await this.validateInstance(instance)) { this.busyInstances.add(instance); instance.lastUsed = Date.now(); instance.usageCount++; this.stats.borrowed++; clearTimeout(timeoutId); return resolve(instance); } else { await this.destroyInstance(instance); } } if (this.totalInstances < this.maxSize) { this.createInstance().catch(err => console.error(`[BrowserPool] Background instance creation failed: ${err.message}`)); } this.waitingQueue.push({ resolve, reject, timeoutId }); console.log(`[BrowserPool] No available instances, request added to waiting queue (size: ${this.waitingQueue.length})`); }; tryAcquire(); }); } async release(instance) { if (!instance || !this.busyInstances.has(instance)) { return; } this.busyInstances.delete(instance); this.stats.returned++; if (this.isShuttingDown || !await this.validateInstance(instance) || instance.usageCount >= this.maxUsage) { await this.destroyInstance(instance); } else { try { const contexts = instance.browser.contexts(); // 默认上下文是第一个,我们不关闭它,只关闭后续创建的 for (let i = 1; i < contexts.length; i++) { await contexts[i].close(); } this.availableInstances.push(instance); } catch (e) { console.warn(`[BrowserPool] Error cleaning up contexts for ${instance.id}, destroying it.`, e.message); await this.destroyInstance(instance); } } this.processWaitingQueue(); } async destroyInstance(instance, isDisconnected = false) { if (!instance) return; this.busyInstances.delete(instance); const index = this.availableInstances.indexOf(instance); if (index > -1) this.availableInstances.splice(index, 1); if (!isDisconnected && instance.browser && instance.browser.isConnected()) { await instance.browser.close().catch(e => console.error(`[BrowserPool] Error closing browser on destroy: ${e.message}`)); } if (this.totalInstances > 0) this.totalInstances--; this.stats.destroyed++; console.log(`[BrowserPool] Instance ${instance.id} destroyed.`); this.emit('instanceDestroyed', instance); if (!this.isShuttingDown && this.totalInstances < this.minSize) { this.createInstance().catch(err => console.error(`[BrowserPool] Failed to replenish instance: ${err.message}`)); } } processWaitingQueue() { if (this.waitingQueue.length > 0 && this.availableInstances.length > 0) { console.log('[BrowserPool] Processing waiting queue...'); const waiter = this.waitingQueue.shift(); const instance = this.availableInstances.shift(); this.busyInstances.add(instance); instance.lastUsed = Date.now(); instance.usageCount++; this.stats.borrowed++; clearTimeout(waiter.timeoutId); waiter.resolve(instance); } } async validateInstance(instance) { if (!instance || !instance.browser || !instance.browser.isConnected()) return false; try { // 一个更轻量级的检查,创建一个新的 incognito context const context = await instance.browser.newContext(); await context.close(); return true; } catch (error) { console.warn(`[BrowserPool] Instance ${instance.id} validation failed.`); return false; } } startHealthCheck() { this.healthCheckInterval = setInterval(() => this.performHealthCheck(), this.validateInterval); } async performHealthCheck() { if (this.isShuttingDown) return; const now = Date.now(); const idleInstances = this.availableInstances.slice(); // 创建副本以安全遍历 for (const instance of idleInstances) { if (this.totalInstances > this.minSize && now - instance.lastUsed > this.maxIdleTime) { console.log(`[BrowserPool] Removing idle instance ${instance.id}`); await this.destroyInstance(instance); } } } getStats() { return { ...this.stats, pool: { available: this.availableInstances.length, busy: this.busyInstances.size, total: this.totalInstances, waiting: this.waitingQueue.length } }; } async shutdown() { console.log('[BrowserPool] Shutting down...'); this.isShuttingDown = true; if (this.healthCheckInterval) clearInterval(this.healthCheckInterval); for (const waiter of this.waitingQueue) { clearTimeout(waiter.timeoutId); waiter.reject(new Error('Pool is shutting down')); } this.waitingQueue = []; const allInstances = [...this.availableInstances, ...Array.from(this.busyInstances)]; await Promise.all(allInstances.map(instance => instance.browser.close())); console.log('[BrowserPool] All browser instances closed.'); } } // 使用 module.exports 导出类 // module.exports = { BrowserPool };