|
|
|
|
@ -7,6 +7,14 @@
|
|
|
|
|
* /api/<name>/ routes, and stops it when the dev server stops. A backend that isn't built is skipped
|
|
|
|
|
* with a hint: plugins without a backend never notice this file.
|
|
|
|
|
*
|
|
|
|
|
* ★ ONE BACKEND PER `bun run dev`, SURVIVING VITE RESTARTS. Vite restarts itself whenever vite.config.js
|
|
|
|
|
* or anything it imports changes, and re-runs configureServer. Stopping the backend on every restart and
|
|
|
|
|
* starting a new one at once raced the old process for its port: the new one died with "address already
|
|
|
|
|
* in use" and Rudder showed "engine offline" until `bun run dev` was restarted by hand (measured
|
|
|
|
|
* 2026-09-27, twice in one session). Each backend now lives in a process-wide registry (globalThis — the
|
|
|
|
|
* config module itself is re-imported on a restart): a restart REUSES the running backend, it is stopped
|
|
|
|
|
* only when the dev process exits, and it is restarted (up to 5 times) if it crashes.
|
|
|
|
|
*
|
|
|
|
|
* DEV ONLY (`apply: 'serve'`). A production build never starts or bundles a backend.
|
|
|
|
|
*/
|
|
|
|
|
import { spawn } from 'node:child_process'
|
|
|
|
|
@ -30,9 +38,40 @@ export function devBackendProxy() {
|
|
|
|
|
return Object.fromEntries(DEV_BACKENDS.map((b) => [b.route, { target: `http://127.0.0.1:${b.port}`, changeOrigin: false }]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const REG = (globalThis.__dxDevBackends ||= { procs: new Map(), exitHooked: false, stopping: false })
|
|
|
|
|
const isAlive = (name) => { const e = REG.procs.get(name); return !!e && e.child.exitCode === null && e.child.signalCode === null }
|
|
|
|
|
|
|
|
|
|
function stopAll() {
|
|
|
|
|
REG.stopping = true
|
|
|
|
|
for (const { child } of REG.procs.values()) { try { child.kill('SIGTERM') } catch {} }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function start(root, b, logger, restarts = 0) {
|
|
|
|
|
const tag = `[${b.name}]`
|
|
|
|
|
const child = spawn(resolve(root, b.bin), [], { cwd: root, env: { ...process.env, ...b.env }, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
|
|
|
const entry = { child, restarts, logger }
|
|
|
|
|
REG.procs.set(b.name, entry)
|
|
|
|
|
const out = (d) => String(d).trim().split('\n').forEach((l) => l && entry.logger.info(`${tag} ${l}`))
|
|
|
|
|
child.stdout.on('data', out)
|
|
|
|
|
child.stderr.on('data', out)
|
|
|
|
|
child.on('exit', (code) => {
|
|
|
|
|
if (REG.procs.get(b.name) !== entry) return
|
|
|
|
|
REG.procs.delete(b.name)
|
|
|
|
|
if (REG.stopping) return
|
|
|
|
|
entry.logger.warn(`${tag} exited (${code})`)
|
|
|
|
|
// Crashed on its own: restart it, after its port has had a moment to free up.
|
|
|
|
|
if (entry.restarts < 5) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (REG.stopping || REG.procs.has(b.name)) return
|
|
|
|
|
entry.logger.warn(`${tag} restarting (${entry.restarts + 1}/5)`)
|
|
|
|
|
start(root, b, entry.logger, entry.restarts + 1)
|
|
|
|
|
}, 2000)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
logger.info(`${tag} started → ${b.route} on 127.0.0.1:${b.port}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function backendDevPlugin(root) {
|
|
|
|
|
const children = []
|
|
|
|
|
const alive = new Set() // backends whose process is running right now
|
|
|
|
|
return {
|
|
|
|
|
name: 'dx-backend-dev',
|
|
|
|
|
apply: 'serve',
|
|
|
|
|
@ -44,7 +83,7 @@ export function backendDevPlugin(root) {
|
|
|
|
|
// list. Registered here, so it runs before vite's proxy.
|
|
|
|
|
for (const b of DEV_BACKENDS) {
|
|
|
|
|
server.middlewares.use(b.route, (req, res, next) => {
|
|
|
|
|
if (alive.has(b.name)) return next()
|
|
|
|
|
if (isAlive(b.name)) return next()
|
|
|
|
|
if ((req.url || '').startsWith('/events')) {
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' })
|
|
|
|
|
res.write(`data: ${JSON.stringify({ type: 'status', status: { engine: 'offline', error: 'The desktop engine is not running.' } })}\n\n`)
|
|
|
|
|
@ -54,27 +93,25 @@ export function backendDevPlugin(root) {
|
|
|
|
|
res.end('[]')
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const logger = server.config.logger
|
|
|
|
|
if (!REG.exitHooked) {
|
|
|
|
|
REG.exitHooked = true
|
|
|
|
|
process.once('exit', stopAll)
|
|
|
|
|
process.once('SIGINT', () => { stopAll(); process.exit(0) })
|
|
|
|
|
process.once('SIGTERM', () => { stopAll(); process.exit(0) })
|
|
|
|
|
}
|
|
|
|
|
for (const b of DEV_BACKENDS) {
|
|
|
|
|
const bin = resolve(root, b.bin)
|
|
|
|
|
if (!existsSync(bin)) {
|
|
|
|
|
server.config.logger.warn(`[backend] ${b.name} not built — its plugin will show "engine offline". Build: ${b.build}`)
|
|
|
|
|
if (isAlive(b.name)) {
|
|
|
|
|
REG.procs.get(b.name).logger = logger // log through the restarted server
|
|
|
|
|
logger.info(`[${b.name}] still running → ${b.route} (kept across the dev-server restart)`)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const child = spawn(bin, [], { cwd: root, env: { ...process.env, ...b.env }, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
|
|
|
const tag = `[${b.name}]`
|
|
|
|
|
const out = (d) => String(d).trim().split('\n').forEach((l) => l && server.config.logger.info(`${tag} ${l}`))
|
|
|
|
|
child.stdout.on('data', out)
|
|
|
|
|
child.stderr.on('data', out)
|
|
|
|
|
child.on('exit', (code) => { alive.delete(b.name); server.config.logger.warn(`${tag} exited (${code})`) })
|
|
|
|
|
children.push(child)
|
|
|
|
|
alive.add(b.name)
|
|
|
|
|
server.config.logger.info(`${tag} started → ${b.route} on 127.0.0.1:${b.port}`)
|
|
|
|
|
if (!existsSync(resolve(root, b.bin))) {
|
|
|
|
|
logger.warn(`[backend] ${b.name} not built — its plugin will show "engine offline". Build: ${b.build}`)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
start(root, b, logger)
|
|
|
|
|
}
|
|
|
|
|
const stop = () => children.forEach((c) => { try { c.kill('SIGTERM') } catch {} })
|
|
|
|
|
server.httpServer?.on('close', stop)
|
|
|
|
|
process.once('exit', stop)
|
|
|
|
|
process.once('SIGINT', () => { stop(); process.exit(0) })
|
|
|
|
|
process.once('SIGTERM', () => { stop(); process.exit(0) })
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|