mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
fix(browse): port allocator range actually stays below the ephemeral floor; terminal-agent retries a raced bind
RANDOM_PORT_MAX was 60000 while the module header documents 49152-65535 as the pool to avoid — ~22% of allocations landed back inside it, preserving the phantom-404 squatting class for both the daemon and the weeks-lived terminal-agent. The cap is now 49151 and the range test pins the true property (< 49152) instead of the old <= 60000 tautology. terminal-agent boot also re-allocates and retries up to 5 times when Bun.serve throws in the probe-then-bind TOCTOU window — previously a concurrent bind killed the boot with no retry via main().catch → exit 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e86dcd6a22
commit
c10a9736b8
@@ -1,12 +1,15 @@
|
||||
/**
|
||||
* Shared loopback port allocation (#2314, decision 8).
|
||||
*
|
||||
* One fixed scan range (10000-60000) for EVERY long-lived gstack listener:
|
||||
* One fixed scan range (10000-49151) for EVERY long-lived gstack listener:
|
||||
* the main browse daemon and the terminal-agent. Binding `port: 0` instead
|
||||
* hands out a port from the OS EPHEMERAL range (49152-65535 on macOS) — the
|
||||
* same pool every short-lived test server draws from — so a daemon that
|
||||
* lives for weeks ends up squatting ports that `app.listen(0)` test servers
|
||||
* expect to receive, silently absorbing their traffic as phantom 404s.
|
||||
* expect to receive, silently absorbing their traffic as phantom 404s. The
|
||||
* range therefore ends AT 49151: a max above it would put a fraction of
|
||||
* picks back inside the pool this module exists to avoid (the original
|
||||
* 60000 cap left ~22% of allocations in 49152-59999).
|
||||
*
|
||||
* Extracted from server.ts (which had this logic since #486) so
|
||||
* terminal-agent.ts can reuse it without importing the whole server module.
|
||||
@@ -24,7 +27,7 @@ export type FailedPortAttempt = {
|
||||
};
|
||||
|
||||
export const RANDOM_PORT_MIN = 10000;
|
||||
export const RANDOM_PORT_MAX = 60000;
|
||||
export const RANDOM_PORT_MAX = 49151; // last port BELOW the macOS ephemeral pool (49152-65535)
|
||||
export const RANDOM_PORT_RETRIES = 5;
|
||||
|
||||
export function normalizePortError(err: unknown): Extract<PortCheckResult, { available: false }> {
|
||||
|
||||
@@ -952,10 +952,25 @@ function readBrowseToken(): string {
|
||||
// Boot.
|
||||
async function main() {
|
||||
writeClaudeAvailable();
|
||||
// #2314: allocate from the shared fixed scan range, then bind. Same
|
||||
// probe-then-bind semantics as the main server's findPort.
|
||||
const allocatedPort = await findAvailablePort();
|
||||
const server = buildServer(allocatedPort);
|
||||
// #2314: allocate from the shared fixed scan range, then bind. Probe-then-
|
||||
// bind has a TOCTOU window — a concurrent process can take the port between
|
||||
// the probe and Bun.serve, which throws and would kill the boot with no
|
||||
// retry (main().catch → exit 1). Re-allocate and retry a few times; each
|
||||
// iteration probes fresh, so only a genuine race lands here.
|
||||
let server: ReturnType<typeof buildServer> | undefined;
|
||||
let lastBindErr: unknown;
|
||||
for (let attempt = 0; attempt < 5 && !server; attempt++) {
|
||||
const allocatedPort = await findAvailablePort();
|
||||
try {
|
||||
server = buildServer(allocatedPort);
|
||||
} catch (err) {
|
||||
lastBindErr = err;
|
||||
}
|
||||
}
|
||||
if (!server) {
|
||||
console.error(`[terminal-agent] failed to bind after 5 attempts: ${lastBindErr}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const port = (server as any).port || (server as any).address?.port;
|
||||
if (!port) {
|
||||
console.error('[terminal-agent] failed to bind: no port');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* #2314: the terminal-agent must allocate its port from the SAME fixed
|
||||
* 10000-60000 scan range the main server uses (port-allocator.ts,
|
||||
* 10000-49151 scan range the main server uses (port-allocator.ts,
|
||||
* decision 8) — never `port: 0`. Binding 0 drew from the OS ephemeral range
|
||||
* (49152-65535 on macOS), where the weeks-lived agent squatted ports that
|
||||
* short-lived `app.listen(0)` test servers expected to receive, absorbing
|
||||
@@ -26,8 +26,10 @@ describe('shared port allocator (#2314)', () => {
|
||||
const port = await findAvailablePort();
|
||||
expect(port).toBeGreaterThanOrEqual(RANDOM_PORT_MIN);
|
||||
expect(port).toBeLessThan(RANDOM_PORT_MAX);
|
||||
// The load-bearing property: below the ephemeral floor (49152).
|
||||
expect(RANDOM_PORT_MAX).toBeLessThanOrEqual(60000);
|
||||
// The load-bearing property: the WHOLE range sits below the ephemeral
|
||||
// floor (49152). The original 60000 cap left ~22% of picks inside the
|
||||
// pool this allocator exists to avoid.
|
||||
expect(RANDOM_PORT_MAX).toBeLessThan(49152);
|
||||
expect(RANDOM_PORT_MIN).toBeGreaterThanOrEqual(1024);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user