Files changed (4) hide show
  1. Dockerfile +11 -0
  2. index.js +62 -0
  3. package.json +9 -0
  4. worker.js +27 -0
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM oven/bun:latest
2
+ ENV CHROME_BIN=/usr/bin/chromium \
3
+ PORT=7860 \
4
+ TZ=Asia/Jakarta
5
+ RUN apt-get update
6
+ RUN apt-get install -y chromium
7
+ RUN apt-get clean
8
+ WORKDIR /app
9
+ COPY . .
10
+ RUN bun i
11
+ CMD ["bun", "."]
index.js ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const bytes = require('bytes')
2
+ const express = require('express')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+ const util = require('node:util')
6
+ const { Worker } = require('node:worker_threads')
7
+
8
+ const app = express()
9
+
10
+ app.set('json spaces', 4)
11
+ app.use(express.json())
12
+ app.use(express.urlencoded({ extended: true }))
13
+
14
+ const runBrowserScript = (code) => new Promise((resolve, reject) => {
15
+ const worker = new Worker(path.resolve(__dirname, 'worker.js'))
16
+ worker.postMessage(code)
17
+ worker.on('message', ({ result, error }) =>
18
+ error ? reject(new Error(error)) : resolve(result)
19
+ )
20
+ worker.on('error', reject)
21
+ worker.on('exit', (code) =>
22
+ (code !== 0) && reject(new Error(`Worker exited with code ${code}`))
23
+ )
24
+ })
25
+
26
+ app.use((req, res, next) => {
27
+ const time = new Date().toLocaleString('id', { timeZone: 'Asia/Jakarta' })
28
+ console.log('[%s] %s: %s', time, req.method, req.url)
29
+ next()
30
+ })
31
+
32
+ app.all('/', (_, res) => {
33
+ const formatSize = (n) => bytes(+n, { unitSeparator: ' ' })
34
+ const status = {}
35
+ const used = process.memoryUsage()
36
+ for (let x in used) status[x] = formatSize(used[x])
37
+
38
+ const totalmem = os.totalmem()
39
+ const freemem = os.freemem()
40
+ status['memoryUsage'] =
41
+ `${formatSize(totalmem - freemem)} / ${formatSize(totalmem)}`
42
+
43
+ res.json({
44
+ uptime: new Date(process.uptime() * 1000).toUTCString().split(' ')[4],
45
+ status
46
+ })
47
+ })
48
+
49
+
50
+ app.post('/run-browser', async (req, res) => {
51
+ const { code } = req.body
52
+ if (!code) return res.status(400).json({ error: 'Code is required' })
53
+ try {
54
+ const result = await runBrowserScript(code)
55
+ res.json({ result })
56
+ } catch (e) {
57
+ res.status(500).json({ error: util.format(e) })
58
+ }
59
+ })
60
+
61
+ const PORT = process.env.SPACE_ID ? 7860 : process.env.PORT || 3000
62
+ app.listen(PORT, () => console.log(`Playwright API listening at http://localhost:${PORT}`))
package.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "playwright-sandbox-api",
3
+ "version": "1.0.0",
4
+ "description": "API to run user-submitted Playwright scripts in an isolated sandbox using worker threads",
5
+ "dependencies": {
6
+ "express": "*",
7
+ "playwright": "*"
8
+ }
9
+ }
worker.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { parentPort } = require('node:worker_threads')
2
+ const playwright = require('playwright')
3
+
4
+ const runUserScript = async (code) => {
5
+ const AsyncFunction =
6
+ Object.getPrototypeOf(async function () {}).constructor
7
+
8
+ const userFunc = new AsyncFunction(
9
+ 'console',
10
+ 'playwright',
11
+ code
12
+ )
13
+
14
+ return await userFunc(
15
+ console,
16
+ playwright
17
+ )
18
+ }
19
+
20
+ parentPort.on('message', async (code) => {
21
+ try {
22
+ const result = await runUserScript(code)
23
+ parentPort.postMessage({ result })
24
+ } catch (e) {
25
+ parentPort.postMessage({ error: e })
26
+ }
27
+ })