import { join } from 'path' import { accessSync, chmodSync, constants, mkdirSync, unlinkSync } from 'fs' import type { ServerWebSocket, Subprocess } from 'bun' const DATA_DIR = process.env.DATA_DIR const PORT = Number(process.env.PORT) || 3000 const SOCKET_PATH = DATA_DIR ? join(DATA_DIR, 'tronbyt.sock') : '' const BIN_DIR = join(import.meta.dir, '..', 'bin') let goHealthy = false let goProcess: Subprocess | undefined interface WsData { path: string protocols: string[] } const upstreams = new Map, WebSocket>() // Proxy fetch to Go server over unix socket function proxyFetch(req: Request): Promise | Response { const url = new URL(req.url) if (url.pathname === '/ok') { if (!goHealthy) return new Response('starting', { status: goProcess ? 200 : 503 }) return fetch('http://localhost/health', { unix: SOCKET_PATH }) .then((r) => (r.ok ? new Response('ok') : new Response('unhealthy', { status: 503 }))) .catch(() => new Response('unhealthy', { status: 503 })) } return fetch(`http://localhost${url.pathname}${url.search}`, { method: req.method, headers: req.headers, body: req.body, unix: SOCKET_PATH, }).catch((e) => { console.error('Proxy error:', e) return new Response('Tronbyt server is not responding', { status: 502 }) }) } // WebSocket proxy const websocket = { open(ws: ServerWebSocket) { const upstream = new WebSocket(`ws+unix://${SOCKET_PATH}:${ws.data.path}`, ws.data.protocols) upstream.binaryType = 'arraybuffer' upstreams.set(ws, upstream) upstream.addEventListener('message', (e) => ws.send(e.data as string | ArrayBuffer)) upstream.addEventListener('close', () => { upstreams.delete(ws); ws.close() }) upstream.addEventListener('error', () => { upstreams.delete(ws); ws.close() }) }, message(ws: ServerWebSocket, msg: string | ArrayBuffer | Uint8Array) { const upstream = upstreams.get(ws) if (upstream?.readyState === WebSocket.OPEN) upstream.send(msg) }, close(ws: ServerWebSocket) { upstreams.get(ws)?.close() upstreams.delete(ws) }, } // Server const server = Bun.serve({ port: PORT, idleTimeout: 255, fetch(req, server) { if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') { const url = new URL(req.url) const protocolHeader = req.headers.get('sec-websocket-protocol') const protocols = protocolHeader ? protocolHeader.split(',').map((p) => p.trim()) : [] const headers: Record = {} if (protocolHeader) headers['sec-websocket-protocol'] = protocolHeader if (server.upgrade(req, { data: { path: url.pathname + url.search, protocols }, headers })) return return new Response('WebSocket upgrade failed', { status: 500 }) } return proxyFetch(req) }, websocket, }) console.log(`Listening on port ${server.port}`) // Go binary management function getBinaryName(): string { const platform = process.platform === 'darwin' ? 'darwin' : 'linux' const arch = process.arch === 'x64' ? 'amd64' : 'arm64' return `tronbyt-server-${platform}-${arch}` } async function waitForHealthy(maxAttempts = 60): Promise { for (let i = 0; i < maxAttempts; i++) { try { const resp = await fetch('http://localhost/health', { unix: SOCKET_PATH }) if (resp.ok) return true } catch {} await Bun.sleep(1000) } return false } async function downloadBinary(binPath: string): Promise { const name = getBinaryName() const url = `https://github.com/tronbyt/server/releases/latest/download/${name}` console.log(`Downloading ${name}...`) try { const resp = await fetch(url, { redirect: 'follow' }) if (!resp.ok) { console.error(`Download failed: ${resp.status} ${resp.statusText}`) return false } mkdirSync(BIN_DIR, { recursive: true }) await Bun.write(binPath, resp) chmodSync(binPath, 0o755) console.log('Download complete') return true } catch (e) { console.error('Download failed:', e) return false } } function validate(): string | undefined { if (!DATA_DIR) 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}` } } async function spawnGoServer() { const binPath = join(BIN_DIR, getBinaryName()) if (!(await Bun.file(binPath).exists())) { if (!(await downloadBinary(binPath))) return } const error = validate() if (error) { console.error(`Setup error: ${error}`) return } try { unlinkSync(SOCKET_PATH) } catch {} console.log('Starting tronbyt server...') goProcess = Bun.spawn([binPath], { env: { ...process.env, TRONBYT_UNIX_SOCKET: SOCKET_PATH, DATA_DIR, DB_DSN: join(DATA_DIR!, 'tronbyt.db'), PRODUCTION: process.env.PRODUCTION ?? 'false', 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', }) goProcess.exited.then((code) => { console.log(`Tronbyt server exited with code ${code}`) goHealthy = false goProcess = undefined }) if (await waitForHealthy()) { goHealthy = true console.log('Tronbyt server is healthy') } else { console.error('Tronbyt server failed to become healthy') } } function shutdown() { if (!goProcess) return console.log('Shutting down tronbyt server...') goProcess.kill('SIGTERM') } process.on('SIGTERM', shutdown) process.on('SIGINT', shutdown) spawnGoServer()