package service import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.io.BufferedReader import java.io.File import java.io.InputStreamReader import java.util.concurrent.ConcurrentHashMap object LlamaServerManager { // 포트별로 프로세스를 관리합니다. private val processes = ConcurrentHashMap() private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { Runtime.getRuntime().addShutdownHook(Thread { stopAll() }) } fun startServer(binPath: String, modelPath: String, port: Int, nGpuLayers: Int = 99) { // 이미 해당 포트에서 실행 중이거나 모델 경로가 비었으면 무시합니다. if (processes.containsKey(port) || modelPath.isBlank()) return val command = listOf( binPath, "-m", modelPath, "--port", port.toString(), "-c", if (port == 8081) "512" else "8192", // 임베딩용은 컨텍스트가 짧아도 충분합니다. "-ngl", nGpuLayers.toString(), "-t", "8", // M3 Pro의 성능 코어를 고려하여 6~8개 권장 "--embedding" // 임베딩 기능을 활성화합니다. ) scope.launch { try { val pb = ProcessBuilder(command) pb.redirectErrorStream(true) File(binPath).setExecutable(true) val process = pb.start() processes[port] = process println("✅ AI 서버 시작 시도 (Port: $port, Model: ${File(modelPath).name})") val reader = BufferedReader(InputStreamReader(process.inputStream)) var line: String? while (reader.readLine().also { line = it } != null) { // 로그 출력 (디버깅용) // println("[Server $port] $line") if (line?.contains("server is listening") == true) { println("🚀 AI 서버 준비 완료 (Port: $port)") if (processes.size > 1) { println("[Cache] ${processes.size}") RagService.active() } } } } catch (e: Exception) { println("❌ AI 서버 실행 실패 (Port: $port): ${e.message}") processes.remove(port) } } } fun stopAll() { processes.forEach { (port, process) -> process.destroy() println("🛑 AI 서버 종료 (Port: $port)") } processes.clear() } }