From da6f669f88e0e80ea137669a02f1f6234fd54171 Mon Sep 17 00:00:00 2001 From: 2977094657 <2977094657@qq.com> Date: Mon, 2 Mar 2026 17:53:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(desktop-runtime):=20=E6=89=93=E5=8C=85?= =?UTF-8?q?=E7=89=88=E5=90=8E=E7=AB=AF=E7=AB=AF=E5=8F=A3=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E6=97=B6=E8=87=AA=E5=8A=A8=E6=8D=A2=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E5=B9=B6=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启动前检测端口可用性,不可用则选择可用端口并持久化 - waitForBackend 失败时重启后端重试一次,提升启动成功率 - dev 环境不自动改端口,避免干扰本地调试 --- desktop/src/main.cjs | 100 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/desktop/src/main.cjs b/desktop/src/main.cjs index f4a2082..ba96a27 100644 --- a/desktop/src/main.cjs +++ b/desktop/src/main.cjs @@ -123,6 +123,68 @@ function isPortAvailable(port, host) { }); } +function getEphemeralPort(host) { + return new Promise((resolve) => { + try { + const srv = net.createServer(); + srv.unref(); + srv.once("error", () => resolve(null)); + srv.listen({ port: 0, host }, () => { + const addr = srv.address(); + const p = addr && typeof addr === "object" ? Number(addr.port) : null; + srv.close(() => resolve(Number.isInteger(p) ? p : null)); + }); + } catch { + resolve(null); + } + }); +} + +async function chooseAvailablePort(preferredPort, host) { + const preferred = parsePort(preferredPort); + if (preferred != null && (await isPortAvailable(preferred, host))) return preferred; + + // Keep the port close to the user's expectation when possible. + if (preferred != null) { + for (let i = 1; i <= 50; i += 1) { + const cand = preferred + i; + if (cand > 65535) break; + if (await isPortAvailable(cand, host)) return cand; + } + } + + // Fall back to an OS-chosen ephemeral port. + const random = await getEphemeralPort(host); + if (random != null && (await isPortAvailable(random, host))) return random; + + return null; +} + +async function ensureBackendPortAvailableOnStartup() { + // Avoid surprising behavior in dev: the frontend dev server expects a stable backend port. + if (!app.isPackaged) return getBackendPort(); + + const bindHost = getBackendBindHost(); + const currentPort = getBackendPort(); + const ok = await isPortAvailable(currentPort, bindHost); + if (ok) return currentPort; + + const chosen = await chooseAvailablePort(currentPort, bindHost); + if (chosen == null) { + logMain(`[main] backend port unavailable: ${currentPort} host=${bindHost}; failed to find a free port`); + return currentPort; + } + + try { + setBackendPortSetting(chosen); + logMain(`[main] backend port ${currentPort} unavailable; switched to ${chosen}`); + } catch (err) { + logMain(`[main] failed to persist backend port ${chosen}: ${err?.message || err}`); + } + + return getBackendPort(); +} + function resolveDataDir() { if (resolvedDataDir) return resolvedDataDir; @@ -1024,6 +1086,16 @@ async function waitForBackend({ timeoutMs, healthUrl } = {}) { const startedAt = Date.now(); // eslint-disable-next-line no-constant-condition while (true) { + // If the backend process died, fail fast (otherwise we'd wait for the full timeout). + if (!backendProc) { + throw new Error(`Backend process exited before becoming ready: ${url}`); + } + if (backendProc.exitCode != null) { + throw new Error( + `Backend process exited (code=${backendProc.exitCode} signal=${backendProc.signalCode || "null"}): ${url}` + ); + } + try { const code = await httpGet(url); if (code >= 200 && code < 500) return; @@ -1379,11 +1451,37 @@ async function main() { // next to the installed exe for easier access. resolveDataDir(); ensureOutputLink(); + await ensureBackendPortAvailableOnStartup(); logMain(`[main] app.isPackaged=${app.isPackaged} argv=${JSON.stringify(process.argv)}`); startBackend(); - await waitForBackend({ timeoutMs: 30_000 }); + try { + await waitForBackend({ timeoutMs: 30_000 }); + } catch (err) { + // In some environments a specific port may be blocked/reserved (WSAEACCES) or taken. + // Best-effort: pick a new port and retry once so the app can still start. + if (app.isPackaged) { + const prevPort = getBackendPort(); + const bindHost = getBackendBindHost(); + const nextPort = await chooseAvailablePort(prevPort + 1, bindHost); + if (nextPort != null && nextPort !== prevPort) { + logMain(`[main] backend not ready on port ${prevPort}; retrying on ${nextPort}`); + try { + setBackendPortSetting(nextPort); + await restartBackend({ timeoutMs: 30_000 }); + logMain(`[main] backend retry succeeded on port ${nextPort}`); + } catch (retryErr) { + logMain(`[main] backend retry failed: ${retryErr?.stack || String(retryErr)}`); + throw retryErr; + } + } else { + throw err; + } + } else { + throw err; + } + } const win = createMainWindow(); mainWindow = win;