Compare commits
No commits in common. "main" and "probablycorey/browser-automation" have entirely different histories.
main
...
probablyco
103
src/binary.ts
103
src/binary.ts
|
|
@ -1,7 +1,6 @@
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { chmodSync, mkdirSync } from 'fs'
|
import { accessSync, chmodSync, constants, mkdirSync } from 'fs'
|
||||||
import type { Subprocess } from 'bun'
|
import type { Subprocess } from 'bun'
|
||||||
import { DATA_DIR, PRODUCTION, SINGLE_USER_AUTO_LOGIN, SYSTEM_APPS_AUTO_REFRESH } from './env'
|
|
||||||
|
|
||||||
const BIN_DIR = join(import.meta.dir, '..', 'bin')
|
const BIN_DIR = join(import.meta.dir, '..', 'bin')
|
||||||
const GO_PORT = 8000
|
const GO_PORT = 8000
|
||||||
|
|
@ -12,48 +11,6 @@ let proc: Subprocess | undefined
|
||||||
export const isHealthy = () => healthy
|
export const isHealthy = () => healthy
|
||||||
export const isRunning = () => !!proc
|
export const isRunning = () => !!proc
|
||||||
|
|
||||||
export async function spawn() {
|
|
||||||
const binPath = join(BIN_DIR, getBinaryName())
|
|
||||||
|
|
||||||
if (!(await Bun.file(binPath).exists())) {
|
|
||||||
if (!(await downloadBinary(binPath))) return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Starting tronbyt server...')
|
|
||||||
|
|
||||||
proc = Bun.spawn([binPath], {
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
DATA_DIR,
|
|
||||||
DB_DSN: join(DATA_DIR, 'tronbyt.db'),
|
|
||||||
PRODUCTION,
|
|
||||||
SINGLE_USER_AUTO_LOGIN,
|
|
||||||
SYSTEM_APPS_AUTO_REFRESH,
|
|
||||||
},
|
|
||||||
stdout: 'inherit',
|
|
||||||
stderr: 'inherit',
|
|
||||||
})
|
|
||||||
|
|
||||||
proc.exited.then((code) => {
|
|
||||||
console.log(`Tronbyt server exited with code ${code}`)
|
|
||||||
healthy = false
|
|
||||||
proc = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
if (await waitForHealthy()) {
|
|
||||||
healthy = true
|
|
||||||
console.log('Tronbyt server is healthy')
|
|
||||||
} else {
|
|
||||||
console.error('Tronbyt server failed to become healthy')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shutdown() {
|
|
||||||
if (!proc) return
|
|
||||||
console.log('Shutting down tronbyt server...')
|
|
||||||
proc.kill('SIGTERM')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBinaryName(): string {
|
function getBinaryName(): string {
|
||||||
const platform = process.platform === 'darwin' ? 'darwin' : 'linux'
|
const platform = process.platform === 'darwin' ? 'darwin' : 'linux'
|
||||||
const arch = process.arch === 'x64' ? 'amd64' : 'arm64'
|
const arch = process.arch === 'x64' ? 'amd64' : 'arm64'
|
||||||
|
|
@ -96,3 +53,61 @@ async function downloadBinary(binPath: string): Promise<boolean> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate(dataDir: string): string | undefined {
|
||||||
|
if (!dataDir) return 'DATA_DIR env var is not set — toes should provide this automatically'
|
||||||
|
|
||||||
|
const binPath = join(BIN_DIR, getBinaryName())
|
||||||
|
try {
|
||||||
|
accessSync(binPath, constants.X_OK)
|
||||||
|
} catch {
|
||||||
|
return `Binary not found or not executable: ${binPath}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function spawn(dataDir: string) {
|
||||||
|
const binPath = join(BIN_DIR, getBinaryName())
|
||||||
|
|
||||||
|
if (!(await Bun.file(binPath).exists())) {
|
||||||
|
if (!(await downloadBinary(binPath))) return
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = validate(dataDir)
|
||||||
|
if (error) {
|
||||||
|
console.error(`Setup error: ${error}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting tronbyt server...')
|
||||||
|
|
||||||
|
proc = Bun.spawn([binPath], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
DATA_DIR: dataDir,
|
||||||
|
DB_DSN: join(dataDir, 'tronbyt.db'),
|
||||||
|
PRODUCTION: process.env.PRODUCTION ?? 'true',
|
||||||
|
SINGLE_USER_AUTO_LOGIN: process.env.SINGLE_USER_AUTO_LOGIN ?? 'true',
|
||||||
|
SYSTEM_APPS_AUTO_REFRESH: process.env.SYSTEM_APPS_AUTO_REFRESH ?? 'true',
|
||||||
|
},
|
||||||
|
stdout: 'inherit',
|
||||||
|
stderr: 'inherit',
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.exited.then((code) => {
|
||||||
|
console.log(`Tronbyt server exited with code ${code}`)
|
||||||
|
healthy = false
|
||||||
|
proc = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
if (await waitForHealthy()) {
|
||||||
|
healthy = true
|
||||||
|
console.log('Tronbyt server is healthy')
|
||||||
|
} else {
|
||||||
|
console.error('Tronbyt server failed to become healthy')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shutdown() {
|
||||||
|
if (!proc) return
|
||||||
|
console.log('Shutting down tronbyt server...')
|
||||||
|
proc.kill('SIGTERM')
|
||||||
|
}
|
||||||
|
|
|
||||||
11
src/env.ts
11
src/env.ts
|
|
@ -1,11 +0,0 @@
|
||||||
function required(name: string): string {
|
|
||||||
const value = process.env[name]
|
|
||||||
if (!value) throw new Error(`Missing required env var: ${name}`)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DATA_DIR = required('DATA_DIR')
|
|
||||||
export const PORT = Number(process.env.PORT) || 3000
|
|
||||||
export const PRODUCTION = process.env.PRODUCTION ?? 'true'
|
|
||||||
export const SINGLE_USER_AUTO_LOGIN = process.env.SINGLE_USER_AUTO_LOGIN ?? 'true'
|
|
||||||
export const SYSTEM_APPS_AUTO_REFRESH = process.env.SYSTEM_APPS_AUTO_REFRESH ?? 'true'
|
|
||||||
68
src/proxy.ts
68
src/proxy.ts
|
|
@ -1,5 +1,4 @@
|
||||||
import type { ServerWebSocket } from 'bun'
|
import type { ServerWebSocket } from 'bun'
|
||||||
import { isHealthy, isRunning } from './binary'
|
|
||||||
|
|
||||||
export interface WsData {
|
export interface WsData {
|
||||||
path: string
|
path: string
|
||||||
|
|
@ -9,17 +8,19 @@ export interface WsData {
|
||||||
const GO_PORT = 8000
|
const GO_PORT = 8000
|
||||||
const GO_BASE = `http://127.0.0.1:${GO_PORT}`
|
const GO_BASE = `http://127.0.0.1:${GO_PORT}`
|
||||||
|
|
||||||
const WS_CONNECT_TIMEOUT = 5000
|
const upstreams = new Map<ServerWebSocket<WsData>, WebSocket>()
|
||||||
|
|
||||||
export function healthCheck(): Promise<Response> {
|
export function createProxy(isHealthy: () => boolean, isRunning: () => boolean) {
|
||||||
if (!isHealthy()) return Promise.resolve(new Response('starting', { status: isRunning() ? 200 : 503 }))
|
async function proxyFetch(req: Request): Promise<Response> {
|
||||||
|
const url = new URL(req.url)
|
||||||
|
|
||||||
|
if (url.pathname === '/ok') {
|
||||||
|
if (!isHealthy()) return new Response('starting', { status: isRunning() ? 200 : 503 })
|
||||||
return fetch(`${GO_BASE}/health`)
|
return fetch(`${GO_BASE}/health`)
|
||||||
.then((r) => (r.ok ? new Response('ok') : new Response('unhealthy', { status: 503 })))
|
.then((r) => (r.ok ? new Response('ok') : new Response('unhealthy', { status: 503 })))
|
||||||
.catch(() => new Response('unhealthy', { status: 503 }))
|
.catch(() => new Response('unhealthy', { status: 503 }))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function proxyFetch(req: Request): Promise<Response> {
|
|
||||||
const url = new URL(req.url)
|
|
||||||
const hasBody = req.method !== 'GET' && req.method !== 'HEAD'
|
const hasBody = req.method !== 'GET' && req.method !== 'HEAD'
|
||||||
const body = hasBody ? await req.arrayBuffer() : undefined
|
const body = hasBody ? await req.arrayBuffer() : undefined
|
||||||
|
|
||||||
|
|
@ -41,54 +42,27 @@ export async function proxyFetch(req: Request): Promise<Response> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpstreamState {
|
const websocket = {
|
||||||
socket: WebSocket
|
|
||||||
pending: (string | ArrayBuffer | Uint8Array)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const upstreams = new Map<ServerWebSocket<WsData>, UpstreamState>()
|
|
||||||
|
|
||||||
function cleanup(ws: ServerWebSocket<WsData>) {
|
|
||||||
const state = upstreams.get(ws)
|
|
||||||
if (!state) return
|
|
||||||
upstreams.delete(ws)
|
|
||||||
if (state.socket.readyState <= WebSocket.OPEN) state.socket.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const websocket = {
|
|
||||||
open(ws: ServerWebSocket<WsData>) {
|
open(ws: ServerWebSocket<WsData>) {
|
||||||
const socket = new WebSocket(`ws://127.0.0.1:${GO_PORT}${ws.data.path}`, ws.data.protocols)
|
const upstream = new WebSocket(`ws://127.0.0.1:${GO_PORT}${ws.data.path}`, ws.data.protocols)
|
||||||
socket.binaryType = 'arraybuffer'
|
upstream.binaryType = 'arraybuffer'
|
||||||
const state: UpstreamState = { socket, pending: [] }
|
upstreams.set(ws, upstream)
|
||||||
upstreams.set(ws, state)
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
upstream.addEventListener('message', (e) => ws.send(e.data as string | ArrayBuffer))
|
||||||
console.error('WebSocket upstream connection timed out')
|
upstream.addEventListener('close', () => { upstreams.delete(ws); ws.close() })
|
||||||
cleanup(ws)
|
upstream.addEventListener('error', () => { upstreams.delete(ws); ws.close() })
|
||||||
ws.close()
|
|
||||||
}, WS_CONNECT_TIMEOUT)
|
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
for (const msg of state.pending) socket.send(msg)
|
|
||||||
state.pending.length = 0
|
|
||||||
})
|
|
||||||
socket.addEventListener('message', (e) => ws.send(e.data as string | ArrayBuffer))
|
|
||||||
socket.addEventListener('close', () => { cleanup(ws); ws.close() })
|
|
||||||
socket.addEventListener('error', () => { clearTimeout(timeout); cleanup(ws); ws.close() })
|
|
||||||
},
|
},
|
||||||
|
|
||||||
message(ws: ServerWebSocket<WsData>, msg: string | ArrayBuffer | Uint8Array) {
|
message(ws: ServerWebSocket<WsData>, msg: string | ArrayBuffer | Uint8Array) {
|
||||||
const state = upstreams.get(ws)
|
const upstream = upstreams.get(ws)
|
||||||
if (!state) return
|
if (upstream?.readyState === WebSocket.OPEN) upstream.send(msg)
|
||||||
if (state.socket.readyState === WebSocket.OPEN) {
|
|
||||||
state.socket.send(msg)
|
|
||||||
} else {
|
|
||||||
state.pending.push(msg)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
close(ws: ServerWebSocket<WsData>) {
|
close(ws: ServerWebSocket<WsData>) {
|
||||||
cleanup(ws)
|
upstreams.get(ws)?.close()
|
||||||
|
upstreams.delete(ws)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { proxyFetch, websocket }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,25 @@
|
||||||
import { healthCheck, proxyFetch, websocket, type WsData } from './proxy'
|
import { createProxy, type WsData } from './proxy'
|
||||||
import { shutdown, spawn } from './binary'
|
import { isHealthy, isRunning, shutdown, spawn } from './binary'
|
||||||
import { PORT } from './env'
|
|
||||||
|
|
||||||
const server = Bun.serve<WsData>({
|
const DATA_DIR = process.env.DATA_DIR!
|
||||||
|
const PORT = Number(process.env.PORT) || 3000
|
||||||
|
|
||||||
|
const { proxyFetch, websocket } = createProxy(isHealthy, isRunning)
|
||||||
|
|
||||||
|
const server = Bun.serve({
|
||||||
port: PORT,
|
port: PORT,
|
||||||
hostname: '::',
|
hostname: '::',
|
||||||
idleTimeout: 255,
|
idleTimeout: 255,
|
||||||
|
|
||||||
fetch(req, server) {
|
fetch(req, server) {
|
||||||
const url = new URL(req.url)
|
|
||||||
|
|
||||||
if (url.pathname === '/ok') {
|
|
||||||
return healthCheck()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
||||||
|
const url = new URL(req.url)
|
||||||
const protocolHeader = req.headers.get('sec-websocket-protocol')
|
const protocolHeader = req.headers.get('sec-websocket-protocol')
|
||||||
const protocols = protocolHeader ? protocolHeader.split(',').map((p) => p.trim()) : []
|
const protocols = protocolHeader ? protocolHeader.split(',').map((p) => p.trim()) : []
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {}
|
||||||
if (protocolHeader) headers['sec-websocket-protocol'] = protocolHeader
|
if (protocolHeader) headers['sec-websocket-protocol'] = protocolHeader
|
||||||
|
|
||||||
const upgradeSuccessful = server.upgrade(req, { data: { path: url.pathname + url.search, protocols }, headers })
|
if (server.upgrade(req, { data: { path: url.pathname + url.search, protocols }, headers })) return
|
||||||
if (upgradeSuccessful) return
|
|
||||||
return new Response('WebSocket upgrade failed', { status: 500 })
|
return new Response('WebSocket upgrade failed', { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,4 +34,4 @@ console.log(`Listening on port ${server.port}`)
|
||||||
process.on('SIGTERM', shutdown)
|
process.on('SIGTERM', shutdown)
|
||||||
process.on('SIGINT', shutdown)
|
process.on('SIGINT', shutdown)
|
||||||
|
|
||||||
spawn()
|
spawn(DATA_DIR)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user