browser / server.js
nekoyama12's picture
Update server.js
f0b8262 verified
Raw
History Blame Contribute Delete
10.5 kB
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); });