fix(desktop): sidecar 동적 포트 + orphan 프로세스 충돌 방지
증상:
- 'Transcription failed: Sidecar 프로세스가 실행 중이 아닙니다' 에러
- 로그: [Errno 48] address already in use ('127.0.0.1', 18765)
- 새로 spawn한 sidecar가 즉시 exit code 1로 종료
- 그러나 health check은 성공 — 이전 인스턴스의 orphan sidecar가 응답
- _sidecarProcess 핸들이 새 (실패한) ChildProcess라서 transcribe 시
null check가 통과 못 하고 STTSidecarNotRunning 에러
원인:
- dev mode HMR/재시작 시 main process는 죽지만 spawn한 python 자식 프로세스가
orphan으로 살아남는 경우가 있음 (특히 SIGKILL이 자식까지 전파 안 될 때)
- 다음 main process가 SIDECAR_PORT(18765)에 다시 바인드하려다 충돌
수정:
- LocalSTTService._spawnSidecar 진입 시 _findFreePort(SIDECAR_PORT, 20) 호출
- net.createServer로 127.0.0.1:port listen 시도해 비어있는 첫 포트 탐색
- 18765부터 18784까지 순차 시도
- 모두 점유 시 STTSidecarSpawnFailed 에러
- this._port를 동적 할당 결과로 갱신 → spawn args + health/load/transcribe URL이
자동으로 새 포트 사용
영향:
- orphan sidecar가 18765를 점유하고 있어도 새 sidecar가 18766/18767에서 정상 부팅
- 이전 orphan은 시간이 지나거나 사용자가 직접 kill하거나 app quit 시 정리 가능
- 클린 환경에서는 항상 18765로 복귀
This commit is contained in:
parent
2409cf9bd8
commit
971906bc83
1 changed files with 49 additions and 0 deletions
|
|
@ -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<number> {
|
||||
return new Promise<number>((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<void> {
|
||||
// 포트 점유 시 동적으로 다음 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(' ')}`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue