Real-time log streaming via SSE

This commit is contained in:
SashegDev
2026-05-10 00:05:10 +00:00
parent e17b1d073a
commit ee1e4fa8d2
2 changed files with 69 additions and 53 deletions
@@ -258,6 +258,7 @@ public class JFXLauncher extends Application {
server.createContext("/api/launch", this::handleLaunch); server.createContext("/api/launch", this::handleLaunch);
server.createContext("/api/install", this::handleInstall); server.createContext("/api/install", this::handleInstall);
server.createContext("/api/logs", this::handleLogs); server.createContext("/api/logs", this::handleLogs);
server.createContext("/api/logs/stream", this::handleLogsStream);
server.createContext("/api/game-logs", this::handleGameLogs); server.createContext("/api/game-logs", this::handleGameLogs);
server.createContext("/api/mc-versions", this::handleMCVersions); server.createContext("/api/mc-versions", this::handleMCVersions);
server.createContext("/api/loader-versions", this::handleLoaderVersions); 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())); 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) { private void handleGameLogs(HttpExchange exchange) {
sendJson(exchange, Map.of("success", true, "data", getGameLogs())); sendJson(exchange, Map.of("success", true, "data", getGameLogs()));
} }
+25 -53
View File
@@ -1,5 +1,5 @@
const API_BASE = '/api'; const API_BASE = '/api';
let consoleLogPollingInterval = null; let consoleEventSource = null;
class LauncherApp { class LauncherApp {
constructor() { constructor() {
@@ -11,71 +11,41 @@ class LauncherApp {
this.selectedInstance = null; this.selectedInstance = null;
this.hasUpdate = false; this.hasUpdate = false;
this.hasMismatches = false; this.hasMismatches = false;
this.consoleLogBuffer = '';
this.init(); this.init();
} }
async init() { async init() {
this.bindEvents(); this.bindEvents();
this.initGridAnimation(); this.initGridAnimation();
this.startConsoleLogPolling();
await this.checkAuth(); await this.checkAuth();
} }
startConsoleLogPolling() { startConsoleLogStream() {
this.pollConsoleLogs(); if (consoleEventSource) {
consoleLogPollingInterval = setInterval(() => this.pollConsoleLogs(), 2000); consoleEventSource.close();
}
stopConsoleLogPolling() {
if (consoleLogPollingInterval) {
clearInterval(consoleLogPollingInterval);
consoleLogPollingInterval = null;
} }
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() { stopConsoleLogStream() {
try { if (consoleEventSource) {
const response = await fetch(`${API_BASE}/logs`); consoleEventSource.close();
const result = await response.json(); consoleEventSource = null;
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
} }
} }
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) { getLogType(line) {
if (line.includes('[STDOUT]') || line.includes('Render thread/ERROR') || line.includes('/ERROR]:')) return 'error'; 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'; 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.account = result.data;
this.username = result.data.username; this.username = result.data.username;
this.showMainScreen(); this.showMainScreen();
this.startConsoleLogStream();
await this.loadInstances(); await this.loadInstances();
} else { } else {
this.showLoginScreen(); this.showLoginScreen();
@@ -241,6 +212,7 @@ class LauncherApp {
this.account = result.data; this.account = result.data;
this.username = result.data.username; this.username = result.data.username;
this.showMainScreen(); this.showMainScreen();
this.startConsoleLogStream();
await this.loadInstances(); await this.loadInstances();
} else { } else {
this.showError(result.error || 'Ошибка входа'); this.showError(result.error || 'Ошибка входа');
@@ -248,6 +220,7 @@ class LauncherApp {
} }
async handleLogout() { async handleLogout() {
this.stopConsoleLogStream();
this.username = null; this.username = null;
this.account = null; this.account = null;
this.currentInstance = null; this.currentInstance = null;
@@ -610,9 +583,8 @@ class LauncherApp {
} }
clearLogs() { clearLogs() {
this.consoleLogBuffer = '';
const container = document.getElementById('logs-container'); const container = document.getElementById('logs-container');
container.innerHTML = '<div class="log-entry info">Логи очищены</div>'; container.innerHTML = '<div class="log-entry info">Логи очищены (консоль продолжает)</div>';
} }
showLoginScreen() { showLoginScreen() {