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/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()));
}
+23 -51
View File
@@ -1,5 +1,5 @@
const API_BASE = '/api';
let consoleLogPollingInterval = null;
let consoleEventSource = null;
class LauncherApp {
constructor() {
@@ -11,70 +11,40 @@ 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);
startConsoleLogStream() {
if (consoleEventSource) {
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() {
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);
stopConsoleLogStream() {
if (consoleEventSource) {
consoleEventSource.close();
consoleEventSource = null;
}
}
} 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) {
if (line.includes('[STDOUT]') || line.includes('Render thread/ERROR') || line.includes('/ERROR]:')) return 'error';
@@ -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 = '<div class="log-entry info">Логи очищены</div>';
container.innerHTML = '<div class="log-entry info">Логи очищены (консоль продолжает)</div>';
}
showLoginScreen() {