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/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()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,70 +11,40 @@ 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() {
|
consoleEventSource = new EventSource(API_BASE + '/logs/stream');
|
||||||
if (consoleLogPollingInterval) {
|
|
||||||
clearInterval(consoleLogPollingInterval);
|
consoleEventSource.onmessage = (event) => {
|
||||||
consoleLogPollingInterval = null;
|
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';
|
||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user