feat(desktop): ride an existing base app on :3000 instead of duplicating (S1)
When the always-on tunnel daemon already serves :3000, the app reuses it (close() is a no-op) rather than spawning a second server on another port, so the GUI and the tunnel share one base app.
This commit is contained in:
@@ -23,6 +23,8 @@ import { buildServerEnv } from './server-config.js'
|
||||
|
||||
/** Default listen port when the user hasn't pinned one (free-port fallback still applies). */
|
||||
const DEFAULT_PORT = 3000
|
||||
/** How long to wait for the probe of an already-running base app before giving up. */
|
||||
const PROBE_TIMEOUT_MS = 1500
|
||||
|
||||
export interface EmbeddedServerDeps {
|
||||
readonly prefs: DesktopPrefs
|
||||
@@ -34,6 +36,26 @@ interface ServerHandle {
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe whether a web-terminal base app is ALREADY serving on `port` (typically
|
||||
* the always-on tunnel daemon that owns :3000 + frpc). `GET /config/ui` returns a
|
||||
* small JSON object with `allowAutoMode` only from our backend, so it doubles as a
|
||||
* cheap "is this one of ours?" fingerprint. Any failure (nothing listening, wrong
|
||||
* shape, timeout) → false, and we spawn our own server as before.
|
||||
*/
|
||||
async function probeExistingBaseApp(port: number): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/config/ui`, {
|
||||
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
||||
})
|
||||
if (!res.ok) return false
|
||||
const body: unknown = await res.json()
|
||||
return typeof body === 'object' && body !== null && 'allowAutoMode' in body
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the embedded backend and return a lifecycle handle. Throws (after
|
||||
* logging) if the compiled server can't be imported or fails to start — the app
|
||||
@@ -41,7 +63,23 @@ interface ServerHandle {
|
||||
*/
|
||||
export async function startEmbeddedServer(deps: EmbeddedServerDeps): Promise<EmbeddedServer> {
|
||||
const { prefs, logger } = deps
|
||||
const port = await pickFreePort(prefs.port ?? DEFAULT_PORT)
|
||||
const preferred = prefs.port ?? DEFAULT_PORT
|
||||
|
||||
// If a base app is already up on the preferred port (the always-on tunnel
|
||||
// daemon), RIDE it instead of spawning a duplicate on another port: one base app
|
||||
// then serves the desktop UI, LAN, and the frpc tunnel alike. close() is a no-op
|
||||
// because we don't own that process — quitting the window must not kill it.
|
||||
if (await probeExistingBaseApp(preferred)) {
|
||||
logger.info(`reusing base app already on http://127.0.0.1:${preferred} (tunnel daemon) — not spawning a duplicate`)
|
||||
return {
|
||||
port: preferred,
|
||||
bindHost: '127.0.0.1',
|
||||
allowedOrigins: [],
|
||||
close: async () => {},
|
||||
}
|
||||
}
|
||||
|
||||
const port = await pickFreePort(preferred)
|
||||
const env = buildServerEnv({ prefs, port, platform: process.platform, env: process.env })
|
||||
|
||||
// dist/ is a sibling of the desktop dir in dev; under resourcesPath when packaged.
|
||||
|
||||
Reference in New Issue
Block a user