mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 09:01:54 +00:00
Replace the PortPool-based port reservation system (9120-9199 range) with OS-assigned ephemeral ports via --port 0. Before: Desktop probed a hardcoded port range, reserved ports in-process to close TOCTOU races, and passed the chosen port to the dashboard via CLI arg. After: Desktop spawns dashboard with --port 0, parses the actual port from a stdout announcement line (HERMES_DASHBOARD_READY port=<N>), and uses that for WebSocket connections. Changes: - web_server.py: add --port 0 support with SO_REUSEADDR pre-bind + announcement; add EADDRINUSE preflight for explicit ports - main.cjs: remove PortPool, PORT_FLOOR/CEILING, pickPort(), isPortAvailable(); add waitForDashboardPort() stdout parser - Delete port-pool.cjs and port-pool.test.cjs (106 lines removed) Net effect: eliminates the entire TOCTOU-mitigation reservation infrastructure and arbitrary port range constraints. OS handles port allocation natively.
66 lines
1.8 KiB
JavaScript
66 lines
1.8 KiB
JavaScript
const _READY_RE = /^HERMES_DASHBOARD_READY port=(\d+)/m
|
|
|
|
/**
|
|
* Watch a child process's stdout for the `HERMES_DASHBOARD_READY port=<N>`
|
|
* line that web_server.py prints after uvicorn binds its socket.
|
|
*
|
|
* Returns the parsed port. Rejects if:
|
|
* - the child exits before emitting the line
|
|
* - the child emits an `error` event
|
|
* - no line arrives within the timeout
|
|
*
|
|
* A single `cleanup()` tears down every listener (data/exit/error/timeout)
|
|
* on every terminal path — resolve, reject, or timeout — so repeated
|
|
* backend spawns don't leak listener slots on the child.
|
|
*/
|
|
function waitForDashboardPort(child, timeoutMs = 45_000) {
|
|
return new Promise((resolve, reject) => {
|
|
let buf = ''
|
|
let done = false
|
|
|
|
function cleanup() {
|
|
if (done) return
|
|
done = true
|
|
clearTimeout(timer)
|
|
child.stdout.off('data', onData)
|
|
child.off('exit', onExit)
|
|
child.off('error', onError)
|
|
}
|
|
|
|
function onData(chunk) {
|
|
buf += chunk.toString()
|
|
let nl
|
|
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
const line = buf.slice(0, nl)
|
|
buf = buf.slice(nl + 1)
|
|
const m = line.match(_READY_RE)
|
|
if (m) {
|
|
cleanup()
|
|
resolve(parseInt(m[1], 10))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
function onExit(code, signal) {
|
|
cleanup()
|
|
reject(new Error(`Hermes backend: exited before port announcement (${signal || code})`))
|
|
}
|
|
|
|
function onError(err) {
|
|
cleanup()
|
|
reject(err)
|
|
}
|
|
|
|
const timer = setTimeout(() => {
|
|
cleanup()
|
|
reject(new Error(`Timed out waiting for Hermes backend port announcement (${timeoutMs}ms)`))
|
|
}, timeoutMs)
|
|
|
|
child.stdout.on('data', onData)
|
|
child.on('exit', onExit)
|
|
child.on('error', onError)
|
|
})
|
|
}
|
|
|
|
module.exports = { waitForDashboardPort }
|