| 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<void> { |
| 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<void> { |
| 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<void> { |
| 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."); |
| } |
| } |