mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-17 19:47:14 +02:00
feat: rewrite port selection to use random ports
Replace CONDUCTOR_PORT magic offset and 9400-9409 scan with random port 10000-60000. Atomic state file writes, log paths from config module, binaryVersion field for auto-restart on update. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
15db8c86ab
commit
17bd61ee72
+32
-21
@@ -6,6 +6,11 @@
|
|||||||
* Console/network/dialog buffers: CircularBuffer in-memory + async disk flush
|
* Console/network/dialog buffers: CircularBuffer in-memory + async disk flush
|
||||||
* Chromium crash → server EXITS with clear error (CLI auto-restarts)
|
* Chromium crash → server EXITS with clear error (CLI auto-restarts)
|
||||||
* Auto-shutdown after BROWSE_IDLE_TIMEOUT (default 30 min)
|
* Auto-shutdown after BROWSE_IDLE_TIMEOUT (default 30 min)
|
||||||
|
*
|
||||||
|
* State:
|
||||||
|
* State file: <project-root>/.gstack/browse.json (set via BROWSE_STATE_FILE env)
|
||||||
|
* Log files: <project-root>/.gstack/browse-{console,network,dialog}.log
|
||||||
|
* Port: random 10000-60000 (or BROWSE_PORT env for debug override)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BrowserManager } from './browser-manager';
|
import { BrowserManager } from './browser-manager';
|
||||||
@@ -13,18 +18,18 @@ import { handleReadCommand } from './read-commands';
|
|||||||
import { handleWriteCommand } from './write-commands';
|
import { handleWriteCommand } from './write-commands';
|
||||||
import { handleMetaCommand } from './meta-commands';
|
import { handleMetaCommand } from './meta-commands';
|
||||||
import { handleCookiePickerRoute } from './cookie-picker-routes';
|
import { handleCookiePickerRoute } from './cookie-picker-routes';
|
||||||
|
import { resolveConfig, ensureStateDir, readVersionHash } from './config';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
// ─── Auth (inline) ─────────────────────────────────────────────
|
// ─── Config ─────────────────────────────────────────────────────
|
||||||
|
const config = resolveConfig();
|
||||||
|
ensureStateDir(config);
|
||||||
|
|
||||||
|
// ─── Auth ───────────────────────────────────────────────────────
|
||||||
const AUTH_TOKEN = crypto.randomUUID();
|
const AUTH_TOKEN = crypto.randomUUID();
|
||||||
const PORT_OFFSET = 45600;
|
const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
|
||||||
const BROWSE_PORT = process.env.CONDUCTOR_PORT
|
|
||||||
? parseInt(process.env.CONDUCTOR_PORT, 10) - PORT_OFFSET
|
|
||||||
: parseInt(process.env.BROWSE_PORT || '0', 10); // 0 = auto-scan
|
|
||||||
const INSTANCE_SUFFIX = BROWSE_PORT ? `-${BROWSE_PORT}` : '';
|
|
||||||
const STATE_FILE = process.env.BROWSE_STATE_FILE || `/tmp/browse-server${INSTANCE_SUFFIX}.json`;
|
|
||||||
const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min
|
const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min
|
||||||
|
|
||||||
function validateAuth(req: Request): boolean {
|
function validateAuth(req: Request): boolean {
|
||||||
@@ -36,9 +41,9 @@ function validateAuth(req: Request): boolean {
|
|||||||
import { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry } from './buffers';
|
import { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry } from './buffers';
|
||||||
export { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry };
|
export { consoleBuffer, networkBuffer, dialogBuffer, addConsoleEntry, addNetworkEntry, addDialogEntry, type LogEntry, type NetworkEntry, type DialogEntry };
|
||||||
|
|
||||||
const CONSOLE_LOG_PATH = `/tmp/browse-console${INSTANCE_SUFFIX}.log`;
|
const CONSOLE_LOG_PATH = config.consoleLog;
|
||||||
const NETWORK_LOG_PATH = `/tmp/browse-network${INSTANCE_SUFFIX}.log`;
|
const NETWORK_LOG_PATH = config.networkLog;
|
||||||
const DIALOG_LOG_PATH = `/tmp/browse-dialog${INSTANCE_SUFFIX}.log`;
|
const DIALOG_LOG_PATH = config.dialogLog;
|
||||||
let lastConsoleFlushed = 0;
|
let lastConsoleFlushed = 0;
|
||||||
let lastNetworkFlushed = 0;
|
let lastNetworkFlushed = 0;
|
||||||
let lastDialogFlushed = 0;
|
let lastDialogFlushed = 0;
|
||||||
@@ -132,22 +137,25 @@ export const META_COMMANDS = new Set([
|
|||||||
const browserManager = new BrowserManager();
|
const browserManager = new BrowserManager();
|
||||||
let isShuttingDown = false;
|
let isShuttingDown = false;
|
||||||
|
|
||||||
// Find port: deterministic from CONDUCTOR_PORT, or scan range
|
// Find port: explicit BROWSE_PORT, or random in 10000-60000
|
||||||
async function findPort(): Promise<number> {
|
async function findPort(): Promise<number> {
|
||||||
// Deterministic port from CONDUCTOR_PORT (e.g., 55040 - 45600 = 9440)
|
// Explicit port override (for debugging)
|
||||||
if (BROWSE_PORT) {
|
if (BROWSE_PORT) {
|
||||||
try {
|
try {
|
||||||
const testServer = Bun.serve({ port: BROWSE_PORT, fetch: () => new Response('ok') });
|
const testServer = Bun.serve({ port: BROWSE_PORT, fetch: () => new Response('ok') });
|
||||||
testServer.stop();
|
testServer.stop();
|
||||||
return BROWSE_PORT;
|
return BROWSE_PORT;
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`[browse] Port ${BROWSE_PORT} (from CONDUCTOR_PORT ${process.env.CONDUCTOR_PORT}) is in use`);
|
throw new Error(`[browse] Port ${BROWSE_PORT} (from BROWSE_PORT env) is in use`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: scan range
|
// Random port with retry
|
||||||
const start = parseInt(process.env.BROWSE_PORT_START || '9400', 10);
|
const MIN_PORT = 10000;
|
||||||
for (let port = start; port < start + 10; port++) {
|
const MAX_PORT = 60000;
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
|
const port = MIN_PORT + Math.floor(Math.random() * (MAX_PORT - MIN_PORT));
|
||||||
try {
|
try {
|
||||||
const testServer = Bun.serve({ port, fetch: () => new Response('ok') });
|
const testServer = Bun.serve({ port, fetch: () => new Response('ok') });
|
||||||
testServer.stop();
|
testServer.stop();
|
||||||
@@ -156,7 +164,7 @@ async function findPort(): Promise<number> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`[browse] No available port in range ${start}-${start + 9}`);
|
throw new Error(`[browse] No available port after ${MAX_RETRIES} attempts in range ${MIN_PORT}-${MAX_PORT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -263,7 +271,7 @@ async function shutdown() {
|
|||||||
await browserManager.close();
|
await browserManager.close();
|
||||||
|
|
||||||
// Clean up state file
|
// Clean up state file
|
||||||
try { fs.unlinkSync(STATE_FILE); } catch {}
|
try { fs.unlinkSync(config.stateFile); } catch {}
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
@@ -329,19 +337,22 @@ async function start() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write state file
|
// Write state file (atomic: write .tmp then rename)
|
||||||
const state = {
|
const state = {
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
port,
|
port,
|
||||||
token: AUTH_TOKEN,
|
token: AUTH_TOKEN,
|
||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
serverPath: path.resolve(import.meta.dir, 'server.ts'),
|
serverPath: path.resolve(import.meta.dir, 'server.ts'),
|
||||||
|
binaryVersion: readVersionHash() || undefined,
|
||||||
};
|
};
|
||||||
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
|
const tmpFile = config.stateFile + '.tmp';
|
||||||
|
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), { mode: 0o600 });
|
||||||
|
fs.renameSync(tmpFile, config.stateFile);
|
||||||
|
|
||||||
browserManager.serverPort = port;
|
browserManager.serverPort = port;
|
||||||
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
||||||
console.log(`[browse] State file: ${STATE_FILE}`);
|
console.log(`[browse] State file: ${config.stateFile}`);
|
||||||
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1000}s`);
|
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1000}s`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user