修复桌面壳开发端口对齐

桌面壳 dev 命令显式锁定 Vite 3000 端口

dev 端口解析支持严格端口模式并避免显式端口被 Linux 端口段改写

补充端口错配回归测试和宿主壳文档记忆
This commit is contained in:
2026-06-21 21:57:00 +08:00
parent 768546fdc7
commit 15623a9eff
9 changed files with 93 additions and 8 deletions
+14
View File
@@ -479,6 +479,7 @@ export async function findAvailablePort({
reservedPorts = new Set(),
maxAttempts = null,
portRange = null,
strict = false,
}) {
const range = normalizePortRange(portRange);
const startPort = normalizePort(preferredPort, 0);
@@ -501,6 +502,18 @@ export async function findAvailablePort({
throw new Error(`端口 ${startPort} 不在允许端口段 ${range.label} 内`);
}
if (strict && startPort !== 0) {
if (reservedPorts.has(startPort)) {
throw new Error(`端口 ${host}:${startPort} 已被当前 dev 启动流程占用,无法严格使用该端口`);
}
if (await isPortAvailable({host, port: startPort})) {
return startPort;
}
throw new Error(`端口 ${host}:${startPort} 不可用,无法严格使用该端口`);
}
const boundedAttempts = range
? Number.isFinite(maxAttempts)
? Math.min(Math.max(0, maxAttempts), range.end - startPort)
@@ -565,6 +578,7 @@ export async function resolveDevStackPorts(config) {
preferredPort: portConfig.preferredPort,
reservedPorts,
portRange: portConfig.portRange,
strict: Boolean(portConfig.strict),
});
reservedPorts.add(resolvedPort);
result[name] = resolvedPort;
+17
View File
@@ -58,6 +58,23 @@ describe('dev stack port utils', () => {
}
});
it('严格端口模式在端口被占用时直接失败而不是漂移', async () => {
const server = await reservePort(0);
const port = server.address().port;
try {
await expect(
findAvailablePort({
host: '127.0.0.1',
preferredPort: port,
strict: true,
}),
).rejects.toThrow('无法严格使用该端口');
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
it('端口查找不会越过 Linux 用户端口段', async () => {
await expect(
findAvailablePort({
+11 -4
View File
@@ -132,6 +132,7 @@ function parseArgs(argv, baseEnv) {
migrationBootstrapSecretMode: 'auto',
watch: false,
interactive: true,
strictWebPort: false,
};
for (let index = 0; index < args.length; index += 1) {
@@ -167,6 +168,9 @@ function parseArgs(argv, baseEnv) {
options.webPort = normalizePort(readValue(), options.webPort);
explicitOptions.add('webPort');
break;
case '--strict-web-port':
options.strictWebPort = true;
break;
case '--admin-web-host':
options.adminWebHost = readValue();
explicitOptions.add('adminWebHost');
@@ -956,13 +960,15 @@ class DevRunner {
async resolvePorts(command) {
const {options} = this;
const portConfig = {};
const portRangeFor = (optionName) =>
this.explicitOptions.has(optionName) ? null : this.state.portRange;
if (command === 'all' || command === 'spacetime') {
if (!options.skipSpacetime && !this.state.spacetimeReused) {
portConfig.spacetime = {
host: options.spacetimeHost,
preferredPort: options.spacetimePort,
portRange: this.state.portRange,
portRange: portRangeFor('spacetimePort'),
};
}
}
@@ -971,7 +977,7 @@ class DevRunner {
portConfig.api = {
host: options.apiHost,
preferredPort: options.apiPort,
portRange: this.state.portRange,
portRange: portRangeFor('apiPort'),
};
}
@@ -979,7 +985,8 @@ class DevRunner {
portConfig.web = {
host: options.webHost,
preferredPort: options.webPort,
portRange: this.state.portRange,
portRange: portRangeFor('webPort'),
strict: options.strictWebPort,
};
}
@@ -987,7 +994,7 @@ class DevRunner {
portConfig.adminWeb = {
host: options.adminWebHost,
preferredPort: options.adminWebPort,
portRange: this.state.portRange,
portRange: portRangeFor('adminWebPort'),
};
}
+30
View File
@@ -139,6 +139,36 @@ describe('dev scheduler argument routing', () => {
}
});
linuxTest('Linux 桌面壳显式指定 web-port 时不被系统级端口段改写', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-'));
try {
const {command, explicitOptions, options} = parseArgs(
['web', '--web-port', '3000', '--strict-web-port'],
{
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir,
},
);
const runner = new DevRunner(options, {
USER: 'alice',
LOGNAME: 'alice',
GENARRATIVE_DEV_PORT_RANGE: '22000-22099',
GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir,
}, explicitOptions);
await runner.prepareLinuxPortRange(command);
expect(runner.state.portRange.label).toBe('22000-22099');
expect(runner.options.webPort).toBe(3000);
expect(runner.options.apiPort).toBe(22001);
expect(runner.options.spacetimePort).toBe(22002);
expect(runner.options.adminWebPort).toBe(22003);
} finally {
rmSync(tempDir, {recursive: true, force: true});
}
});
test('Windows 仍沿用原有端口解析,不启用 Linux 端口段登记', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {