167 lines
4.1 KiB
TypeScript
167 lines
4.1 KiB
TypeScript
import { createServer } from 'node:net'
|
|
import { dirname, join } from 'node:path'
|
|
import { BrowserWindow, PATHS } from 'electrobun/bun'
|
|
|
|
const DEV_SERVER_URL = 'http://localhost:3000'
|
|
const SERVER_READY_TIMEOUT_MS = 5_000
|
|
const MAX_SPAWN_RETRIES = 3
|
|
|
|
const isDev = (): boolean => {
|
|
const env = process.env.ELECTROBUN_BUILD_ENV
|
|
return !env || env === 'dev'
|
|
}
|
|
|
|
const getServerEntryPath = (): string => {
|
|
return join(PATHS.VIEWS_FOLDER, '..', 'server-output', 'server', 'index.mjs')
|
|
}
|
|
|
|
const getFreePort = (): Promise<number> => {
|
|
return new Promise((resolve, reject) => {
|
|
const srv = createServer()
|
|
srv.unref()
|
|
srv.once('error', reject)
|
|
srv.listen({ port: 0, host: '127.0.0.1', exclusive: true }, () => {
|
|
const addr = srv.address()
|
|
if (addr && typeof addr === 'object') {
|
|
const port = addr.port
|
|
srv.close((err) => (err ? reject(err) : resolve(port)))
|
|
} else {
|
|
srv.close(() => reject(new Error('Unexpected address() result')))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
const waitForServer = async (
|
|
url: string,
|
|
timeoutMs = SERVER_READY_TIMEOUT_MS,
|
|
): Promise<boolean> => {
|
|
const start = Date.now()
|
|
while (Date.now() - start < timeoutMs) {
|
|
try {
|
|
const response = await fetch(url, { method: 'HEAD' })
|
|
if (response.ok) return true
|
|
} catch (_) {
|
|
// Server not up yet, retry after sleep
|
|
}
|
|
await Bun.sleep(100)
|
|
}
|
|
return false
|
|
}
|
|
|
|
const spawnServer = async (): Promise<{
|
|
process: ReturnType<typeof Bun.spawn>
|
|
url: string
|
|
}> => {
|
|
const serverEntryPath = getServerEntryPath()
|
|
const serverDir = dirname(serverEntryPath)
|
|
|
|
for (let attempt = 1; attempt <= MAX_SPAWN_RETRIES; attempt++) {
|
|
const port = await getFreePort()
|
|
const url = `http://127.0.0.1:${port}`
|
|
|
|
const serverProc = Bun.spawn([process.execPath, serverEntryPath], {
|
|
cwd: serverDir,
|
|
env: {
|
|
...process.env,
|
|
PORT: String(port),
|
|
HOST: '127.0.0.1',
|
|
},
|
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
})
|
|
|
|
const ready = await Promise.race([
|
|
waitForServer(url),
|
|
serverProc.exited.then((code) => {
|
|
throw new Error(`Server exited with code ${code} before becoming ready`)
|
|
}),
|
|
])
|
|
|
|
if (ready) {
|
|
return { process: serverProc, url }
|
|
}
|
|
|
|
serverProc.kill()
|
|
await serverProc.exited
|
|
console.warn(
|
|
`Server failed to become ready on port ${port} (attempt ${attempt}/${MAX_SPAWN_RETRIES})`,
|
|
)
|
|
}
|
|
|
|
throw new Error(`Server failed to start after ${MAX_SPAWN_RETRIES} attempts`)
|
|
}
|
|
|
|
const main = async () => {
|
|
console.log('Starting Furtherverse Desktop...')
|
|
|
|
let serverUrl: string
|
|
let serverProcess: ReturnType<typeof Bun.spawn> | null = null
|
|
|
|
if (isDev()) {
|
|
console.log('Dev mode: waiting for external server at', DEV_SERVER_URL)
|
|
const ready = await waitForServer(DEV_SERVER_URL)
|
|
if (!ready) {
|
|
console.error(
|
|
'Dev server not responding. Make sure to run: bun dev in apps/server',
|
|
)
|
|
process.exit(1)
|
|
}
|
|
console.log('Dev server ready!')
|
|
serverUrl = DEV_SERVER_URL
|
|
} else {
|
|
console.log('Production mode: starting embedded server...')
|
|
try {
|
|
const server = await spawnServer()
|
|
serverProcess = server.process
|
|
serverUrl = server.url
|
|
console.log('Server ready at', serverUrl)
|
|
} catch (err) {
|
|
console.error('Failed to start embedded server:', err)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
new BrowserWindow({
|
|
title: 'Furtherverse',
|
|
url: serverUrl,
|
|
frame: {
|
|
x: 100,
|
|
y: 100,
|
|
width: 1200,
|
|
height: 800,
|
|
},
|
|
renderer: 'cef',
|
|
})
|
|
|
|
if (serverProcess) {
|
|
const cleanup = () => {
|
|
if (serverProcess) {
|
|
serverProcess.kill()
|
|
serverProcess = null
|
|
}
|
|
}
|
|
|
|
process.on('exit', cleanup)
|
|
process.on('SIGTERM', () => {
|
|
cleanup()
|
|
process.exit(0)
|
|
})
|
|
process.on('SIGINT', () => {
|
|
cleanup()
|
|
process.exit(0)
|
|
})
|
|
|
|
serverProcess.exited.then((code) => {
|
|
if (serverProcess) {
|
|
console.error(`Server exited unexpectedly with code ${code}`)
|
|
process.exit(1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error('Failed to start:', error)
|
|
process.exit(1)
|
|
})
|