From e2f2b03a4890323c26a1eae3c61aa3cf37dff32e Mon Sep 17 00:00:00 2001 From: Fabian Schindler Date: Mon, 13 Apr 2026 09:59:01 +0200 Subject: [PATCH] fix: Ensure ports are confirmed free before allocation in Cloudflare and Python runners --- src/runner/cloudflare-runner.ts | 4 ++-- src/runner/port-allocator.ts | 34 +++++++++++++++++++++++++++++++-- src/runner/python-runner.ts | 2 +- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/runner/cloudflare-runner.ts b/src/runner/cloudflare-runner.ts index cf7379b..7c5b59f 100644 --- a/src/runner/cloudflare-runner.ts +++ b/src/runner/cloudflare-runner.ts @@ -235,8 +235,8 @@ export class CloudflareRunner { JSON.stringify(wranglerConfig, null, 2), ); - // Allocate a unique port to avoid collisions when running tests in parallel - const port = allocatePort(); + // Allocate a unique port confirmed free to avoid collisions in parallel + const port = await allocatePort(); let wranglerProcess: ChildProcess | null = null; let stdout = ""; diff --git a/src/runner/port-allocator.ts b/src/runner/port-allocator.ts index 3803ecc..02dcb61 100644 --- a/src/runner/port-allocator.ts +++ b/src/runner/port-allocator.ts @@ -1,10 +1,40 @@ /** * Shared port allocator for test runners. * Assigns unique ports to avoid collisions when running tests in parallel. + * + * Probes each candidate port with a temporary TCP server to ensure it is + * actually free before handing it out. */ +import * as net from "net"; + let nextPort = 10000 + Math.floor(Math.random() * 40000); -export function allocatePort(): number { - return nextPort++; +/** + * Check whether a port is available by briefly binding to it. + * Binds on both IPv4 and IPv6 loopback to match what most servers do. + */ +function isPortFree(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.unref(); + server.on("error", () => resolve(false)); + server.listen(port, "0.0.0.0", () => { + server.close(() => resolve(true)); + }); + }); +} + +/** + * Allocate a port that is confirmed free at the moment of allocation. + * Tries up to 200 sequential candidates before giving up. + */ +export async function allocatePort(): Promise { + for (let attempts = 0; attempts < 200; attempts++) { + const candidate = nextPort++; + if (await isPortFree(candidate)) { + return candidate; + } + } + throw new Error("Failed to find a free port after 200 attempts"); } diff --git a/src/runner/python-runner.ts b/src/runner/python-runner.ts index 3940650..601e0f7 100644 --- a/src/runner/python-runner.ts +++ b/src/runner/python-runner.ts @@ -180,7 +180,7 @@ ${dependencies.map(d => ` ${d},`).join('\n')} // Assign a unique port for MCP SSE transport tests if (transportMode === 'sse') { - env.MCP_SSE_PORT = String(allocatePort()); + env.MCP_SSE_PORT = String(await allocatePort()); } try {