diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts index b5cb93e3..969d10a7 100644 --- a/apps/cli/src/cli.ts +++ b/apps/cli/src/cli.ts @@ -24,6 +24,7 @@ import { } from '@openchatlab/core' import { getVersion } from './version' import { resolveCliPath } from './paths' +import { isPortAvailable, formatPortInUseError } from './http/port' const program = new Command() @@ -303,6 +304,12 @@ program const { startHttpServer } = await import('./http') const port = parseInt(options.port, 10) + // 启动前预检端口,快速失败,避免无谓的初始化后再报 EADDRINUSE + if (!(await isPortAvailable(port, options.host))) { + console.error(formatPortInUseError(port)) + process.exit(1) + } + let webRoot: string | undefined if (!options.headless) { const webDir = resolveCliPath('dist-web') @@ -353,7 +360,8 @@ program } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err) if (message.includes('EADDRINUSE')) { - console.error(`Error: port ${port} is already in use`) + // 极低概率的 TOCTOU 竞态(预检通过后端口被占),复用统一文案 + console.error(formatPortInUseError(port)) } else { console.error(`Startup failed: ${message}`) } diff --git a/apps/cli/src/http/port.ts b/apps/cli/src/http/port.ts new file mode 100644 index 00000000..364e00fb --- /dev/null +++ b/apps/cli/src/http/port.ts @@ -0,0 +1,33 @@ +/** + * 端口可用性检测与错误文案 + */ + +import * as net from 'net' + +/** + * 检测指定端口在给定 host 上是否可用。 + * 使用 net.createServer() 试探性绑定,无副作用(成功后立即释放)。 + */ +export function isPortAvailable(port: number, host: string): Promise { + return new Promise((resolve) => { + const server = net.createServer() + server.once('error', () => resolve(false)) + server.once('listening', () => server.close(() => resolve(true))) + server.listen(port, host) + }) +} + +/** + * 返回端口被占用时统一的友好报错文案。 + */ +export function formatPortInUseError(port: number): string { + return [ + ``, + ` ✖ Error: port ${port} is already in use.`, + ``, + ` Another process (possibly a ChatLab instance) is using this port. You can:`, + ` • Use another port: chatlab start --port `, + ` • Find the process: lsof -iTCP:${port} -sTCP:LISTEN`, + ``, + ].join('\n') +}