| import { serve } from '@hono/node-server' |
| import { serveStatic } from '@hono/node-server/serve-static' |
| import { Hono } from 'hono' |
| import { logger } from 'hono/logger' |
| import { prettyJSON } from 'hono/pretty-json' |
| import os from 'node:os' |
| import { format } from 'node:util' |
| import { isMainThread, parentPort, Worker } from 'node:worker_threads' |
| import playwright from 'playwright-extra' |
| import prettyBytes from 'pretty-bytes' |
| import prettyMs from 'pretty-ms' |
| import pluginStealth from 'puppeteer-extra-plugin-stealth' |
|
|
| const TIMEOUT_MS = 6e4 |
| const MAX_CODE_LENGTH = 6e4 |
|
|
| if (!isMainThread) { |
| const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor |
| parentPort.on('message', async (code) => { |
| try { |
| const fn = new AsyncFunction( |
| 'playwright', |
| 'pluginStealth', |
| 'console', |
| code |
| ) |
| parentPort.postMessage({ |
| result: await fn(playwright, pluginStealth, console) |
| }) |
| } catch (e) { |
| parentPort.postMessage({ error: format(e) }) |
| } |
| }) |
| } else { |
| const isNumber = (v) => typeof v === 'number' && !isNaN(v) |
|
|
| const transformObj = (obj, cb) => |
| JSON.parse( |
| JSON.stringify(obj, (key, val) => |
| cb(key, val) ? prettyBytes(val) : val |
| ).replace(/_(\w)/g, (_, g) => g.toUpperCase()) |
| ) |
|
|
| const getServerStats = () => { |
| const stats = {} |
| stats.uptime = prettyMs(process.uptime() * 1e3) |
| stats.osUptime = prettyMs(os.uptime() * 1e3) |
|
|
| const report = process.report?.getReport?.() |
| if (report) |
| Object.assign(stats, { |
| header: report.header, |
| javascriptHeap: transformObj( |
| report.javascriptHeap, |
| (k, v) => !/(ContextCount|Garbage)/i.test(k) && isNumber(v) |
| ), |
| resourceUsage: transformObj( |
| report.resourceUsage, |
| (k, v) => |
| !/(Percent|IO|reads|write)/i.test(k) && isNumber(v) |
| ), |
| uvthreadResourceUsage: transformObj( |
| report.uvthreadResourceUsage, |
| (k, v) => isNumber(v) |
| ) |
| }) |
|
|
| stats.memoryUsage = transformObj(process.memoryUsage(), (k, v) => |
| isNumber(v) |
| ) |
|
|
| return stats |
| } |
|
|
| const runBrowserScript = (code) => |
| new Promise((resolve, reject) => { |
| const worker = new Worker(new URL(import.meta.url), { |
| resourceLimits: { maxOldGenerationSizeMb: 512 } |
| }) |
|
|
| const timer = setTimeout(() => { |
| worker.terminate() |
| reject(new Error('Execution timeout')) |
| }, TIMEOUT_MS) |
|
|
| const cleanup = () => { |
| clearTimeout(timer) |
| worker.terminate() |
| } |
|
|
| worker.postMessage(code) |
| worker.on('message', ({ result, error }) => { |
| cleanup() |
| error ? reject(new Error(error)) : resolve(format(result)) |
| }) |
| worker.on('error', (e) => { |
| cleanup() |
| reject(e) |
| }) |
| worker.on('exit', (exitCode) => { |
| clearTimeout(timer) |
| exitCode !== 0 && |
| reject(new Error(`Worker exited with code ${exitCode}`)) |
| }) |
| }) |
|
|
| const app = new Hono() |
|
|
| app.use(logger()) |
| app.use(prettyJSON({ force: true })) |
| app.use( |
| '/file/*', |
| serveStatic({ |
| root: os.tmpdir(), |
| rewriteRequestPath: (path) => path.replace(/^\/file/, '') |
| }) |
| ) |
|
|
| app.get('/', (c) => c.json(getServerStats())) |
|
|
| app.post('/run', async (c) => { |
| const body = await c.req.json().catch(() => ({})) |
| const { code } = body |
|
|
| if (!code) return c.json({ error: 'Code is required' }, 400) |
| if (typeof code !== 'string') |
| return c.json({ error: 'Code must be a string' }, 400) |
| if (code.length > MAX_CODE_LENGTH) |
| return c.json({ error: 'Code too long' }, 400) |
|
|
| try { |
| return c.json({ result: await runBrowserScript(code) }) |
| } catch (e) { |
| return c.json({ error: format(e) }, 500) |
| } |
| }) |
|
|
| const port = process.env.SPACE_ID ? 7860 : +(process.env.PORT || 3000) |
| serve({ fetch: app.fetch, port }, console.log) |
| } |