fix(dev): resolve local stack ports before startup
This commit is contained in:
164
scripts/dev-stack-port-utils.mjs
Normal file
164
scripts/dev-stack-port-utils.mjs
Normal file
@@ -0,0 +1,164 @@
|
||||
import {createServer} from 'node:net';
|
||||
|
||||
function toListenHosts(host) {
|
||||
if (host === '0.0.0.0') {
|
||||
return ['0.0.0.0'];
|
||||
}
|
||||
|
||||
if (host === '::') {
|
||||
return ['::'];
|
||||
}
|
||||
|
||||
return [host];
|
||||
}
|
||||
|
||||
export function normalizePort(value, fallback) {
|
||||
const port = Number.parseInt(String(value ?? ''), 10);
|
||||
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
export async function isPortAvailable({host, port}) {
|
||||
if (port === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const listenHosts = toListenHosts(host);
|
||||
|
||||
for (const listenHost of listenHosts) {
|
||||
const available = await new Promise((resolve) => {
|
||||
const server = createServer();
|
||||
server.unref();
|
||||
server.once('error', () => resolve(false));
|
||||
server.listen({host: listenHost, port}, () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
});
|
||||
|
||||
if (!available) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function findAvailablePort({host, preferredPort, reservedPorts = new Set(), maxAttempts = 200}) {
|
||||
const startPort = normalizePort(preferredPort, 0);
|
||||
|
||||
if (startPort === 0) {
|
||||
return await reserveEphemeralPort(host, reservedPorts);
|
||||
}
|
||||
|
||||
for (let offset = 0; offset <= maxAttempts; offset += 1) {
|
||||
const candidate = startPort + offset;
|
||||
if (candidate > 65535) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (reservedPorts.has(candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await isPortAvailable({host, port: candidate})) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`无法从 ${host}:${startPort} 开始找到可用端口`);
|
||||
}
|
||||
|
||||
async function reserveEphemeralPort(host, reservedPorts) {
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
const port = await new Promise((resolve, reject) => {
|
||||
const server = createServer();
|
||||
server.unref();
|
||||
server.once('error', reject);
|
||||
server.listen({host, port: 0}, () => {
|
||||
const address = server.address();
|
||||
const resolvedPort = typeof address === 'object' && address ? address.port : 0;
|
||||
server.close(() => resolve(resolvedPort));
|
||||
});
|
||||
});
|
||||
|
||||
if (typeof port === 'number' && port > 0 && !reservedPorts.has(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`无法为 ${host} 分配临时可用端口`);
|
||||
}
|
||||
|
||||
export async function resolveDevStackPorts(config) {
|
||||
const reservedPorts = new Set();
|
||||
const entries = [
|
||||
['spacetime', config.spacetime],
|
||||
['api', config.api],
|
||||
['web', config.web],
|
||||
['adminWeb', config.adminWeb],
|
||||
].filter(([, portConfig]) => Boolean(portConfig));
|
||||
const result = {};
|
||||
|
||||
for (const [name, portConfig] of entries) {
|
||||
const resolvedPort = await findAvailablePort({
|
||||
host: portConfig.host,
|
||||
preferredPort: portConfig.preferredPort,
|
||||
reservedPorts,
|
||||
});
|
||||
reservedPorts.add(resolvedPort);
|
||||
result[name] = resolvedPort;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function formatPortDecision({name, host, preferredPort, resolvedPort}) {
|
||||
if (preferredPort === resolvedPort && preferredPort !== 0) {
|
||||
return `[dev:ports] ${name}: ${host}:${resolvedPort} 可用`;
|
||||
}
|
||||
|
||||
return `[dev:ports] ${name}: ${host}:${preferredPort} 不可用,改用 ${host}:${resolvedPort}`;
|
||||
}
|
||||
|
||||
function parseCliPortConfig(rawArgs) {
|
||||
const config = {};
|
||||
|
||||
for (const rawArg of rawArgs) {
|
||||
const [name, host, rawPreferredPort] = rawArg.split(':');
|
||||
if (!name || !host || rawPreferredPort == null) {
|
||||
throw new Error(`端口配置参数格式错误: ${rawArg}`);
|
||||
}
|
||||
|
||||
config[name] = {
|
||||
host,
|
||||
preferredPort: normalizePort(rawPreferredPort, 0),
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function envKeyForPortName(name) {
|
||||
return `${name.replace(/[A-Z]/gu, (letter) => `_${letter}`).toUpperCase()}_PORT`;
|
||||
}
|
||||
|
||||
if (process.argv[2] === 'resolve-dev-stack') {
|
||||
const config = parseCliPortConfig(process.argv.slice(3));
|
||||
const resolvedPorts = await resolveDevStackPorts(config);
|
||||
|
||||
for (const [name, resolvedPort] of Object.entries(resolvedPorts)) {
|
||||
const portConfig = config[name];
|
||||
console.error(
|
||||
formatPortDecision({
|
||||
name,
|
||||
host: portConfig.host,
|
||||
preferredPort: portConfig.preferredPort,
|
||||
resolvedPort,
|
||||
}),
|
||||
);
|
||||
console.log(`${envKeyForPortName(name)}=${resolvedPort}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user