import { Browser, BrowserContext, Page, chromium } from "playwright"; let browser: Browser | null = null; let context: BrowserContext | null = null; let page: Page | null = null; function getLaunchArgs(): string[] { return [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-software-rasterizer", "--no-first-run", "--no-default-browser-check", "--disable-extensions", "--disable-sync", "--mute-audio", "--window-size=1280,720", ]; } function ensureNodeRuntimeEnv() { process.env.HOME = process.env.HOME || "/home/node"; process.env.XDG_CACHE_HOME = process.env.XDG_CACHE_HOME || "/home/node/.cache"; process.env.XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME || "/home/node/.config"; process.env.XDG_RUNTIME_DIR = process.env.XDG_RUNTIME_DIR || "/tmp/runtime-node"; process.env.DBUS_SESSION_BUS_ADDRESS = process.env.DBUS_SESSION_BUS_ADDRESS || "disabled"; process.env.PLAYWRIGHT_BROWSERS_PATH = process.env.PLAYWRIGHT_BROWSERS_PATH || "/home/node/.cache/ms-playwright"; } export async function initBrowser(): Promise { await closeBrowser(); ensureNodeRuntimeEnv(); console.log(`[Browser] UID : ${process.getuid?.() ?? "unknown"}`); console.log(`[Browser] HOME : ${process.env.HOME}`); console.log(`[Browser] PW path : ${process.env.PLAYWRIGHT_BROWSERS_PATH}`); browser = await chromium.launch({ headless: true, args: getLaunchArgs(), env: { ...process.env, HOME: process.env.HOME, XDG_CACHE_HOME: process.env.XDG_CACHE_HOME, XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS, PLAYWRIGHT_BROWSERS_PATH: process.env.PLAYWRIGHT_BROWSERS_PATH, }, }); context = await browser.newContext({ viewport: { width: 1280, height: 720 }, locale: "en-US", ignoreHTTPSErrors: true, userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", }); context.setDefaultTimeout(10_000); context.setDefaultNavigationTimeout(15_000); page = await context.newPage(); await page.route("**/*.{woff,woff2,ttf,otf,eot}", (route) => route.abort()); await page.route( "**/{analytics,gtag,ga.js,fbq,hotjar,clarity,segment,mixpanel}*", (route) => route.abort() ); await page.setExtraHTTPHeaders({ "Accept-Language": "en-US,en;q=0.9", }); await page.goto("about:blank", { timeout: 5_000 }); console.log("[Browser] Ready on about:blank"); } export async function navigateTo(url: string): Promise { if (!page) { throw new Error("No active browser page"); } console.log(`[Browser] Navigating to ${url}`); try { const response = await page.goto(url, { waitUntil: "commit", timeout: 15_000, }); console.log("[Browser] commit ok:", response?.status() ?? "no-response"); await page .waitForLoadState("domcontentloaded", { timeout: 8_000 }) .catch(() => {}); await page.waitForTimeout(1200); return; } catch (err) { console.warn("[Browser] commit failed, retrying with domcontentloaded", err); } try { await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15_000, }); console.log("[Browser] Navigation OK (domcontentloaded)"); await page.waitForTimeout(1200); return; } catch (err) { console.warn("[Browser] domcontentloaded failed, waiting for body", err); } try { await page.waitForSelector("body", { timeout: 8_000, }); console.log("[Browser] Body detected"); await page.waitForTimeout(800); return; } catch { throw new Error(`Navigation failed for ${url}`); } } export function getPage(): Page | null { return page; } export function isBrowserAlive(): boolean { return !!browser && browser.isConnected() && !!context && !!page && !page.isClosed(); } export async function closeBrowser(): Promise { try { if (page && !page.isClosed()) { await page.close().catch(() => {}); } page = null; if (context) { await context.close().catch(() => {}); } context = null; if (browser && browser.isConnected()) { await browser.close().catch(() => {}); } browser = null; } catch (e) { console.warn("[Browser] Close warning:", e); page = null; context = null; browser = null; } finally { console.log("[Browser] Session closed cleanly."); } }