refactor(desktop): 使用预分配端口替代 stdout 解析获取服务器端口

This commit is contained in:
2026-02-07 19:32:56 +08:00
parent 14bcdb33af
commit 3306e18395
2 changed files with 53 additions and 59 deletions

View File

@@ -1,9 +1,10 @@
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 = 30_000
const PORT_PATTERN = /Listening on:?\s*https?:\/\/[^\s:]+:(\d+)/
const SERVER_READY_TIMEOUT_MS = 5_000
const MAX_SPAWN_RETRIES = 3
const isDev = (): boolean => {
const env = process.env.ELECTROBUN_BUILD_ENV
@@ -14,6 +15,23 @@ 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,
@@ -38,64 +56,39 @@ const spawnServer = async (): Promise<{
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',
})
for (let attempt = 1; attempt <= MAX_SPAWN_RETRIES; attempt++) {
const port = await getFreePort()
const url = `http://127.0.0.1:${port}`
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`))
const serverProc = Bun.spawn([process.execPath, serverEntryPath], {
cwd: serverDir,
env: {
...process.env,
PORT: String(port),
HOST: '127.0.0.1',
},
stdio: ['ignore', 'inherit', 'inherit'],
})
read()
})
const ready = await Promise.race([
waitForServer(url),
serverProc.exited.then((code) => {
throw new Error(`Server exited with code ${code} before becoming ready`)
}),
])
return { process: serverProc, url: `http://127.0.0.1:${port}` }
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 () => {