diff --git a/apps/web/app/api/terminal/port/route.ts b/apps/web/app/api/terminal/port/route.ts new file mode 100644 index 00000000000..ac490480e8a --- /dev/null +++ b/apps/web/app/api/terminal/port/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { getTerminalPort } from "@/lib/terminal-server"; + +export const dynamic = "force-dynamic"; + +export function GET() { + const port = getTerminalPort(); + return NextResponse.json({ port }); +} diff --git a/apps/web/app/components/terminal/terminal-drawer.tsx b/apps/web/app/components/terminal/terminal-drawer.tsx index 08128ee17d7..da491c7684f 100644 --- a/apps/web/app/components/terminal/terminal-drawer.tsx +++ b/apps/web/app/components/terminal/terminal-drawer.tsx @@ -16,7 +16,7 @@ const MIN_DRAWER_HEIGHT = 180; const MAX_DRAWER_HEIGHT_RATIO = 0.75; const DEFAULT_DRAWER_HEIGHT = 280; const STORAGE_KEY = "dench-terminal-height"; -const WS_PORT = 3101; +const DEFAULT_WS_PORT = 3101; const MAX_TERMINALS = 8; function maxDrawerHeight(): number { @@ -159,7 +159,7 @@ function TerminalViewport({ termRef.current = terminal; fitRef.current = fitAddon; - const connectWs = () => { + const connectWs = async () => { if (disposed) return; // Fit now that the container has layout dimensions @@ -167,8 +167,16 @@ function TerminalViewport({ const cols = terminal.cols > 0 ? terminal.cols : 80; const rows = terminal.rows > 0 ? terminal.rows : 24; + let wsPort = DEFAULT_WS_PORT; + try { + const res = await fetch("/api/terminal/port"); + const json = await res.json(); + if (json.port) wsPort = json.port; + } catch {} + + if (disposed) return; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//127.0.0.1:${WS_PORT}`; + const wsUrl = `${protocol}//127.0.0.1:${wsPort}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; diff --git a/apps/web/lib/terminal-server.ts b/apps/web/lib/terminal-server.ts index 6860577cc03..26dce87889a 100644 --- a/apps/web/lib/terminal-server.ts +++ b/apps/web/lib/terminal-server.ts @@ -11,12 +11,19 @@ interface TerminalSession { const sessions = new Map(); -let wss: WebSocketServer | null = null; -let didFixSpawnHelper = false; +const _g = globalThis as unknown as { + __terminalWss?: WebSocketServer; + __terminalDidFixSpawnHelper?: boolean; + __terminalPort?: number; +}; + +let wss: WebSocketServer | null = _g.__terminalWss ?? null; +let didFixSpawnHelper = _g.__terminalDidFixSpawnHelper ?? false; function ensureSpawnHelperExecutable() { if (didFixSpawnHelper || process.platform === "win32") return; didFixSpawnHelper = true; + _g.__terminalDidFixSpawnHelper = true; try { const req = createRequire(import.meta.url); const pkgPath = req.resolve("node-pty/package.json"); @@ -163,16 +170,25 @@ export function startTerminalServer(port: number) { if (wss) return; wss = new WebSocketServer({ port, host: "127.0.0.1" }); + _g.__terminalWss = wss; wss.on("connection", handleConnection); + wss.on("listening", () => { + _g.__terminalPort = port; + }); wss.on("error", (err) => { if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") { console.warn(`[terminal] Port ${port} in use, retrying on ${port + 1}`); wss = null; + _g.__terminalWss = undefined; startTerminalServer(port + 1); } }); } +export function getTerminalPort(): number | null { + return _g.__terminalPort ?? null; +} + export function stopTerminalServer() { if (!wss) return; for (const session of sessions.values()) { @@ -181,4 +197,5 @@ export function stopTerminalServer() { sessions.clear(); wss.close(); wss = null; + _g.__terminalWss = undefined; }