browser-manager.tsā¢6.61 kB
/**
* Stable Browser Manager
* Handles browser automation with crash prevention and recovery
*/
import playwright, { Browser, Page, BrowserContext } from 'playwright';
import { readFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const currentDir = dirname(fileURLToPath(import.meta.url));
export interface BrowserSession {
browser: Browser;
context: BrowserContext;
page: Page;
}
export class StableBrowserManager {
private jwtToken!: string;
private baseURL = 'https://composer.euconquisto.com/#/embed';
private orgId = '36c92686-c494-ec11-a22a-dc984041c95d';
private maxRetries = 3;
private retryDelay = 2000;
constructor() {
this.loadJWTToken();
}
private loadJWTToken() {
try {
const tokenPath = resolve(currentDir, '..', 'correct-jwt-new.txt');
this.jwtToken = readFileSync(tokenPath, 'utf8').trim();
} catch (error) {
throw new Error('Failed to load JWT token: ' + error);
}
}
async createSession(options: {
headless?: boolean;
timeout?: number;
slowMo?: number;
} = {}): Promise<BrowserSession> {
const {
headless = false,
timeout = 120000,
slowMo = 100,
} = options;
let attempt = 0;
while (attempt < this.maxRetries) {
try {
const browser = await playwright.chromium.launch({
headless,
timeout,
slowMo,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-blink-features=AutomationControlled',
'--disable-extensions',
'--no-first-run',
'--no-default-browser-check',
],
});
const context = await browser.newContext({
viewport: { width: 1366, height: 768 },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
extraHTTPHeaders: {
'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',
},
});
const page = await context.newPage();
// Set up crash detection
page.on('crash', () => {
console.log('šØ Page crashed, attempting recovery...');
});
browser.on('disconnected', () => {
console.log('šØ Browser disconnected');
});
// Navigate to Composer
const embedURL = `${this.baseURL}/auth-with-token/pt_br/home/${this.orgId}/${this.jwtToken}`;
await page.goto(embedURL, {
waitUntil: 'networkidle',
timeout: timeout,
});
// Wait for page to be ready
await this.waitForPageReady(page);
return { browser, context, page };
} catch (error) {
attempt++;
console.log(`š Browser launch attempt ${attempt} failed:`, (error as Error).message);
if (attempt < this.maxRetries) {
console.log(`ā³ Retrying in ${this.retryDelay}ms...`);
await this.sleep(this.retryDelay);
} else {
throw new Error(`Failed to launch browser after ${this.maxRetries} attempts: ${(error as Error).message}`);
}
}
}
throw new Error('Unexpected error in browser launch');
}
async safeExecute<T>(
session: BrowserSession,
operation: (page: Page) => Promise<T>,
operationName: string
): Promise<T> {
let attempt = 0;
while (attempt < this.maxRetries) {
try {
// Check if page is still active
if (session.page.isClosed()) {
throw new Error('Page is closed');
}
return await operation(session.page);
} catch (error) {
attempt++;
console.log(`š ${operationName} attempt ${attempt} failed:`, (error as Error).message);
if (attempt < this.maxRetries) {
console.log(`ā³ Retrying ${operationName} in ${this.retryDelay}ms...`);
await this.sleep(this.retryDelay);
// Try to recover the page
if (session.page.isClosed()) {
console.log('š§ Attempting to recover closed page...');
session.page = await session.context.newPage();
const embedURL = `${this.baseURL}/auth-with-token/pt_br/home/${this.orgId}/${this.jwtToken}`;
await session.page.goto(embedURL, { waitUntil: 'networkidle' });
await this.waitForPageReady(session.page);
}
} else {
throw new Error(`${operationName} failed after ${this.maxRetries} attempts: ${(error as Error).message}`);
}
}
}
throw new Error(`Unexpected error in ${operationName}`);
}
async waitForPageReady(page: Page): Promise<void> {
// Wait for the page to be fully loaded
await page.waitForLoadState('networkidle');
// Wait for key elements to be present
await page.waitForSelector('body', { timeout: 30000 });
// Wait for any async operations to complete
await this.sleep(2000);
}
async safeClick(page: Page, selector: string): Promise<void> {
await page.waitForSelector(selector, { timeout: 30000 });
await page.click(selector);
await this.sleep(1000); // Allow UI to update
}
async safeType(page: Page, selector: string, text: string): Promise<void> {
await page.waitForSelector(selector, { timeout: 30000 });
await page.fill(selector, text);
await this.sleep(500);
}
async safeWaitForSelector(page: Page, selector: string, timeout: number = 30000): Promise<void> {
await page.waitForSelector(selector, { timeout });
}
async cleanupSession(session: BrowserSession): Promise<void> {
try {
if (session.page && !session.page.isClosed()) {
await session.page.close();
}
if (session.context) {
await session.context.close();
}
if (session.browser) {
await session.browser.close();
}
} catch (error) {
console.log('ā ļø Error during session cleanup:', (error as Error).message);
}
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async withStableSession<T>(
operation: (session: BrowserSession) => Promise<T>,
options: {
headless?: boolean;
timeout?: number;
slowMo?: number;
} = {}
): Promise<T> {
const session = await this.createSession(options);
try {
return await operation(session);
} finally {
await this.cleanupSession(session);
}
}
}