| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import puppeteer from "puppeteer";
- let browser;
- const pagePool = [];
- const MAX_PAGES = Number.parseInt(process.env.CHART_MAX_PAGES || "1", 10);
- const DEFAULT_TIMEOUT_MS = Number.parseInt(
- process.env.CHART_RENDER_TIMEOUT_MS || "",
- 10,
- );
- const EFFECTIVE_TIMEOUT_MS = Number.isFinite(DEFAULT_TIMEOUT_MS)
- ? DEFAULT_TIMEOUT_MS
- : 500000;
- const isBrowserAlive = () => browser && browser.isConnected();
- const clearPagePool = async () => {
- const pages = pagePool.splice(0, pagePool.length);
- await Promise.allSettled(
- pages.map(async (page) => {
- if (page && !page.isClosed()) await page.close();
- }),
- );
- };
- const createPage = async () => {
- if (!isBrowserAlive()) {
- await initBrowserPool();
- }
- const page = await browser.newPage();
- page.setDefaultTimeout(EFFECTIVE_TIMEOUT_MS);
- page.setDefaultNavigationTimeout(EFFECTIVE_TIMEOUT_MS);
- return page;
- };
- const isUsablePage = (page) =>
- page && !page.isClosed() && page.browser()?.isConnected();
- /**
- * 初始化浏览器 + 预创建 page
- */
- export const initBrowserPool = async () => {
- if (!isBrowserAlive()) {
- await clearPagePool();
- browser = null;
- browser = await puppeteer.launch({
- headless: "new",
- executablePath: process.env.CHROME_PATH || undefined,
- args: [
- "--no-sandbox",
- "--disable-setuid-sandbox",
- "--disable-dev-shm-usage",
- "--disable-background-timer-throttling",
- "--disable-backgrounding-occluded-windows",
- "--disable-renderer-backgrounding",
- ],
- // 避免渲染压力下 CDP 调用超时(对应报错:Runtime.callFunctionOn timed out)
- protocolTimeout: EFFECTIVE_TIMEOUT_MS,
- });
- browser.on("disconnected", () => {
- browser = null;
- pagePool.splice(0, pagePool.length);
- });
- console.log("✅ Browser initialized");
- // 预创建 page
- for (let i = 0; i < MAX_PAGES; i++) {
- pagePool.push(await createPage());
- }
- console.log(`✅ Page pool initialized: ${MAX_PAGES}`);
- }
- };
- /**
- * 获取 page(核心)
- */
- export const getPage = async () => {
- if (!isBrowserAlive()) {
- await initBrowserPool();
- }
- if (pagePool.length > 0) {
- while (pagePool.length > 0) {
- const page = pagePool.pop();
- if (isUsablePage(page)) return page;
- await discardPage(page);
- }
- }
- // pool 空了就新建(兜底)
- return createPage();
- };
- /**
- * 释放 page
- */
- export const releasePage = async (page) => {
- if (!page) return;
- try {
- if (!isUsablePage(page)) {
- await discardPage(page);
- return;
- }
- if (pagePool.length < MAX_PAGES) {
- pagePool.push(page);
- } else {
- await page.close();
- }
- } catch (err) {
- console.error("❌ releasePage error:", err);
- }
- };
- export const discardPage = async (page) => {
- if (!page) return;
- try {
- if (!page.isClosed()) await page.close();
- } catch (err) {
- console.warn("⚠️ discardPage failed:", err.message);
- }
- };
- /**
- * 关闭
- */
- export const closeBrowserPool = async () => {
- try {
- await clearPagePool();
- if (browser) {
- await browser.close();
- browser = null;
- }
- console.log("🛑 Browser pool closed");
- } catch (err) {
- console.error("❌ closeBrowserPool error:", err);
- }
- };
|