mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-18 10:52:24 +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).
|
* 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
|
* 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
|
* 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
|
* 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
|
* 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
|
* Extracted from server.ts (which had this logic since #486) so
|
||||||
* terminal-agent.ts can reuse it without importing the whole server module.
|
* 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_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 const RANDOM_PORT_RETRIES = 5;
|
||||||
|
|
||||||
export function normalizePortError(err: unknown): Extract<PortCheckResult, { available: false }> {
|
export function normalizePortError(err: unknown): Extract<PortCheckResult, { available: false }> {
|
||||||
|
|||||||
@@ -952,10 +952,25 @@ function readBrowseToken(): string {
|
|||||||
// Boot.
|
// Boot.
|
||||||
async function main() {
|
async function main() {
|
||||||
writeClaudeAvailable();
|
writeClaudeAvailable();
|
||||||
// #2314: allocate from the shared fixed scan range, then bind. Same
|
// #2314: allocate from the shared fixed scan range, then bind. Probe-then-
|
||||||
// probe-then-bind semantics as the main server's findPort.
|
// bind has a TOCTOU window — a concurrent process can take the port between
|
||||||
const allocatedPort = await findAvailablePort();
|
// the probe and Bun.serve, which throws and would kill the boot with no
|
||||||
const server = buildServer(allocatedPort);
|
// 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;
|
const port = (server as any).port || (server as any).address?.port;
|
||||||
if (!port) {
|
if (!port) {
|
||||||
console.error('[terminal-agent] failed to bind: no 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
|
* #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
|
* 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
|
* (49152-65535 on macOS), where the weeks-lived agent squatted ports that
|
||||||
* short-lived `app.listen(0)` test servers expected to receive, absorbing
|
* short-lived `app.listen(0)` test servers expected to receive, absorbing
|
||||||
@@ -26,8 +26,10 @@ describe('shared port allocator (#2314)', () => {
|
|||||||
const port = await findAvailablePort();
|
const port = await findAvailablePort();
|
||||||
expect(port).toBeGreaterThanOrEqual(RANDOM_PORT_MIN);
|
expect(port).toBeGreaterThanOrEqual(RANDOM_PORT_MIN);
|
||||||
expect(port).toBeLessThan(RANDOM_PORT_MAX);
|
expect(port).toBeLessThan(RANDOM_PORT_MAX);
|
||||||
// The load-bearing property: below the ephemeral floor (49152).
|
// The load-bearing property: the WHOLE range sits below the ephemeral
|
||||||
expect(RANDOM_PORT_MAX).toBeLessThanOrEqual(60000);
|
// 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);
|
expect(RANDOM_PORT_MIN).toBeGreaterThanOrEqual(1024);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user