Spaces:
Paused
Paused
| const express = require("express"); | |
| const puppeteer = require("puppeteer-extra"); | |
| const StealthPlugin = require("puppeteer-extra-plugin-stealth"); | |
| const { execSync } = require("child_process"); | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const os = require("os"); | |
| puppeteer.use(StealthPlugin()); | |
| const app = express(); | |
| app.use(express.json({ limit: "10mb" })); | |
| const PORT = process.env.PORT || 7860; | |
| const POOL_SIZE = 2; | |
| const MAX_QUEUE = 20; | |
| const PAGE_TIMEOUT = 30000; | |
| const TMP_DIR = path.join(os.tmpdir(), "pp_files"); | |
| if (!fs.existsSync(TMP_DIR)) fs.mkdirSync(TMP_DIR, { recursive: true }); | |
| const pool = { | |
| browsers: [], | |
| available: [], | |
| waiters: [], | |
| stats: { totalRequests: 0, completed: 0, failed: 0, queued: 0 }, | |
| }; | |
| function getChromiumPath() { | |
| const candidates = [ | |
| "/usr/bin/chromium", | |
| "/usr/bin/chromium-browser", | |
| "/usr/bin/google-chrome", | |
| "/usr/bin/google-chrome-stable", | |
| "/snap/bin/chromium", | |
| ]; | |
| for (const p of candidates) { | |
| try { execSync(`test -f ${p}`); return p; } catch {} | |
| } | |
| return null; | |
| } | |
| async function launchBrowser(id) { | |
| const chromiumPath = getChromiumPath(); | |
| const opts = { | |
| headless: "new", | |
| args: [ | |
| "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", | |
| "--disable-gpu", "--no-zygote", "--single-process", | |
| "--disable-extensions", "--mute-audio", "--no-first-run", | |
| "--window-size=1280,720", | |
| ], | |
| ignoreHTTPSErrors: true, | |
| }; | |
| if (chromiumPath) opts.executablePath = chromiumPath; | |
| const browser = await puppeteer.launch(opts); | |
| browser._poolId = id; | |
| browser.on("disconnected", () => restartBrowser(id)); | |
| console.log(`[Pool] Browser #${id} up (PID: ${browser.process()?.pid})`); | |
| return browser; | |
| } | |
| async function restartBrowser(id) { | |
| pool.browsers[id] = null; | |
| pool.available = pool.available.filter((b) => b._poolId !== id); | |
| try { | |
| await new Promise((r) => setTimeout(r, 2000)); | |
| const browser = await launchBrowser(id); | |
| pool.browsers[id] = browser; | |
| releaseSlot(browser); | |
| } catch { | |
| setTimeout(() => restartBrowser(id), 5000); | |
| } | |
| } | |
| async function initPool() { | |
| console.log(`[Pool] Starting ${POOL_SIZE} browsers...`); | |
| for (let i = 0; i < POOL_SIZE; i++) { | |
| try { | |
| const browser = await launchBrowser(i); | |
| pool.browsers[i] = browser; | |
| pool.available.push(browser); | |
| } catch (e) { | |
| console.error(`[Pool] Browser #${i} failed:`, e.message); | |
| } | |
| } | |
| console.log(`[Pool] Ready β ${pool.available.length}/${POOL_SIZE} up`); | |
| } | |
| function acquireBrowser() { | |
| return new Promise((resolve, reject) => { | |
| if (pool.waiters.length >= MAX_QUEUE) | |
| return reject(new Error(`Queue full (max ${MAX_QUEUE})`)); | |
| if (pool.available.length > 0) { | |
| const b = pool.available.shift(); | |
| pool.stats.queued = pool.waiters.length; | |
| return resolve(b); | |
| } | |
| pool.waiters.push({ resolve, reject }); | |
| pool.stats.queued = pool.waiters.length; | |
| console.log(`[Queue] Waiting β size: ${pool.waiters.length}`); | |
| }); | |
| } | |
| function releaseSlot(browser) { | |
| if (pool.waiters.length > 0) { | |
| const w = pool.waiters.shift(); | |
| pool.stats.queued = pool.waiters.length; | |
| w.resolve(browser); | |
| } else { | |
| pool.available.push(browser); | |
| } | |
| } | |
| async function withPage(taskFn) { | |
| pool.stats.totalRequests++; | |
| const browser = await acquireBrowser(); | |
| let page = null; | |
| try { | |
| page = await browser.newPage(); | |
| await page.setUserAgent( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" | |
| ); | |
| await page.setViewport({ width: 1280, height: 720 }); | |
| await page.setDefaultNavigationTimeout(PAGE_TIMEOUT); | |
| await page.setDefaultTimeout(PAGE_TIMEOUT); | |
| const result = await taskFn(page, browser); | |
| pool.stats.completed++; | |
| return result; | |
| } finally { | |
| if (page && !page.isClosed()) await page.close().catch(() => {}); | |
| releaseSlot(browser); | |
| } | |
| } | |
| // βββ POST /run β kirim raw JS code string, page & utils tersedia ββββββββββββββ | |
| app.post("/run", async (req, res) => { | |
| const { code } = req.body; | |
| if (!code) return res.status(400).json({ error: "code required" }); | |
| const start = Date.now(); | |
| const logs = []; | |
| const tmpId = Date.now() + "_" + Math.random().toString(36).slice(2); | |
| const tmpBase = path.join(TMP_DIR, tmpId); | |
| try { | |
| const result = await withPage(async (page, browser) => { | |
| const fakeConsole = { | |
| log: (...a) => logs.push(a.map(String).join(" ")), | |
| error: (...a) => logs.push("[ERR] " + a.map(String).join(" ")), | |
| warn: (...a) => logs.push("[WARN] " + a.map(String).join(" ")), | |
| }; | |
| const ctx = { | |
| page, | |
| browser, | |
| console: fakeConsole, | |
| __file: tmpBase, | |
| goto: (url, opts) => page.goto(url, opts), | |
| screenshot: (opts = {}) => | |
| page.screenshot({ path: opts.path || `${tmpBase}.png`, fullPage: opts.fullPage || false, ...opts }), | |
| evaluate: (fn) => page.evaluate(fn), | |
| waitForSelector: (sel, opts) => page.waitForSelector(sel, opts), | |
| content: () => page.content(), | |
| url: () => page.url(), | |
| title: () => page.title(), | |
| click: (sel) => page.click(sel), | |
| type: (sel, text) => page.type(sel, text), | |
| select: (sel, val) => page.select(sel, val), | |
| waitForNavigation: (opts) => page.waitForNavigation(opts), | |
| setViewport: (vp) => page.setViewport(vp), | |
| setUserAgent: (ua) => page.setUserAgent(ua), | |
| }; | |
| const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; | |
| const fn = new AsyncFunction(...Object.keys(ctx), code); | |
| return await fn(...Object.values(ctx)); | |
| }); | |
| const files = []; | |
| const exts = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf", ".html", ".txt", ".json"]; | |
| for (const ext of exts) { | |
| const fp = `${tmpBase}${ext}`; | |
| if (fs.existsSync(fp)) { | |
| const buf = fs.readFileSync(fp); | |
| files.push({ | |
| fileName: path.basename(fp), | |
| extension: ext.replace(".", ""), | |
| base64: buf.toString("base64"), | |
| size: buf.length, | |
| }); | |
| fs.unlinkSync(fp); | |
| } | |
| } | |
| res.json({ ok: true, elapsed: Date.now() - start, result: result ?? null, logs, files }); | |
| } catch (err) { | |
| pool.stats.failed++; | |
| res.status(500).json({ ok: false, error: err.message, logs, elapsed: Date.now() - start }); | |
| } | |
| }); | |
| // βββ Standard endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/", (req, res) => { | |
| res.json({ | |
| service: "Puppeteer Pool Server", | |
| pool: { total: POOL_SIZE, available: pool.available.length, queued: pool.waiters.length }, | |
| stats: pool.stats, | |
| endpoints: { | |
| "GET /status": "Pool status", | |
| "POST /run": "Run raw code { code } β tersedia: page, browser, goto, screenshot, evaluate, waitForSelector, click, type, content, url, title", | |
| "POST /scrape": "Scrape { url, waitFor? }", | |
| "POST /screenshot": "Screenshot { url, fullPage? }", | |
| "POST /html": "Raw HTML { url }", | |
| "POST /custom": "JS evaluate { url, script }", | |
| }, | |
| }); | |
| }); | |
| app.get("/status", (req, res) => { | |
| res.json({ | |
| available: pool.available.length, | |
| queued: pool.waiters.length, | |
| total: POOL_SIZE, | |
| browsers: pool.browsers.map((b, i) => ({ | |
| id: i, alive: b !== null, connected: b?.isConnected?.() ?? false, | |
| })), | |
| stats: pool.stats, | |
| }); | |
| }); | |
| app.post("/scrape", async (req, res) => { | |
| const { url, waitFor } = req.body; | |
| if (!url) return res.status(400).json({ error: "url required" }); | |
| const start = Date.now(); | |
| try { | |
| const data = await withPage(async (page) => { | |
| await page.goto(url, { waitUntil: waitFor || "domcontentloaded" }); | |
| return await page.evaluate(() => ({ | |
| title: document.title, | |
| url: location.href, | |
| description: document.querySelector('meta[name="description"]')?.content || "", | |
| h1: Array.from(document.querySelectorAll("h1")).map((h) => h.innerText.trim()), | |
| text: document.body?.innerText?.slice(0, 3000) || "", | |
| })); | |
| }); | |
| res.json({ ok: true, elapsed: Date.now() - start, data }); | |
| } catch (err) { | |
| pool.stats.failed++; | |
| res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); | |
| } | |
| }); | |
| app.post("/screenshot", async (req, res) => { | |
| const { url, fullPage = false, waitFor } = req.body; | |
| if (!url) return res.status(400).json({ error: "url required" }); | |
| const start = Date.now(); | |
| try { | |
| const image = await withPage(async (page) => { | |
| await page.goto(url, { waitUntil: waitFor || "networkidle2" }); | |
| return await page.screenshot({ type: "png", fullPage, encoding: "base64" }); | |
| }); | |
| res.json({ ok: true, elapsed: Date.now() - start, image, mime: "image/png" }); | |
| } catch (err) { | |
| pool.stats.failed++; | |
| res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); | |
| } | |
| }); | |
| app.post("/html", async (req, res) => { | |
| const { url, waitFor } = req.body; | |
| if (!url) return res.status(400).json({ error: "url required" }); | |
| const start = Date.now(); | |
| try { | |
| const html = await withPage(async (page) => { | |
| await page.goto(url, { waitUntil: waitFor || "domcontentloaded" }); | |
| return await page.content(); | |
| }); | |
| res.json({ ok: true, elapsed: Date.now() - start, html }); | |
| } catch (err) { | |
| pool.stats.failed++; | |
| res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); | |
| } | |
| }); | |
| app.post("/custom", async (req, res) => { | |
| const { url, script, waitFor } = req.body; | |
| if (!url || !script) return res.status(400).json({ error: "url and script required" }); | |
| const start = Date.now(); | |
| try { | |
| const result = await withPage(async (page) => { | |
| await page.goto(url, { waitUntil: waitFor || "domcontentloaded" }); | |
| const fn = new Function(`return (${script})`); | |
| return await page.evaluate(fn()); | |
| }); | |
| res.json({ ok: true, elapsed: Date.now() - start, result }); | |
| } catch (err) { | |
| pool.stats.failed++; | |
| res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); | |
| } | |
| }); | |
| async function main() { | |
| await initPool(); | |
| app.listen(PORT, "0.0.0.0", () => { | |
| console.log(`\nπ Ready at http://0.0.0.0:${PORT}`); | |
| }); | |
| } | |
| main().catch((e) => { console.error("Fatal:", e); process.exit(1); }); | |