Files
1960-utils/ReforgerOutputConsole/server.js
T
2026-09-26 18:37:30 +02:00

235 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Reforger Output Console
// Überwacht mehrere Log-Ordner, nimmt jeweils den neuesten Unterordner und
// streamt die darin liegende Log-Datei live an eine Browser-Oberfläche.
const http = require('http');
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const { StringDecoder } = require('string_decoder');
const CONFIG_PATH = path.join(__dirname, 'config.json');
const INDEX_PATH = path.join(__dirname, 'index.html');
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
const PORT = config.port || 8420;
const POLL_MS = config.pollIntervalMs || 500;
const MAX_LINES = config.maxLinesPerPane || 20000;
// Server-seitiger Puffer, damit neu verbundene Browser den bisherigen Inhalt bekommen.
const MAX_CHARS = MAX_LINES * 300;
const CRASH_FILE = config.crashFile || 'crash.log';
const panes = config.panes.map((p, i) => ({
id: i,
name: p.name || path.basename(path.dirname(p.logsDir)),
logsDir: path.resolve(__dirname, p.logsDir),
file: p.file || 'console.log',
folder: undefined,
offset: 0,
decoder: new StringDecoder('utf8'),
text: '',
dirMissing: false,
crash: null,
crashStamp: '',
}));
const clients = new Set();
function broadcast(event, data) {
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const res of clients) res.write(msg);
}
function snapshot(p) {
return {
id: p.id,
name: p.name,
file: p.file,
folder: p.folder || null,
logsDir: p.logsDir,
dirMissing: p.dirMissing,
text: p.text,
crash: p.crash,
};
}
// Neuester Unterordner nach Erstellungszeit (bei Gleichstand nach Name).
function newestFolder(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return undefined;
}
let best = null;
let bestTime = -Infinity;
for (const e of entries) {
if (!e.isDirectory()) continue;
let t;
try {
t = fs.statSync(path.join(dir, e.name)).birthtimeMs;
} catch {
continue;
}
if (t > bestTime || (t === bestTime && e.name > best)) {
best = e.name;
bestTime = t;
}
}
return best;
}
function resetPane(p) {
p.offset = 0;
p.decoder = new StringDecoder('utf8');
p.text = '';
p.crash = null;
p.crashStamp = '';
broadcast('reset', snapshot(p));
}
// Crash-Report ist klein und wird bei jeder Änderung komplett neu gesendet.
function pollCrash(p) {
const file = path.join(p.logsDir, p.folder, CRASH_FILE);
let st;
try {
st = fs.statSync(file);
} catch {
return;
}
const stamp = `${st.size}:${st.mtimeMs}`;
if (stamp === p.crashStamp) return;
let text;
try {
text = fs.readFileSync(file, 'utf8');
} catch {
return;
}
if (p.crash === null) console.log(`[${p.name}] Crash-Report gefunden: ${p.folder}\\${CRASH_FILE}`);
p.crashStamp = stamp;
p.crash = text;
broadcast('crash', { id: p.id, text });
}
function pollPane(p) {
const folder = newestFolder(p.logsDir);
const dirMissing = folder === undefined;
if (folder !== p.folder || dirMissing !== p.dirMissing) {
if (p.folder !== undefined && folder) {
console.log(`[${p.name}] Neuer Log-Ordner: ${folder}`);
}
p.folder = folder;
p.dirMissing = dirMissing;
resetPane(p);
}
if (!folder) return;
pollCrash(p);
const file = path.join(p.logsDir, folder, p.file);
let size;
try {
size = fs.statSync(file).size;
} catch {
return; // Datei existiert (noch) nicht
}
if (size < p.offset) resetPane(p); // Datei wurde gekürzt/neu geschrieben
if (size === p.offset) return;
let start = p.offset;
if (size - start > MAX_CHARS) start = size - MAX_CHARS;
const len = size - start;
const buf = Buffer.alloc(len);
let fd;
try {
fd = fs.openSync(file, 'r');
fs.readSync(fd, buf, 0, len, start);
} catch {
return;
} finally {
if (fd !== undefined) fs.closeSync(fd);
}
p.offset = size;
const chunk = p.decoder.write(buf);
if (!chunk) return;
p.text += chunk;
if (p.text.length > MAX_CHARS) {
const cut = p.text.indexOf('\n', p.text.length - MAX_CHARS);
p.text = p.text.slice(cut + 1);
}
broadcast('append', { id: p.id, chunk });
}
function pollAll() {
for (const p of panes) {
try {
pollPane(p);
} catch (err) {
console.error(`[${p.name}]`, err.message);
}
}
}
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
fs.createReadStream(INDEX_PATH).pipe(res);
return;
}
if (url.pathname === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
res.write(`event: init\ndata: ${JSON.stringify({ maxLines: MAX_LINES, panes: panes.map(snapshot) })}\n\n`);
clients.add(res);
req.on('close', () => clients.delete(res));
return;
}
// Öffnet den aktuellen Log-Ordner eines Panes im Explorer.
if (url.pathname === '/open' && req.method === 'POST') {
const p = panes[Number(url.searchParams.get('id'))];
if (p) {
const dir = p.folder ? path.join(p.logsDir, p.folder) : p.logsDir;
spawn('explorer', [dir], { detached: true, stdio: 'ignore' }).unref();
}
res.writeHead(204).end();
return;
}
res.writeHead(404).end();
});
// Keep-alive, damit Verbindungen nicht wegen Inaktivität geschlossen werden.
setInterval(() => {
for (const res of clients) res.write(': ping\n\n');
}, 15000);
server.listen(PORT, '127.0.0.1', () => {
const url = `http://localhost:${PORT}`;
console.log(`Reforger Output Console läuft auf ${url}`);
for (const p of panes) console.log(` - ${p.name}: ${p.logsDir}\\<neuester Ordner>\\${p.file}`);
console.log('Beenden mit Strg+C.');
pollAll();
setInterval(pollAll, POLL_MS);
if (config.openBrowser !== false) {
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
}
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${PORT} ist schon belegt – läuft die Konsole bereits? Sonst "port" in config.json ändern.`);
} else {
console.error(err);
}
process.exit(1);
});