When `orca serve --port <P>` is set, bind the pinned port before any persisted mobile-ws-fallback-port.json entry so clients dialing <P> are not stranded. Default auto/desktop keep STA-1511 fallback-first.
This commit is contained in:
parent
655f846d2c
commit
1cdc8e85b3
|
|
@ -2178,7 +2178,15 @@ app.whenReady().then(async () => {
|
|||
enableWebSocket: true,
|
||||
...(isE2E ? { wsPort: 0 } : {}),
|
||||
...(devWsPort !== undefined ? { wsPort: devWsPort } : {}),
|
||||
...(serveOptions?.wsPort !== undefined ? { wsPort: serveOptions.wsPort } : {}),
|
||||
...(serveOptions?.wsPort !== undefined
|
||||
? {
|
||||
wsPort: serveOptions.wsPort,
|
||||
// Why: only an explicit `orca serve --port` pin prefers the requested
|
||||
// port over a stale STA-1511 fallback (issue #8535). Default 6768 /
|
||||
// dev 6769 keep fallback-first so mobile pairings stay stable.
|
||||
preferPinnedWsPort: true
|
||||
}
|
||||
: {}),
|
||||
webClientRoot: getBundledWebClientRoot()
|
||||
})
|
||||
registerMobileHandlers(runtimeRpc, { getRelayStatus: () => desktopRelayStatus })
|
||||
|
|
|
|||
|
|
@ -472,6 +472,43 @@ describe('WebSocketTransport', () => {
|
|||
expect(transport.resolvedPort).toBe(fallbackPort)
|
||||
})
|
||||
|
||||
it('binds the preferred port first when preferPinnedPort is set and both are free', async () => {
|
||||
// Why: issue #8535 — `orca serve --port <P>` clients dial the pin. A
|
||||
// free but stale mobile-ws-fallback-port.json must not pre-empt it.
|
||||
const preferredPort = await reserveFreePort()
|
||||
const fallbackPort = await reserveFreePort()
|
||||
|
||||
const transport = new WebSocketTransport({
|
||||
host: '127.0.0.1',
|
||||
port: preferredPort,
|
||||
fallbackPort,
|
||||
preferPinnedPort: true
|
||||
})
|
||||
transports.push(transport)
|
||||
await transport.start()
|
||||
expect(transport.resolvedPort).toBe(preferredPort)
|
||||
})
|
||||
|
||||
it('falls back when preferPinnedPort is set but the preferred port is taken', async () => {
|
||||
// Why: explicit pins still degrade to the STA-1511 fallback on
|
||||
// EADDRINUSE so previously-paired mobile devices remain reachable.
|
||||
const preferredHolder = new WebSocketTransport({ host: '127.0.0.1', port: 0 })
|
||||
transports.push(preferredHolder)
|
||||
await preferredHolder.start()
|
||||
const preferredPort = preferredHolder.resolvedPort
|
||||
const fallbackPort = await reserveFreePort()
|
||||
|
||||
const transport = new WebSocketTransport({
|
||||
host: '127.0.0.1',
|
||||
port: preferredPort,
|
||||
fallbackPort,
|
||||
preferPinnedPort: true
|
||||
})
|
||||
transports.push(transport)
|
||||
await transport.start()
|
||||
expect(transport.resolvedPort).toBe(fallbackPort)
|
||||
})
|
||||
|
||||
it('binds the preferred port when the persisted fallback is taken', async () => {
|
||||
const fallbackHolder = new WebSocketTransport({ host: '127.0.0.1', port: 0 })
|
||||
transports.push(fallbackHolder)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ export type WebSocketTransportOptions = {
|
|||
// the (now free) preferred port instead would strand those pairings
|
||||
// (STA-1511). Callers pass the previously assigned fallback port here.
|
||||
fallbackPort?: number
|
||||
// Why: `orca serve --port <P>` clients dial the pinned port. Prefer that port
|
||||
// first (fallback second) so a stale mobile-ws-fallback-port.json cannot
|
||||
// silently steal the pin (issue #8535). Default auto/desktop keeps
|
||||
// fallback-first for STA-1511 pairing stability.
|
||||
preferPinnedPort?: boolean
|
||||
}
|
||||
|
||||
export class WebSocketTransport implements RpcTransport {
|
||||
|
|
@ -65,6 +70,7 @@ export class WebSocketTransport implements RpcTransport {
|
|||
private readonly preAuthTimeoutMs: number
|
||||
private readonly staticRoot: string | undefined
|
||||
private readonly fallbackPort: number | undefined
|
||||
private readonly preferPinnedPort: boolean
|
||||
private httpServer: HttpsServer | HttpServer | null = null
|
||||
private wss: WebSocketServer | null = null
|
||||
private heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
|
@ -89,7 +95,8 @@ export class WebSocketTransport implements RpcTransport {
|
|||
heartbeatIntervalMs,
|
||||
preAuthTimeoutMs,
|
||||
staticRoot,
|
||||
fallbackPort
|
||||
fallbackPort,
|
||||
preferPinnedPort
|
||||
}: WebSocketTransportOptions) {
|
||||
this.host = host
|
||||
this.port = port
|
||||
|
|
@ -99,6 +106,7 @@ export class WebSocketTransport implements RpcTransport {
|
|||
this.preAuthTimeoutMs = preAuthTimeoutMs ?? PRE_AUTH_TIMEOUT_MS
|
||||
this.staticRoot = staticRoot
|
||||
this.fallbackPort = fallbackPort
|
||||
this.preferPinnedPort = preferPinnedPort === true
|
||||
}
|
||||
|
||||
onMessage(handler: WebSocketMessageHandler): void {
|
||||
|
|
@ -150,10 +158,12 @@ export class WebSocketTransport implements RpcTransport {
|
|||
return
|
||||
}
|
||||
|
||||
// Why: a persisted fallback port is bound FIRST — devices paired while it
|
||||
// was active store ws://ip:<fallback> and would be permanently stranded if
|
||||
// a later launch grabbed the (now free) preferred port instead (STA-1511).
|
||||
// Without a persisted fallback the preferred port is tried first. On
|
||||
// Why: default order binds a persisted fallback FIRST — devices paired
|
||||
// while it was active store ws://ip:<fallback> and would be stranded if a
|
||||
// later launch grabbed the (now free) preferred port instead (STA-1511).
|
||||
// Explicit serve --port flips the order so the pinned port wins when free
|
||||
// (issue #8535); fallback remains a secondary candidate for EADDRINUSE.
|
||||
// Without a persisted fallback only the preferred port is tried. On
|
||||
// EADDRINUSE each candidate falls through to the next, ending at port 0
|
||||
// (OS-assigned) so mobile pairing still works when everything is taken.
|
||||
// The QR code reads resolvedPort after start, so it always advertises the
|
||||
|
|
@ -163,7 +173,11 @@ export class WebSocketTransport implements RpcTransport {
|
|||
? this.fallbackPort
|
||||
: undefined
|
||||
const candidatePorts =
|
||||
persistedFallbackPort !== undefined ? [persistedFallbackPort, this.port] : [this.port]
|
||||
persistedFallbackPort === undefined
|
||||
? [this.port]
|
||||
: this.preferPinnedPort
|
||||
? [this.port, persistedFallbackPort]
|
||||
: [persistedFallbackPort, this.port]
|
||||
for (const port of candidatePorts) {
|
||||
try {
|
||||
await this.tryListen(port)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ type OrcaRuntimeRpcServerOptions = {
|
|||
platform?: NodeJS.Platform
|
||||
enableWebSocket?: boolean
|
||||
wsPort?: number
|
||||
// Why: true when the caller set an explicit port (e.g. `orca serve --port`).
|
||||
// Distinguishes that pin from the DEFAULT_WS_PORT default so transport bind
|
||||
// order can prefer the pin over a stale STA-1511 fallback (issue #8535).
|
||||
preferPinnedWsPort?: boolean
|
||||
webClientRoot?: string
|
||||
// Why: test-only overrides for the two time-bound constants below.
|
||||
// Production callers must not pass these — defaults are set by the design
|
||||
|
|
@ -450,6 +454,7 @@ export class OrcaRuntimeRpcServer {
|
|||
private readonly platform: NodeJS.Platform
|
||||
private readonly enableWebSocket: boolean
|
||||
private readonly wsPort: number
|
||||
private readonly preferPinnedWsPort: boolean
|
||||
private readonly webClientRoot: string | undefined
|
||||
private readonly authToken = randomBytes(24).toString('hex')
|
||||
private readonly keepaliveIntervalMs: number
|
||||
|
|
@ -482,6 +487,7 @@ export class OrcaRuntimeRpcServer {
|
|||
platform = process.platform,
|
||||
enableWebSocket = false,
|
||||
wsPort = DEFAULT_WS_PORT,
|
||||
preferPinnedWsPort = false,
|
||||
webClientRoot,
|
||||
keepaliveIntervalMs = KEEPALIVE_INTERVAL_MS,
|
||||
longPollCap = LONG_POLL_CAP
|
||||
|
|
@ -493,6 +499,7 @@ export class OrcaRuntimeRpcServer {
|
|||
this.platform = platform
|
||||
this.enableWebSocket = enableWebSocket
|
||||
this.wsPort = wsPort
|
||||
this.preferPinnedWsPort = preferPinnedWsPort
|
||||
this.webClientRoot = webClientRoot
|
||||
this.keepaliveIntervalMs = keepaliveIntervalMs
|
||||
this.longPollCap = longPollCap
|
||||
|
|
@ -863,10 +870,11 @@ export class OrcaRuntimeRpcServer {
|
|||
staticRoot: this.webClientRoot,
|
||||
// Why: keep the fallback port stable across restarts so paired
|
||||
// devices' stored endpoints stay valid (STA-1511) — the transport
|
||||
// binds a persisted fallback before the preferred port. wsPort 0
|
||||
// means the caller explicitly wants a random port (E2E) — don't
|
||||
// pin it.
|
||||
...(this.wsPort !== 0 ? { fallbackPort: readWsFallbackPort(this.userDataPath) } : {})
|
||||
// binds a persisted fallback before the preferred port unless the
|
||||
// caller explicitly pinned a port (serve --port). wsPort 0 means
|
||||
// the caller wants a random port (E2E) — don't pin it.
|
||||
...(this.wsPort !== 0 ? { fallbackPort: readWsFallbackPort(this.userDataPath) } : {}),
|
||||
...(this.preferPinnedWsPort ? { preferPinnedPort: true } : {})
|
||||
})
|
||||
const mobileSocketWiring = new MobileSocketWiring({
|
||||
deviceRegistry: this.deviceRegistry,
|
||||
|
|
|
|||
Loading…
Reference in New Issue