Retry listen(0)/bind(0) when the preferred port is taken, gated by BARE_OS_BIND_FALLBACK (default on; set 0/false for strict bind). - Add lib/bare-os-bind-fallback.js and use it from bare-os-www-initd and bare-openssh (including privilege-path + holesail actual port). - POSIX socket bridge in index.js: TCP listen and UDP bind fallbacks. - bare-holesail + bare-holesail-managed: retry Holesail ready(); persist resolved listen port in managed state when it changes. - Add test.bare-os-bind-fallback.js.
101 lines
2.7 KiB
JavaScript
101 lines
2.7 KiB
JavaScript
/**
|
|
* Ephemeral TCP/UDP fallback when bind/listen hits EADDRINUSE (see BARE_OS_BIND_FALLBACK).
|
|
*/
|
|
|
|
export const BARE_OS_BIND_FALLBACK_ENV = 'BARE_OS_BIND_FALLBACK'
|
|
|
|
/**
|
|
* When unset or truthy, retry with port `0` after EADDRINUSE. Set `0` / `false` for strict bind.
|
|
* @param {Record<string, string | undefined> | null | undefined} env
|
|
*/
|
|
export function bindFallbackEnabled(env) {
|
|
if (!env || typeof env !== 'object') return true
|
|
const v = env[BARE_OS_BIND_FALLBACK_ENV]
|
|
if (v === '0' || v === 'false') return false
|
|
return true
|
|
}
|
|
|
|
/** @param {unknown} err */
|
|
export function isAddrInUse(err) {
|
|
const o = err && typeof err === 'object' ? err : null
|
|
const code = o && (o.code || o.errno)
|
|
if (code === 'EADDRINUSE') return true
|
|
const msg = String((o && /** @type {{ message?: string }} */ (o).message) || err || '').toLowerCase()
|
|
return msg.includes('eaddrinuse') || msg.includes('address already in use')
|
|
}
|
|
|
|
/**
|
|
* @param {{ address?: () => unknown }} server
|
|
* @param {number} fallback
|
|
*/
|
|
export function readListeningTcpPort(server, fallback) {
|
|
try {
|
|
const a =
|
|
server.address && typeof server.address === 'function'
|
|
? server.address()
|
|
: null
|
|
if (a && typeof a === 'object' && Number.isFinite(/** @type {{ port?: number }} */ (a).port))
|
|
return Number(/** @type {{ port?: number }} */ (a).port)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
/**
|
|
* `listen(port, host, cb)` + `once('error')` / `off('error')` (net.Server / http.Server).
|
|
* @param {{ listen: Function, address?: Function, once: Function, off: Function }} server
|
|
* @param {number} preferredPort
|
|
* @param {string} host
|
|
* @param {Record<string, string | undefined> | null | undefined} env
|
|
* @returns {Promise<{ port: number }>}
|
|
*/
|
|
export async function listenTcpWithFallback(server, preferredPort, host, env) {
|
|
const tryOnce = (port) =>
|
|
new Promise((resolve, reject) => {
|
|
const onErr = (e) => {
|
|
try {
|
|
server.off('error', onErr)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
reject(e)
|
|
}
|
|
server.once('error', onErr)
|
|
try {
|
|
server.listen(port, host, () => {
|
|
try {
|
|
server.off('error', onErr)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
resolve(null)
|
|
})
|
|
} catch (e) {
|
|
try {
|
|
server.off('error', onErr)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
reject(e)
|
|
}
|
|
})
|
|
|
|
try {
|
|
await tryOnce(preferredPort)
|
|
} catch (e) {
|
|
if (
|
|
bindFallbackEnabled(env) &&
|
|
isAddrInUse(e) &&
|
|
preferredPort !== 0
|
|
) {
|
|
await tryOnce(0)
|
|
} else {
|
|
throw e
|
|
}
|
|
}
|
|
|
|
const port = readListeningTcpPort(server, preferredPort)
|
|
return { port }
|
|
}
|