forked from imbytecat/fullstack-starter
feat(desktop): 实现生产模式下的内嵌服务器子进程支持
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"extends": ["//"],
|
"extends": ["//"],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
|
"dependsOn": ["@furtherverse/server#build"],
|
||||||
"outputs": ["build/**", "artifacts/**"]
|
"outputs": ["build/**", "artifacts/**"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user