File size: 3,635 Bytes
5b1b707 06a22a1 5b1b707 11ba7a9 5b1b707 06a22a1 5b1b707 518ff8a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
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)
} |