diff --git a/apps/desktop/src/main/services/LocalSTTService.ts b/apps/desktop/src/main/services/LocalSTTService.ts index 60d134d..ff229b6 100644 --- a/apps/desktop/src/main/services/LocalSTTService.ts +++ b/apps/desktop/src/main/services/LocalSTTService.ts @@ -5,6 +5,7 @@ import { EventEmitter } from 'events' import { type ChildProcess, spawn } from 'child_process' +import { createServer } from 'net' import { getLogger } from './LoggerService' import { configGet } from './ConfigService' import { getSidecarCommand } from '../utils/paths' @@ -358,7 +359,55 @@ class LocalSTTService extends EventEmitter { // ── Sidecar 관리 ── + /** + * 시작 포트부터 maxAttempts개 포트 중 첫 번째 free 포트를 찾는다. + * dev mode 재시작으로 이전 sidecar가 orphan으로 남아있을 수 있어 + * 매번 동적 할당해서 충돌을 회피한다. + */ + private _findFreePort(startPort: number, maxAttempts: number): Promise { + return new Promise((resolve, reject) => { + let attempt = 0 + const tryPort = (port: number): void => { + const server = createServer() + server.unref() + server.once('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { + attempt += 1 + if (attempt >= maxAttempts) { + reject( + new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `Free port not found in range ${startPort}~${startPort + maxAttempts - 1}` + ) + ) + return + } + tryPort(port + 1) + } else { + reject( + new D3ROError( + ErrorCode.STTSidecarSpawnFailed, + `Port probe failed: ${err.message}` + ) + ) + } + }) + server.once('listening', () => { + server.close(() => { + resolve(port) + }) + }) + server.listen(port, '127.0.0.1') + } + tryPort(startPort) + }) + } + private async _spawnSidecar(): Promise { + // 포트 점유 시 동적으로 다음 free 포트 탐색 (최대 20번 시도). + // dev mode HMR/재시작으로 이전 sidecar가 orphan으로 남아있을 수 있음. + this._port = await this._findFreePort(SIDECAR_PORT, 20) + const { command, args } = getSidecarCommand() const fullArgs = [...args, '--port', String(this._port)] logger.info(`Sidecar 시작: ${command} ${fullArgs.join(' ')}`)