mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-17 02:15:31 +02:00
fix(browse): terminal-agent allocates from the fixed port scan range, not port:0 (#2314)
The terminal-agent bound `Bun.serve({ port: 0 })` and kept that OS-assigned
port for its whole (weeks-long) lifetime. `port: 0` draws from the OS
EPHEMERAL range (49152-65535 on macOS) — the exact pool every short-lived
`app.listen(0)` test server draws from — so the agent squatted ports that
test suites expected to receive and silently absorbed their traffic as
phantom 404s (two squatting daemons verified in the report).
Fix per decision 8: extract the main server's port allocation into
browse/src/port-allocator.ts (checkPortAvailable / isPortAvailable /
findAvailablePort + the 10000-60000 range constants and the actionable
sandbox-vs-occupied error formatters, all verbatim from server.ts) and make
BOTH long-lived listeners use it — server.ts's findPort is now a thin
findAvailablePort(BROWSE_PORT) wrapper, and terminal-agent's buildServer
takes a pre-allocated port from the same range. No terminal-port consumer
carries a range assumption (they read the port file), verified by grep.
Tests: terminal-agent-port-range (new — allocator stays inside
10000-60000 and below the 49152 ephemeral floor, explicit-port honored,
occupied-explicit throws, static tripwires pin no-port:0 in
terminal-agent.ts and the shared wrapper in server.ts) + findport +
terminal-agent-integration/session-routing/detach-reattach +
dual-listener: 67 pass, 0 fail.
Fixes #2314.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
908c5a6a69
commit
0d4d554e13
@@ -27,6 +27,7 @@ import { writeSecureFile, restrictFilePermissions, mkdirSecure } from './file-pe
|
||||
import { atomicWriteSync, atomicWriteQuiet } from '../../lib/fs-atomic';
|
||||
import { safeUnlink } from './error-handling';
|
||||
import { writeAgentRecord, clearAgentRecord } from './terminal-agent-control';
|
||||
import { findAvailablePort } from './port-allocator';
|
||||
import { extractPtyCookie } from './pty-session-cookie';
|
||||
|
||||
const STATE_FILE = process.env.BROWSE_STATE_FILE || path.join(process.env.HOME || '/tmp', '.gstack', 'browse.json');
|
||||
@@ -490,10 +491,15 @@ function maybeSpawnPty(ws: any, session: PtySession): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildServer() {
|
||||
function buildServer(port: number) {
|
||||
return Bun.serve({
|
||||
hostname: '127.0.0.1',
|
||||
port: 0,
|
||||
// #2314: allocated from the SAME fixed 10000-60000 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 this
|
||||
// weeks-lived agent squatted ports that short-lived `app.listen(0)` test
|
||||
// servers expected to receive, absorbing their traffic as phantom 404s.
|
||||
port,
|
||||
idleTimeout: 0, // PTY connections are long-lived; default idleTimeout would kill them
|
||||
|
||||
fetch(req, server) {
|
||||
@@ -944,9 +950,12 @@ function readBrowseToken(): string {
|
||||
}
|
||||
|
||||
// Boot.
|
||||
function main() {
|
||||
async function main() {
|
||||
writeClaudeAvailable();
|
||||
const server = buildServer();
|
||||
// #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);
|
||||
const port = (server as any).port || (server as any).address?.port;
|
||||
if (!port) {
|
||||
console.error('[terminal-agent] failed to bind: no port');
|
||||
@@ -1015,4 +1024,7 @@ try {
|
||||
writeSecureFile(INTERNAL_TOKEN_FILE, INTERNAL_TOKEN);
|
||||
} catch {}
|
||||
|
||||
main();
|
||||
main().catch((err) => {
|
||||
console.error(`[terminal-agent] boot failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user