feat(desktop): 实现生产模式下的内嵌服务器子进程支持

This commit is contained in:
2026-02-07 18:49:41 +08:00
parent 6b12745e50
commit b7a6a793a3
3 changed files with 149 additions and 16 deletions

View File

@@ -10,6 +10,15 @@ export default {
bun: { bun: {
entrypoint: 'src/bun/index.ts', entrypoint: 'src/bun/index.ts',
}, },
copy: {
'../server/.output': 'server-output',
},
mac: {
bundleCEF: true,
},
win: {
bundleCEF: true,
},
linux: { linux: {
bundleCEF: true, bundleCEF: true,
}, },

View File

@@ -1,37 +1,136 @@
import Electrobun, { BrowserWindow } from 'electrobun/bun' import { dirname, join } from 'node:path'
import { BrowserWindow, PATHS } from 'electrobun/bun'
const DEV_SERVER_URL = 'http://localhost:3000' const DEV_SERVER_URL = 'http://localhost:3000'
const SERVER_READY_TIMEOUT_MS = 30_000
const PORT_PATTERN = /Listening on:?\s*https?:\/\/[^\s:]+:(\d+)/
async function waitForServer(url: string, timeoutMs = 30000): Promise<boolean> { 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 waitForServer = async (
url: string,
timeoutMs = SERVER_READY_TIMEOUT_MS,
): Promise<boolean> => {
const start = Date.now() const start = Date.now()
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
try { try {
const response = await fetch(url, { method: 'HEAD' }) const response = await fetch(url, { method: 'HEAD' })
if (response.ok) return true if (response.ok) return true
} catch { } catch (_) {
await Bun.sleep(100) // Server not up yet, retry after sleep
} }
await Bun.sleep(100)
} }
return false return false
} }
async function main() { const spawnServer = async (): Promise<{
console.log('Starting Furtherverse Desktop...') process: ReturnType<typeof Bun.spawn>
console.log('Waiting for dev server at', DEV_SERVER_URL) url: string
}> => {
const serverEntryPath = getServerEntryPath()
const serverDir = dirname(serverEntryPath)
const serverProc = Bun.spawn([process.execPath, serverEntryPath], {
cwd: serverDir,
env: {
...process.env,
PORT: '0',
HOST: '127.0.0.1',
},
stdout: 'pipe',
stderr: 'pipe',
})
const port = await new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
serverProc.kill()
reject(
new Error(`Server did not start within ${SERVER_READY_TIMEOUT_MS}ms`),
)
}, SERVER_READY_TIMEOUT_MS)
const reader = serverProc.stdout.getReader()
let buffer = ''
const read = async () => {
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
clearTimeout(timeout)
reject(new Error('Server process exited before reporting port'))
return
}
const text = new TextDecoder().decode(value)
buffer += text
process.stdout.write(text)
const match = PORT_PATTERN.exec(buffer)
if (match?.[1]) {
clearTimeout(timeout)
resolve(match[1])
return
}
}
} catch (err) {
clearTimeout(timeout)
reject(err)
}
}
serverProc.exited.then((code) => {
clearTimeout(timeout)
reject(new Error(`Server exited with code ${code} before becoming ready`))
})
read()
})
return { process: serverProc, url: `http://127.0.0.1:${port}` }
}
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) const ready = await waitForServer(DEV_SERVER_URL)
if (!ready) { if (!ready) {
console.error( console.error(
'Dev server not responding. Make sure to run: cd apps/server && bun dev', 'Dev server not responding. Make sure to run: bun dev in apps/server',
) )
process.exit(1) process.exit(1)
} }
console.log('Dev server ready!') 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({ new BrowserWindow({
title: 'Furtherverse', title: 'Furtherverse',
url: DEV_SERVER_URL, url: serverUrl,
frame: { frame: {
x: 100, x: 100,
y: 100, y: 100,
@@ -41,7 +140,31 @@ async function main() {
renderer: 'cef', renderer: 'cef',
}) })
Electrobun.events.on('will-quit', () => console.log('Quitting...')) 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) => { main().catch((error) => {

View File

@@ -3,6 +3,7 @@
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": ["@furtherverse/server#build"],
"outputs": ["build/**", "artifacts/**"] "outputs": ["build/**", "artifacts/**"]
} }
} }