package service import Defines.DETAILLOG import Defines.EMBEDDING_PORT import Defines.LLM_PORT import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import network.RagService import util.HardwareDetector import util.NetworkPortDiagnostic 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 { killZombieProcesses() Runtime.getRuntime().addShutdownHook(Thread { stopAll() }) } // OS 시스템 명령어를 이용해 찌꺼기 프로세스를 이름으로 찾아 강제 종료하는 함수 private fun killZombieProcesses() { try { val os = System.getProperty("os.name").lowercase() if (os.contains("win")) { // 윈도우: taskkill 명령어로 llama-server.exe 강제 종료 (/F: 강제, /T: 트리거된 자식까지) ProcessBuilder("cmd", "/c", "taskkill /F /IM llama-server.exe /T") .redirectErrorStream(true) .start() .waitFor() println("🧹 [System] 이전 llama-server 좀비 프로세스 정리 완료 (Windows)") } else { // 맥/리눅스: pkill 사용 ProcessBuilder("pkill", "-f", "llama-server") .redirectErrorStream(true) .start() .waitFor() println("🧹 [System] 이전 llama-server 좀비 프로세스 정리 완료 (Mac/Linux)") } } catch (e: Exception) { // 실행 중인 프로세스가 없어서 에러가 나도 조용히 무시합니다. } } fun checkPortStatus(port: Int): String { return try { // netstat 명령어로 해당 포트를 점유 중인 프로세스 확인 val process = Runtime.getRuntime().exec("cmd /c netstat -ano | findstr :$port") val reader = process.inputStream.bufferedReader() val result = reader.readText() if (result.contains("LISTENING")) { val pid = result.trim().split(Regex("\\s+")).last() "✅ 포트 $port 상태: 사용 중 (PID: $pid - 정상 대기 중)" } else { "⚠️ 포트 $port 상태: 리스닝 상태가 아님 (서버 미구동 또는 차단 가능성)" } } catch (e: Exception) { "❌ 포트 점검 실패: ${e.message}" } } fun startServer(binPath: String, modelPath: String, port: Int) { val os = System.getProperty("os.name").lowercase() val arch = System.getProperty("os.arch").lowercase() val isWin = os.contains("win") val isMacArm = os.contains("mac") && (arch.contains("arm64") || arch.contains("aarch64")) val canUsePort = if (isWin) NetworkPortDiagnostic.testPortAvailability(port) else true if (canUsePort) { if (processes.containsKey(port) || modelPath.isBlank()) return val cpuCores = Runtime.getRuntime().availableProcessors() // HardwareDetector.getCpuCores()와 동일 val hasGpu = HardwareDetector.hasNvidiaGpu() val ratio = if (isWin) 0.5 else 0.7 val optimalThreads = if(isWin)(cpuCores * ratio).toInt().coerceIn(4, 12) else (cpuCores * ratio).toInt().coerceIn(4, 16) var optimalGpuLayers = 99 //if ((isWin && hasGpu) || isMacArm) 99 else 4 // if(HardwareDetector.getCpuName().contains("i7")) { // optimalGpuLayers = 0 // } println("🖥️ OS: $os / Arch: $arch") println("⚙️ 할당 스레드: $optimalThreads (Core: $cpuCores, Ratio: $ratio)") println("🚀 GPU 레이어: $optimalGpuLayers (NVIDIA/MacArm: ${if(optimalGpuLayers == 99) "YES" else "NO"})") val command = mutableListOf( binPath, "-m", modelPath, "--port", port.toString(), "-c", if (port == EMBEDDING_PORT) "2048" else "8192", "-ngl", optimalGpuLayers.toString(), "-t", optimalThreads.toString(), "--embedding", "--mlock", // RAM 고정으로 스왑 방지 "--no-mmap", ) if (port != EMBEDDING_PORT) { // 텍스트 생성용 모델에만 적용 command.addAll(listOf( "-b", "512", // Batch size (토큰 병렬 처리량 제한으로 연산 안정화) "--threads-batch", optimalThreads.toString(), "-fa","on", // Flash Attention 활성화 (메모리 절약 및 긴 컨텍스트 연산 안정성 증가) "--cont-batching" )) } if (isWin) { command.addAll(listOf( "--numa", "distribute" )) } scope.launch { try { val pb = ProcessBuilder(command) // 2. 윈도우 Vulkan 환경 변수 설정 if (isWin ) { val env = pb.environment() // 특정 GPU 선택 (내장 GPU가 여러 개일 경우) env["GGML_VULKAN_DEVICE"] = "0" env["GGML_VULKAN_MAX_NODES"] = "1" // DLL 로드 경로 강제 지정 (bin 폴더 내 dll 참조) val libraryPath = File(binPath).parentFile.absolutePath val currentPath = System.getenv("PATH") ?: "" env["PATH"] = "$libraryPath;$currentPath" println("🔧 [Vulkan] 환경 변수 설정 완료: $libraryPath") } pb.redirectErrorStream(true) File(binPath).setExecutable(true) val process = pb.start() processes[port] = process println("✅ AI 서버 시작 시도 (Port: $port, Model: ${File(modelPath).name})") if (isWin) { delay(3000) val status = checkPortStatus(port) println(status) // 콘솔 로그 TradingLogStore.addAnalyzer("System", "Port:$port", status, status.contains("✅")) } Thread { process.errorStream.bufferedReader().use { reader -> reader.lines().forEach { line -> println("[SERVER ERROR] $line") } } }.start() val reader = BufferedReader(InputStreamReader(process.inputStream)) var line: String? while (reader.readLine().also { line = it } != null) { // 로그 출력 (디버깅용) if (DETAILLOG) println("[Server $port] $line") if (line?.contains("server is listening") == true) { println("🚀 AI 서버 준비 완료 (Port: $port)") if (port == LLM_PORT){ AutoTradingManager.llmAnalyser = true } if (port == EMBEDDING_PORT){ AutoTradingManager.llmNews = true RagService.active() } if (processes.size > 1) { println("[Cache] ${processes.size}") } } } } catch (e: Exception) { println("❌ AI 서버 실행 실패 (Port: $port): ${e.message}") processes.remove(port) } } } else { println("🚨 포트 $port 가 보안 정책에 의해 막혀있어 서버 기동을 중단합니다.") TradingLogStore.addAnalyzer("System", "Port:$port", "보안 정책에 의한 포트 차단 감지", false) } } fun stopAll(): Boolean { var allStopped = true // 모든 프로세스 종료 여부를 추적하는 플래그 processes.forEach { (port, process) -> try { process.destroy() // 1차: 부드러운 종료 시도 // 2차: 최대 3초 대기 후 종료되지 않으면 강제 종료 if (!process.waitFor(3, java.util.concurrent.TimeUnit.SECONDS)) { process.destroyForcibly() // 강제 사살 // 강제 종료 후에도 프로세스가 살아있는지 최종 확인 if (process.isAlive) { println("❌ [Server $port] 강제 종료 명령 후에도 프로세스가 살아있습니다.") allStopped = false } else { println("⚠️ [Server $port] 응답이 없어 강제 종료되었습니다.") } } else { println("🛑 [Server $port] 정상 종료되었습니다.") } } catch (e: Exception) { println("❌ [Server $port] 종료 중 오류: ${e.message}") allStopped = false } } if (allStopped) { processes.clear() // 모든 프로세스가 성공적으로 종료되거나 리스트에서 제거될 준비가 된 경우 } return allStopped } }