269 lines
8.0 KiB
TypeScript
269 lines
8.0 KiB
TypeScript
import {createServer} from 'node:net';
|
|
import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
|
|
import {tmpdir} from 'node:os';
|
|
import {join} from 'node:path';
|
|
import {describe, expect, it} from 'vitest';
|
|
import {
|
|
findAvailablePort,
|
|
mapDevPortsToPortRange,
|
|
parsePortRangeSpec,
|
|
reserveLinuxDevPortRange,
|
|
resolveDevStackPorts,
|
|
} from './dev-stack-port-utils.mjs';
|
|
|
|
function reservePort(port) {
|
|
return new Promise((resolve, reject) => {
|
|
const server = createServer();
|
|
|
|
server.once('error', reject);
|
|
server.listen(port, '127.0.0.1', () => {
|
|
server.off('error', reject);
|
|
resolve(server);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('dev stack port utils', () => {
|
|
it('解析端口段并映射到四个 dev 端口', () => {
|
|
expect(parsePortRangeSpec('10000-10099')).toEqual({
|
|
start: 10000,
|
|
end: 10099,
|
|
label: '10000-10099',
|
|
});
|
|
expect(mapDevPortsToPortRange('10000-10099')).toMatchObject({
|
|
webPort: 10000,
|
|
apiPort: 10001,
|
|
spacetimePort: 10002,
|
|
adminWebPort: 10003,
|
|
});
|
|
});
|
|
|
|
it('使用端口可用性检查为被占用端口寻找后续可用端口', async () => {
|
|
const firstServer = await reservePort(0);
|
|
const firstPort = firstServer.address().port;
|
|
const secondServer = await reservePort(firstPort + 1);
|
|
|
|
try {
|
|
const availablePort = await findAvailablePort({
|
|
host: '127.0.0.1',
|
|
preferredPort: firstPort,
|
|
});
|
|
|
|
expect(availablePort).toBeGreaterThan(firstPort + 1);
|
|
} finally {
|
|
await Promise.all([
|
|
new Promise((resolve) => firstServer.close(resolve)),
|
|
new Promise((resolve) => secondServer.close(resolve)),
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('端口查找不会越过 Linux 用户端口段', async () => {
|
|
await expect(
|
|
findAvailablePort({
|
|
host: '127.0.0.1',
|
|
preferredPort: 9999,
|
|
portRange: {start: 10000, end: 10099, label: '10000-10099'},
|
|
}),
|
|
).rejects.toThrow('不在允许端口段');
|
|
});
|
|
|
|
it('为 npm run dev 的所有后续流程解析互不冲突的端口', async () => {
|
|
const resolvedPorts = await resolveDevStackPorts({
|
|
spacetime: {host: '127.0.0.1', preferredPort: 0},
|
|
api: {host: '127.0.0.1', preferredPort: 0},
|
|
web: {host: '127.0.0.1', preferredPort: 0},
|
|
adminWeb: {host: '127.0.0.1', preferredPort: 0},
|
|
});
|
|
|
|
expect(new Set(Object.values(resolvedPorts)).size).toBe(4);
|
|
});
|
|
|
|
it('端口段内会一直漂移到段尾,不会被默认 200 次尝试截断', async () => {
|
|
const rangeStart = 10000;
|
|
const rangeEnd = 10300;
|
|
const reservedPorts = new Set(
|
|
Array.from({length: 201}, (_, index) => rangeStart + index),
|
|
);
|
|
|
|
const availablePort = await findAvailablePort({
|
|
host: '127.0.0.1',
|
|
preferredPort: rangeStart,
|
|
reservedPorts,
|
|
portRange: {start: rangeStart, end: rangeEnd, label: `${rangeStart}-${rangeEnd}`},
|
|
});
|
|
|
|
expect(availablePort).toBeGreaterThan(rangeStart + 200);
|
|
expect(availablePort).toBeLessThanOrEqual(rangeEnd);
|
|
});
|
|
|
|
const linuxIt = process.platform === 'linux' ? it : it.skip;
|
|
|
|
linuxIt('Linux 未手动指定端口段时从 10000 开始按 100 端口块自动分配', async () => {
|
|
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
|
|
const registryPath = join(tempRoot, 'registry.json');
|
|
const lockPath = join(tempRoot, 'registry.lock');
|
|
|
|
try {
|
|
const aliceAllocation = await reserveLinuxDevPortRange({
|
|
env: {
|
|
USER: 'alice',
|
|
LOGNAME: 'alice',
|
|
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
|
},
|
|
username: 'alice',
|
|
requestedRange: null,
|
|
registryPath,
|
|
lockPath,
|
|
});
|
|
const bobAllocation = await reserveLinuxDevPortRange({
|
|
env: {
|
|
USER: 'bob',
|
|
LOGNAME: 'bob',
|
|
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
|
},
|
|
username: 'bob',
|
|
requestedRange: null,
|
|
registryPath,
|
|
lockPath,
|
|
});
|
|
|
|
expect(aliceAllocation.range.label).toBe('10000-10099');
|
|
expect(aliceAllocation.source).toBe('auto');
|
|
expect(bobAllocation.range.label).toBe('10100-10199');
|
|
expect(bobAllocation.source).toBe('auto');
|
|
} finally {
|
|
rmSync(tempRoot, {recursive: true, force: true});
|
|
}
|
|
});
|
|
|
|
linuxIt('Linux 系统级端口段记录会阻止两个用户拿到同一段', async () => {
|
|
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
|
|
const registryPath = join(tempRoot, 'registry.json');
|
|
const lockPath = join(tempRoot, 'registry.lock');
|
|
|
|
try {
|
|
const baseEnv = {
|
|
USER: 'alice',
|
|
LOGNAME: 'alice',
|
|
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
|
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
|
|
};
|
|
|
|
const aliceAllocation = await reserveLinuxDevPortRange({
|
|
env: baseEnv,
|
|
username: 'alice',
|
|
registryPath,
|
|
lockPath,
|
|
});
|
|
|
|
expect(aliceAllocation.range.label).toBe('10000-10099');
|
|
|
|
const registryAfterAlice = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
expect(registryAfterAlice.allocations.alice.range.label).toBe('10000-10099');
|
|
|
|
await expect(
|
|
reserveLinuxDevPortRange({
|
|
env: {
|
|
...baseEnv,
|
|
USER: 'bob',
|
|
LOGNAME: 'bob',
|
|
},
|
|
username: 'bob',
|
|
requestedRange: '10000-10099',
|
|
registryPath,
|
|
lockPath,
|
|
}),
|
|
).rejects.toThrow('已被用户 alice 占用');
|
|
|
|
const aliceReuse = await reserveLinuxDevPortRange({
|
|
env: {
|
|
...baseEnv,
|
|
USER: 'alice',
|
|
LOGNAME: 'alice',
|
|
},
|
|
username: 'alice',
|
|
registryPath,
|
|
lockPath,
|
|
});
|
|
|
|
expect(aliceReuse.range.label).toBe('10000-10099');
|
|
expect(aliceReuse.source).toBe('manual');
|
|
expect(
|
|
(
|
|
await reserveLinuxDevPortRange({
|
|
env: {
|
|
...baseEnv,
|
|
USER: 'alice',
|
|
LOGNAME: 'alice',
|
|
GENARRATIVE_DEV_PORT_RANGE: '10100-10199',
|
|
},
|
|
username: 'alice',
|
|
requestedRange: '10100-10199',
|
|
registryPath,
|
|
lockPath,
|
|
})
|
|
).range.label,
|
|
).toBe('10000-10099');
|
|
|
|
const registryAfterReuse = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
expect(registryAfterReuse.allocations.alice.range.label).toBe('10000-10099');
|
|
|
|
const bobAllocation = await reserveLinuxDevPortRange({
|
|
env: {
|
|
...baseEnv,
|
|
USER: 'bob',
|
|
LOGNAME: 'bob',
|
|
GENARRATIVE_DEV_PORT_RANGE: '10100-10199',
|
|
},
|
|
username: 'bob',
|
|
requestedRange: '10100-10199',
|
|
registryPath,
|
|
lockPath,
|
|
});
|
|
|
|
expect(bobAllocation.range.label).toBe('10100-10199');
|
|
} finally {
|
|
rmSync(tempRoot, {recursive: true, force: true});
|
|
}
|
|
});
|
|
|
|
linuxIt('Linux 同一用户第二个 dev 会话会复用同一用户段并继续在段内漂移', async () => {
|
|
const tempRoot = mkdtempSync(join(tmpdir(), 'genarrative-port-range-'));
|
|
const registryPath = join(tempRoot, 'registry.json');
|
|
const lockPath = join(tempRoot, 'registry.lock');
|
|
|
|
try {
|
|
const baseEnv = {
|
|
USER: 'alice',
|
|
LOGNAME: 'alice',
|
|
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempRoot,
|
|
GENARRATIVE_DEV_PORT_RANGE: '10000-10099',
|
|
};
|
|
|
|
const first = await reserveLinuxDevPortRange({
|
|
env: baseEnv,
|
|
username: 'alice',
|
|
registryPath,
|
|
lockPath,
|
|
});
|
|
|
|
const second = await reserveLinuxDevPortRange({
|
|
env: baseEnv,
|
|
username: 'alice',
|
|
registryPath,
|
|
lockPath,
|
|
});
|
|
|
|
expect(first.range.label).toBe('10000-10099');
|
|
expect(second.range.label).toBe('10000-10099');
|
|
expect(second.username).toBe('alice');
|
|
expect(JSON.parse(readFileSync(registryPath, 'utf8')).allocations.alice.range.label).toBe(
|
|
'10000-10099',
|
|
);
|
|
} finally {
|
|
rmSync(tempRoot, {recursive: true, force: true});
|
|
}
|
|
});
|
|
});
|