Real-time log streaming via SSE
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user