diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java index e6e836e..d075e58 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java @@ -258,6 +258,7 @@ public class JFXLauncher extends Application { server.createContext("/api/launch", this::handleLaunch); server.createContext("/api/install", this::handleInstall); server.createContext("/api/logs", this::handleLogs); + server.createContext("/api/logs/stream", this::handleLogsStream); server.createContext("/api/game-logs", this::handleGameLogs); server.createContext("/api/mc-versions", this::handleMCVersions); server.createContext("/api/loader-versions", this::handleLoaderVersions); @@ -396,6 +397,49 @@ public class JFXLauncher extends Application { sendJson(exchange, Map.of("success", true, "data", logBuffer.toString())); } + private void handleLogsStream(HttpExchange exchange) { + try { + exchange.getResponseHeaders().set("Content-Type", "text/event-stream"); + exchange.getResponseHeaders().set("Cache-Control", "no-cache"); + exchange.getResponseHeaders().set("Connection", "keep-alive"); + exchange.sendResponseHeaders(200, 0); + + String lastLog = ""; + int sameCount = 0; + final OutputStream os = exchange.getResponseBody(); + + Thread.currentThread().setName("LogStream-" + System.currentTimeMillis()); + + for (int i = 0; !Thread.currentThread().isInterrupted() && i < 3000; i++) { + String currentLog = logBuffer.toString(); + if (!currentLog.equals(lastLog)) { + String newContent = currentLog.substring(lastLog.length()); + if (!newContent.isEmpty()) { + for (String line : newContent.split("\n")) { + if (!line.trim().isEmpty()) { + String data = "data: " + line.replace("\n", "").replace("\r", "") + "\n\n"; + os.write(data.getBytes(StandardCharsets.UTF_8)); + os.flush(); + } + } + } + lastLog = currentLog; + sameCount = 0; + } else { + sameCount++; + if (sameCount > 50) { + os.write("data: \n\n".getBytes(StandardCharsets.UTF_8)); + os.flush(); + sameCount = 0; + } + } + Thread.sleep(100); + } + } catch (Exception ignored) {} finally { + try { exchange.getResponseBody().close(); } catch (Exception ignored) {} + } + } + private void handleGameLogs(HttpExchange exchange) { sendJson(exchange, Map.of("success", true, "data", getGameLogs())); } diff --git a/launcher/launcher/src/resources/ui/launcher.js b/launcher/launcher/src/resources/ui/launcher.js index 7d5c22c..d6a12d1 100644 --- a/launcher/launcher/src/resources/ui/launcher.js +++ b/launcher/launcher/src/resources/ui/launcher.js @@ -1,5 +1,5 @@ const API_BASE = '/api'; -let consoleLogPollingInterval = null; +let consoleEventSource = null; class LauncherApp { constructor() { @@ -11,71 +11,41 @@ class LauncherApp { this.selectedInstance = null; this.hasUpdate = false; this.hasMismatches = false; - this.consoleLogBuffer = ''; this.init(); } async init() { this.bindEvents(); this.initGridAnimation(); - this.startConsoleLogPolling(); await this.checkAuth(); } - startConsoleLogPolling() { - this.pollConsoleLogs(); - consoleLogPollingInterval = setInterval(() => this.pollConsoleLogs(), 2000); - } - - stopConsoleLogPolling() { - if (consoleLogPollingInterval) { - clearInterval(consoleLogPollingInterval); - consoleLogPollingInterval = null; + startConsoleLogStream() { + if (consoleEventSource) { + consoleEventSource.close(); } + + consoleEventSource = new EventSource(API_BASE + '/logs/stream'); + + consoleEventSource.onmessage = (event) => { + if (event.data && event.data.trim()) { + this.addLog(event.data, this.getLogType(event.data)); + } + }; + + consoleEventSource.onerror = () => { + consoleEventSource.close(); + setTimeout(() => this.startConsoleLogStream(), 3000); + }; } - async pollConsoleLogs() { - try { - const response = await fetch(`${API_BASE}/logs`); - const result = await response.json(); - if (result.success && result.data) { - const newLogs = result.data; - if (newLogs !== this.consoleLogBuffer) { - this.consoleLogBuffer = newLogs; - this.syncConsoleLogs(newLogs); - } - } - } catch (e) { - // Silent fail for polling + stopConsoleLogStream() { + if (consoleEventSource) { + consoleEventSource.close(); + consoleEventSource = null; } } - syncConsoleLogs(fullLogs) { - const lines = fullLogs.split('\n').filter(l => l.trim()); - const container = document.getElementById('logs-container'); - if (!container) return; - - const existingLines = Array.from(container.querySelectorAll('.log-entry')).length; - const newLines = lines.slice(existingLines); - - newLines.forEach(line => { - if (!line.trim()) return; - const entry = document.createElement('div'); - entry.className = 'log-entry ' + this.getLogType(line); - - const timestampMatch = line.match(/^\[(\d{2}:\d{2}:\d{2})\]/); - if (timestampMatch) { - entry.textContent = line; - } else { - const time = new Date().toLocaleTimeString(); - entry.textContent = '[' + time + '] ' + line; - } - container.appendChild(entry); - }); - - container.scrollTop = container.scrollHeight; - } - getLogType(line) { if (line.includes('[STDOUT]') || line.includes('Render thread/ERROR') || line.includes('/ERROR]:')) return 'error'; if (line.includes('[STDOUT]') || line.includes('Render thread/WARN') || line.includes('/WARN]:')) return 'warning'; @@ -202,6 +172,7 @@ class LauncherApp { this.account = result.data; this.username = result.data.username; this.showMainScreen(); + this.startConsoleLogStream(); await this.loadInstances(); } else { this.showLoginScreen(); @@ -241,6 +212,7 @@ class LauncherApp { this.account = result.data; this.username = result.data.username; this.showMainScreen(); + this.startConsoleLogStream(); await this.loadInstances(); } else { this.showError(result.error || 'Ошибка входа'); @@ -248,6 +220,7 @@ class LauncherApp { } async handleLogout() { + this.stopConsoleLogStream(); this.username = null; this.account = null; this.currentInstance = null; @@ -610,9 +583,8 @@ class LauncherApp { } clearLogs() { - this.consoleLogBuffer = ''; const container = document.getElementById('logs-container'); - container.innerHTML = '
Логи очищены
'; + container.innerHTML = '
Логи очищены (консоль продолжает)
'; } showLoginScreen() {