diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 717a759..a7a6756 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -77,8 +77,23 @@ fun getLlamaBinPath(): String { else -> "$basePath/llama-server" } } -fun main() = application { +fun initLogger(isDebug: Boolean) { + val root = org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger + + if (isDebug) { + root.level = ch.qos.logback.classic.Level.DEBUG + println("πŸ› οΈ 디버그 λͺ¨λ“œ: 상세 둜그λ₯Ό 좜λ ₯ν•©λ‹ˆλ‹€.") + } else { + root.level = ch.qos.logback.classic.Level.ERROR + // νŠΉμ • 라이브러리(Exposed)만 콕 μ§‘μ–΄μ„œ 끄기 + (org.slf4j.LoggerFactory.getLogger("Exposed") as ch.qos.logback.classic.Logger).level = ch.qos.logback.classic.Level.OFF + println("🀫 운영 λͺ¨λ“œ: μ—λŸ¬ 둜그만 좜λ ₯ν•©λ‹ˆλ‹€.") + } +} + +fun main() = application { + initLogger(AutoTradingManager.DETAILLOG) val trayState = rememberTrayState() var isWindowOpen by remember { mutableStateOf(false) } // 창의 ν‘œμ‹œ μƒνƒœ 관리 @@ -193,10 +208,10 @@ fun main() = application { } if (config.modelPath.isNotEmpty()) { - LlamaServerManager.startServer(binPath, config.modelPath,port = 8080) + LlamaServerManager.startServer(binPath, config.modelPath,port = AutoTradingManager.LLM_PORT) } if (config.embedModelPath.isNotEmpty()) { - LlamaServerManager.startServer(binPath, config.embedModelPath, port = 8081) + LlamaServerManager.startServer(binPath, config.embedModelPath, port = AutoTradingManager.EMBEDDING_PORT) } // λŒ€μ‹œλ³΄λ“œλ‘œ ν™”λ©΄ μ „ν™˜ diff --git a/src/main/kotlin/network/RagService.kt b/src/main/kotlin/network/RagService.kt index 416a52c..208fca7 100644 --- a/src/main/kotlin/network/RagService.kt +++ b/src/main/kotlin/network/RagService.kt @@ -33,6 +33,7 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import org.apache.lucene.store.MMapDirectory import org.slf4j.MDC.put +import service.AutoTradingManager import service.FinancialAnalyzer import service.InvestmentScores import service.TechnicalAnalyzer @@ -55,12 +56,12 @@ object RagService { // μž„λ² λ”© λͺ¨λΈ (8081) 및 μ±„νŒ… λͺ¨λΈ (8080) μ„€μ • private val embeddingModel = OpenAiEmbeddingModel.builder() - .baseUrl("http://127.0.0.1:8081/v1") + .baseUrl("http://127.0.0.1:${AutoTradingManager.EMBEDDING_PORT}/v1") .apiKey("unused") .build() private val chatModel = OpenAiChatModel.builder() - .baseUrl("http://127.0.0.1:8080/v1") + .baseUrl("http://127.0.0.1:${AutoTradingManager.LLM_PORT}/v1") .apiKey("unused") .temperature(0.0) // [μ€‘μš”] 0.0으둜 μ„€μ •ν•˜μ—¬ 결정둠적 응닡 μœ λ„ .timeout(Duration.ofSeconds(60)) diff --git a/src/main/kotlin/service/AutoTradingManager.kt b/src/main/kotlin/service/AutoTradingManager.kt index 5b43996..391c0a4 100644 --- a/src/main/kotlin/service/AutoTradingManager.kt +++ b/src/main/kotlin/service/AutoTradingManager.kt @@ -49,6 +49,9 @@ import kotlin.math.* // service/AutoTradingManager.kt typealias TradingDecisionCallback = (TradingDecision?, Boolean)->Unit object AutoTradingManager { + val DETAILLOG = true + val LLM_PORT = 13080 + val EMBEDDING_PORT = 13081 private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private var discoveryJob: Job? = null @@ -418,10 +421,10 @@ object AutoTradingManager { val config = KisSession.config // LLM μ„œλ²„ μ‹œμž‘ (μ„€μ •λœ λͺ¨λΈ 경둜 μ‚¬μš©) if (config.modelPath.isNotEmpty()) { - LlamaServerManager.startServer(binPath, config.modelPath,port = 8080) + LlamaServerManager.startServer(binPath, config.modelPath,port = LLM_PORT) } if (config.embedModelPath.isNotEmpty()) { - LlamaServerManager.startServer(binPath, config.embedModelPath, port = 8081) + LlamaServerManager.startServer(binPath, config.embedModelPath, port = EMBEDDING_PORT) } KisWebSocketManager.connect() isSystemReadyToday = true diff --git a/src/main/kotlin/service/LlamaServerManager.kt b/src/main/kotlin/service/LlamaServerManager.kt index 2aeae7b..6b4da8a 100644 --- a/src/main/kotlin/service/LlamaServerManager.kt +++ b/src/main/kotlin/service/LlamaServerManager.kt @@ -7,6 +7,7 @@ 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 @@ -50,6 +51,7 @@ object LlamaServerManager { fun checkPortStatus(port: Int): String { return try { + // netstat λͺ…λ Ήμ–΄λ‘œ ν•΄λ‹Ή 포트λ₯Ό 점유 쀑인 ν”„λ‘œμ„ΈμŠ€ 확인 val process = Runtime.getRuntime().exec("cmd /c netstat -ano | findstr :$port") val reader = process.inputStream.bufferedReader() @@ -67,110 +69,107 @@ object LlamaServerManager { } fun startServer(binPath: String, modelPath: String, port: Int) { - - if (processes.containsKey(port) || modelPath.isBlank()) return 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 cpuCores = Runtime.getRuntime().availableProcessors() // HardwareDetector.getCpuCores()와 동일 - val hasGpu = HardwareDetector.hasNvidiaGpu() + 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 = (cpuCores * ratio).toInt().coerceIn(4, 16) -// 1. optimalThreads: ν• λ‹Ή λΉ„μœ¨ 적용 및 μ΅œμ†Œ/μ΅œλŒ€ λ²”μœ„ μ œν•œ(Safety Boundary) -// κ³Όλ„ν•œ μŠ€λ ˆλ“œ 할당은 였히렀 μ»¨ν…μŠ€νŠΈ μŠ€μœ„μΉ­ λΉ„μš©μ„ λ†’μ—¬ μ„±λŠ₯을 μ €ν•˜μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€. - val ratio = if (isWin) 0.5 else 0.7 - val optimalThreads = (cpuCores * ratio).toInt().coerceIn(4, 16) + var optimalGpuLayers = 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"})") -// 2. optimalGpuLayers: GPU 가속 쑰건 (μœˆλ„μš° NVIDIA λ˜λŠ” λ§₯ ARM) - var optimalGpuLayers = 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 == AutoTradingManager.EMBEDDING_PORT) "512" else "8192", + "-ngl", optimalGpuLayers.toString(), + "-t", optimalThreads.toString(), + "--embedding" + ) + if (port != AutoTradingManager.EMBEDDING_PORT) { // ν…μŠ€νŠΈ μƒμ„±μš© λͺ¨λΈμ—λ§Œ 적용 + command.addAll(listOf( + "-b", "512", // Batch size (토큰 병렬 μ²˜λ¦¬λŸ‰ μ œν•œμœΌλ‘œ μ—°μ‚° μ•ˆμ •ν™”) + "--threads-batch", optimalThreads.toString(), + "-fa","on" // Flash Attention ν™œμ„±ν™” (λ©”λͺ¨λ¦¬ μ ˆμ•½ 및 κΈ΄ μ»¨ν…μŠ€νŠΈ μ—°μ‚° μ•ˆμ •μ„± 증가) + )) + } + scope.launch { + try { + val pb = ProcessBuilder(command) -// val (nGpuLayers, threads) = when { -// os.contains("mac") && (arch.contains("arm64") || arch.contains("aarch64")) -> 99 to 8 -// isWin -> optimalGpuLayers to optimalThreads // NUC Core Ultra 7: GPU λ ˆμ΄μ–΄ 40 λ‚΄μ™Έ, μŠ€λ ˆλ“œ 12 ꢌμž₯ -// else -> 0 to 4 // 인텔 λ§₯ 2017 λ“± -// } + // 2. μœˆλ„μš° Vulkan ν™˜κ²½ λ³€μˆ˜ μ„€μ • + if (isWin && binPath.contains("win-x64")) { + val env = pb.environment() + // νŠΉμ • GPU 선택 (λ‚΄μž₯ GPUκ°€ μ—¬λŸ¬ 개일 경우) + // env["GGML_VULKAN_DEVICE"] = "0" - val command = mutableListOf( - binPath, - "-m", modelPath, - "--port", port.toString(), - "-c", if (port == 8081) "512" else "8192", - "-ngl", optimalGpuLayers.toString(), - "-t", optimalThreads.toString(), - "--embedding" - ) - if (port != 8081) { // ν…μŠ€νŠΈ μƒμ„±μš© λͺ¨λΈμ—λ§Œ 적용 - command.addAll(listOf( - "-b", "512", // Batch size (토큰 병렬 μ²˜λ¦¬λŸ‰ μ œν•œμœΌλ‘œ μ—°μ‚° μ•ˆμ •ν™”) - "--threads-batch", optimalThreads.toString(), - "-fa","on" // Flash Attention ν™œμ„±ν™” (λ©”λͺ¨λ¦¬ μ ˆμ•½ 및 κΈ΄ μ»¨ν…μŠ€νŠΈ μ—°μ‚° μ•ˆμ •μ„± 증가) - )) - } - scope.launch { - try { - val pb = ProcessBuilder(command) + // DLL λ‘œλ“œ 경둜 κ°•μ œ μ§€μ • (bin 폴더 λ‚΄ dll μ°Έμ‘°) + val libraryPath = File(binPath).parentFile.absolutePath + val currentPath = System.getenv("PATH") ?: "" + env["PATH"] = "$libraryPath;$currentPath" - // 2. μœˆλ„μš° Vulkan ν™˜κ²½ λ³€μˆ˜ μ„€μ • - if (isWin && binPath.contains("win-x64")) { - val env = pb.environment() - // νŠΉμ • GPU 선택 (λ‚΄μž₯ GPUκ°€ μ—¬λŸ¬ 개일 경우) - // env["GGML_VULKAN_DEVICE"] = "0" + println("πŸ”§ [Vulkan] ν™˜κ²½ λ³€μˆ˜ μ„€μ • μ™„λ£Œ: $libraryPath") + } - // DLL λ‘œλ“œ 경둜 κ°•μ œ μ§€μ • (bin 폴더 λ‚΄ dll μ°Έμ‘°) - val libraryPath = File(binPath).parentFile.absolutePath - val currentPath = System.getenv("PATH") ?: "" - env["PATH"] = "$libraryPath;$currentPath" + pb.redirectErrorStream(true) + File(binPath).setExecutable(true) - println("πŸ”§ [Vulkan] ν™˜κ²½ λ³€μˆ˜ μ„€μ • μ™„λ£Œ: $libraryPath") - } + val process = pb.start() + processes[port] = process + println("βœ… AI μ„œλ²„ μ‹œμž‘ μ‹œλ„ (Port: $port, Model: ${File(modelPath).name})") + if (isWin) { + delay(3000) - pb.redirectErrorStream(true) - File(binPath).setExecutable(true) + val status = checkPortStatus(port) + println(status) // μ½˜μ†” 둜그 + TradingLogStore.addAnalyzer("System", "Port:$port", status, status.contains("βœ…")) + } - 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) { + // 둜그 좜λ ₯ (λ””λ²„κΉ…μš©) + if (AutoTradingManager.DETAILLOG) println("[Server $port] $line") - delay(3000) - - val status = checkPortStatus(port) - println(status) // μ½˜μ†” 둜그 - - // UI 둜그 μŠ€ν† μ–΄μ—λ„ 기둝 (TradingDecisionLog λ“±μ—μ„œ 확인 κ°€λŠ₯) - TradingLogStore.addAnalyzer("System", "Port:$port", status, status.contains("βœ…")) - - 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 (port == 8080){ - AutoTradingManager.llmAnalyser = true - } - if (port == 8081){ - AutoTradingManager.llmNews = true - } - if (processes.size > 1) { - println("[Cache] ${processes.size}") - RagService.active() + if (line?.contains("server is listening") == true) { + println("πŸš€ AI μ„œλ²„ μ€€λΉ„ μ™„λ£Œ (Port: $port)") + if (port == AutoTradingManager.LLM_PORT){ + AutoTradingManager.llmAnalyser = true + } + if (port == AutoTradingManager.EMBEDDING_PORT){ + AutoTradingManager.llmNews = true + } + if (processes.size > 1) { + println("[Cache] ${processes.size}") + RagService.active() + } } } + } catch (e: Exception) { + println("❌ AI μ„œλ²„ μ‹€ν–‰ μ‹€νŒ¨ (Port: $port): ${e.message}") + processes.remove(port) } - } catch (e: Exception) { - println("❌ AI μ„œλ²„ μ‹€ν–‰ μ‹€νŒ¨ (Port: $port): ${e.message}") - processes.remove(port) - } + } + } else { + println("🚨 포트 $port κ°€ λ³΄μ•ˆ 정책에 μ˜ν•΄ λ§‰ν˜€μžˆμ–΄ μ„œλ²„ 기동을 μ€‘λ‹¨ν•©λ‹ˆλ‹€.") + TradingLogStore.addAnalyzer("System", "Port:$port", "λ³΄μ•ˆ 정책에 μ˜ν•œ 포트 차단 감지", false) } + + } fun stopAll(): Boolean { diff --git a/src/main/kotlin/util/NetworkPortDiagnostic.kt b/src/main/kotlin/util/NetworkPortDiagnostic.kt new file mode 100644 index 0000000..58a452d --- /dev/null +++ b/src/main/kotlin/util/NetworkPortDiagnostic.kt @@ -0,0 +1,53 @@ +package util + +import java.net.ServerSocket +import java.net.InetSocketAddress + +object NetworkPortDiagnostic { + + /** + * νŠΉμ • ν¬νŠΈμ— μ†ŒμΌ“μ„ 직접 열어보고 λ‹«μŒμœΌλ‘œμ¨ λ„€νŠΈμ›Œν¬ κ°€μš©μ„±μ„ ν…ŒμŠ€νŠΈν•©λ‹ˆλ‹€. + */ + fun testPortAvailability(port: Int): Boolean { + return try { + // 1. ServerSocket 생성 및 바인딩 μ‹œλ„ + val serverSocket = ServerSocket() + // νƒ€μž„μ•„μ›ƒ 2초 μ„€μ •ν•˜μ—¬ λ¬΄ν•œ λŒ€κΈ° λ°©μ§€ + serverSocket.reuseAddress = true + serverSocket.bind(InetSocketAddress("127.0.0.1", port)) + + println("βœ… [Diagnosis] 포트 $port 개방 성곡: μ‹œμŠ€ν…œ 및 λ³΄μ•ˆ ν”„λ‘œκ·Έλž¨μ—μ„œ ν—ˆμš©λ¨.") + + // 2. ν…ŒμŠ€νŠΈ 성곡 ν›„ μ¦‰μ‹œ 포트 λ‹«κΈ° + serverSocket.close() + println("βœ… [Diagnosis] ν…ŒμŠ€νŠΈ μ’…λ£Œ ν›„ 포트 $port μ •μƒμ μœΌλ‘œ λ‹€μ‹œ λ‹«νž˜.") + true + } catch (e: Exception) { + println("❌ [Diagnosis] 포트 $port 개방 μ‹€νŒ¨: ${e.message}") + // μ—λŸ¬ λ©”μ‹œμ§€μ— 'Permission denied'κ°€ ν¬ν•¨λ˜λ©΄ λ³΄μ•ˆ ν”„λ‘œκ·Έλž¨(μ•ˆλž© λ“±) 차단일 ν™•λ₯ μ΄ λ†’μŒ + false + } + } + + /** + * 기쑴에 κ΅¬ν˜„ν•˜μ‹  netstat 방식을 κ²°ν•©ν•˜μ—¬ 상세 둜그λ₯Ό λ‚¨κΉλ‹ˆλ‹€. + */ + fun runFullDiagnostic(port: Int) { + val isHardwareReady = testPortAvailability(port) + + if (!isHardwareReady) { + println("⚠️ [Alert] OS λ ˆλ²¨μ—μ„œ 포트 $port μ‚¬μš©μ΄ κ±°λΆ€λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ³΄μ•ˆ ν”„λ‘œκ·Έλž¨ 섀정을 ν™•μΈν•˜μ„Έμš”.") + return + } + + // netstat μ‹€ν–‰ 둜직 (κΈ°μ‘΄ NetworkChecker ν™œμš©) + val process = Runtime.getRuntime().exec("cmd /c netstat -ano | findstr :$port") + val result = process.inputStream.bufferedReader().readText() + + if (result.contains("LISTENING")) { + println("ℹ️ ν˜„μž¬ 포트 $port λ₯Ό λ‹€λ₯Έ ν”„λ‘œμ„ΈμŠ€κ°€ 점유 μ€‘μž…λ‹ˆλ‹€.") + } else { + println("βœ… 포트 $port λŠ” ν˜„μž¬ κΉ¨λ—ν•˜λ©° μ‚¬μš© κ°€λŠ₯ν•œ μƒνƒœμž…λ‹ˆλ‹€.") + } + } +} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index cb49365..225ba31 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,7 +1,10 @@ - + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + - - + - + \ No newline at end of file