forked from imbytecat/fullstack-starter
feat(desktop): 实现生产模式下的内嵌服务器子进程支持
This commit is contained in:
@@ -10,6 +10,15 @@ export default {
|
||||
bun: {
|
||||
entrypoint: 'src/bun/index.ts',
|
||||
},
|
||||
copy: {
|
||||
'../server/.output': 'server-output',
|
||||
},
|
||||
mac: {
|
||||
bundleCEF: true,
|
||||
},
|
||||
win: {
|
||||
bundleCEF: true,
|
||||
},
|
||||
linux: {
|
||||
bundleCEF: true,
|
||||
},
|
||||
|
||||
@@ -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 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()
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' })
|
||||
if (response.ok) return true
|
||||
} catch {
|
||||
await Bun.sleep(100)
|
||||
} catch (_) {
|
||||
// Server not up yet, retry after sleep
|
||||
}
|
||||
await Bun.sleep(100)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting Furtherverse Desktop...')
|
||||
console.log('Waiting for dev server at', DEV_SERVER_URL)
|
||||
const spawnServer = async (): Promise<{
|
||||
process: ReturnType<typeof Bun.spawn>
|
||||
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)
|
||||
if (!ready) {
|
||||
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)
|
||||
}
|
||||
|
||||
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: DEV_SERVER_URL,
|
||||
url: serverUrl,
|
||||
frame: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
@@ -41,7 +140,31 @@ async function main() {
|
||||
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) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["@furtherverse/server#build"],
|
||||
"outputs": ["build/**", "artifacts/**"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user