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)
}