diff --git a/ReforgerOutputConsole/Start.bat b/ReforgerOutputConsole/Start.bat new file mode 100644 index 0000000..d4b9fa8 --- /dev/null +++ b/ReforgerOutputConsole/Start.bat @@ -0,0 +1,5 @@ +@echo off +title Reforger Output Console +cd /d "%~dp0" +node server.js +pause diff --git a/ReforgerOutputConsole/config.json b/ReforgerOutputConsole/config.json new file mode 100644 index 0000000..f2cbdac --- /dev/null +++ b/ReforgerOutputConsole/config.json @@ -0,0 +1,23 @@ +{ + "port": 8420, + "openBrowser": true, + "pollIntervalMs": 500, + "maxLinesPerPane": 20000, + "panes": [ + { + "name": "Dedicated Server", + "logsDir": "C:/Users/nicho/Documents/My Games/DedicatedServer/logs", + "file": "console.log" + }, + { + "name": "PeerPlugin11", + "logsDir": "C:/Users/nicho/Documents/My Games/PeerPlugin11/logs", + "file": "console.log" + }, + { + "name": "PeerPlugin22", + "logsDir": "C:/Users/nicho/Documents/My Games/PeerPlugin22/logs", + "file": "console.log" + } + ] +} diff --git a/ReforgerOutputConsole/index.html b/ReforgerOutputConsole/index.html new file mode 100644 index 0000000..bc57f1c --- /dev/null +++ b/ReforgerOutputConsole/index.html @@ -0,0 +1,505 @@ + + + + +Reforger Output Console + + + +
+

Reforger Output Console

+ verbinde… + +
+
+ + + + + + diff --git a/ReforgerOutputConsole/server.js b/ReforgerOutputConsole/server.js new file mode 100644 index 0000000..25d77fd --- /dev/null +++ b/ReforgerOutputConsole/server.js @@ -0,0 +1,234 @@ +// 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}\\\\${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); +});