import { createServer } from 'node:net';
import type { AddressInfo, Server } from 'node:net';
* 以 Promise 方式在指定 host:port 启动 net.Server,失败时 reject。
*/
export async function listen(server: Server, port: number, host: string): Promise<void> {
await new Promise<void>((resolve, reject) => {
const cleanup = () => {
server.off('error', onError);
server.off('listening', onListening);
};
const onError = (error: Error) => {
cleanup();
reject(error);
};
const onListening = () => {
cleanup();
resolve();
};
server.once('error', onError);
server.once('listening', onListening);
server.listen(port, host);
});
}
* 关闭正在监听的 server;若已关闭则直接返回。
*/
export async function closeServer(server: Server | undefined): Promise<void> {
if (!server?.listening) {
return;
}
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
* 检测 host:port 是否可以被监听(探测后立即关闭)。
*/
export async function canListen(port: number, host: string): Promise<boolean> {
const probe = createServer();
try {
await listen(probe, port, host);
return true;
} catch {
return false;
} finally {
await closeServer(probe);
}
}
* 预占单个端口,并确保其后一个端口当时可用。
* 用于模拟"首选端口已占用、下一个空闲"的场景。
*/
export async function reservePortWithFreeNext(host: string): Promise<{ blocker: Server; occupiedPort: number }> {
for (let attempt = 0; attempt < 20; attempt += 1) {
const blocker = createServer();
await listen(blocker, 0, host);
const occupiedPort = (blocker.address() as AddressInfo).port;
if (occupiedPort < 65535 && (await canListen(occupiedPort + 1, host))) {
return { blocker, occupiedPort };
}
await closeServer(blocker);
}
throw new Error('无法找到相邻可用端口用于测试');
}
* 预占 count 个连续端口,并确保第 count+1 个端口当时可用。
* 用于构造"多个端口均被占用、首个可用端口在更后面"的集成测试场景。
*/
export async function reserveConsecutivePorts(
count: number,
listenHost = '0.0.0.0',
): Promise<{ blockers: Server[]; firstPort: number; firstFreePort: number }> {
for (let attempt = 0; attempt < 20; attempt += 1) {
const blockers: Server[] = [];
let keepBlockers = false;
try {
const firstBlocker = createServer();
await listen(firstBlocker, 0, listenHost);
blockers.push(firstBlocker);
const firstPort = (firstBlocker.address() as AddressInfo).port;
if (firstPort + count > 65535) {
continue;
}
for (let offset = 1; offset < count; offset += 1) {
const blocker = createServer();
await listen(blocker, firstPort + offset, listenHost);
blockers.push(blocker);
}
const firstFreePort = firstPort + count;
if (await canListen(firstFreePort, listenHost)) {
keepBlockers = true;
return { blockers, firstPort, firstFreePort };
}
} catch {
} finally {
if (!keepBlockers) {
await Promise.all(blockers.map((blocker) => closeServer(blocker)));
}
}
}
throw new Error('无法找到连续端口段用于测试');
}