feat: auto port selection when default port is busy

This commit is contained in:
YeonGyu-Kim
2026-02-05 01:53:46 +09:00
parent b6a52466e1
commit 57358da6eb
4 changed files with 169 additions and 8 deletions
+1
View File
@@ -40,3 +40,4 @@ export * from "./session-utils"
export * from "./tmux"
export * from "./model-suggestion-retry"
export * from "./opencode-server-auth"
export * from "./port-utils"
+105
View File
@@ -0,0 +1,105 @@
import { describe, it, expect, beforeAll, afterAll } from "bun:test"
import {
isPortAvailable,
findAvailablePort,
getAvailableServerPort,
DEFAULT_SERVER_PORT,
} from "./port-utils"
describe("port-utils", () => {
describe("isPortAvailable", () => {
it("#given unused port #when checking availability #then returns true", async () => {
const port = 59999
const result = await isPortAvailable(port)
expect(result).toBe(true)
})
it("#given port in use #when checking availability #then returns false", async () => {
const port = 59998
const blocker = Bun.serve({
port,
hostname: "127.0.0.1",
fetch: () => new Response("blocked"),
})
try {
const result = await isPortAvailable(port)
expect(result).toBe(false)
} finally {
blocker.stop(true)
}
})
})
describe("findAvailablePort", () => {
it("#given start port available #when finding port #then returns start port", async () => {
const startPort = 59997
const result = await findAvailablePort(startPort)
expect(result).toBe(startPort)
})
it("#given start port blocked #when finding port #then returns next available", async () => {
const startPort = 59996
const blocker = Bun.serve({
port: startPort,
hostname: "127.0.0.1",
fetch: () => new Response("blocked"),
})
try {
const result = await findAvailablePort(startPort)
expect(result).toBe(startPort + 1)
} finally {
blocker.stop(true)
}
})
it("#given multiple ports blocked #when finding port #then skips all blocked", async () => {
const startPort = 59993
const blockers = [
Bun.serve({ port: startPort, hostname: "127.0.0.1", fetch: () => new Response() }),
Bun.serve({ port: startPort + 1, hostname: "127.0.0.1", fetch: () => new Response() }),
Bun.serve({ port: startPort + 2, hostname: "127.0.0.1", fetch: () => new Response() }),
]
try {
const result = await findAvailablePort(startPort)
expect(result).toBe(startPort + 3)
} finally {
blockers.forEach((b) => b.stop(true))
}
})
})
describe("getAvailableServerPort", () => {
it("#given preferred port available #when getting port #then returns preferred with wasAutoSelected=false", async () => {
const preferredPort = 59990
const result = await getAvailableServerPort(preferredPort)
expect(result.port).toBe(preferredPort)
expect(result.wasAutoSelected).toBe(false)
})
it("#given preferred port blocked #when getting port #then returns alternative with wasAutoSelected=true", async () => {
const preferredPort = 59989
const blocker = Bun.serve({
port: preferredPort,
hostname: "127.0.0.1",
fetch: () => new Response("blocked"),
})
try {
const result = await getAvailableServerPort(preferredPort)
expect(result.port).toBeGreaterThan(preferredPort)
expect(result.wasAutoSelected).toBe(true)
} finally {
blocker.stop(true)
}
})
})
describe("DEFAULT_SERVER_PORT", () => {
it("#given constant #when accessed #then returns 4096", () => {
expect(DEFAULT_SERVER_PORT).toBe(4096)
})
})
})
+48
View File
@@ -0,0 +1,48 @@
const DEFAULT_SERVER_PORT = 4096
const MAX_PORT_ATTEMPTS = 20
export async function isPortAvailable(port: number, hostname: string = "127.0.0.1"): Promise<boolean> {
try {
const server = Bun.serve({
port,
hostname,
fetch: () => new Response(),
})
server.stop(true)
return true
} catch {
return false
}
}
export async function findAvailablePort(
startPort: number = DEFAULT_SERVER_PORT,
hostname: string = "127.0.0.1"
): Promise<number> {
for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
const port = startPort + attempt
if (await isPortAvailable(port, hostname)) {
return port
}
}
throw new Error(`No available port found in range ${startPort}-${startPort + MAX_PORT_ATTEMPTS - 1}`)
}
export interface AutoPortResult {
port: number
wasAutoSelected: boolean
}
export async function getAvailableServerPort(
preferredPort: number = DEFAULT_SERVER_PORT,
hostname: string = "127.0.0.1"
): Promise<AutoPortResult> {
if (await isPortAvailable(preferredPort, hostname)) {
return { port: preferredPort, wasAutoSelected: false }
}
const port = await findAvailablePort(preferredPort + 1, hostname)
return { port, wasAutoSelected: true }
}
export { DEFAULT_SERVER_PORT }