- Socket-Nachrichten werden geprüft, Handlerfehler abgefangen, Abfragen pro Verbindung begrenzt (vorher beendete stream:subscribe mit null ohne Anmeldung das Backend). - Fehlermeldungen von Prozessen ohne Befehlszeile und mit geschwärzten Passwörtern/Tokens. - Logparser wertet nur ganze Systemzeilen nach dem Zeitstempel aus; Chat kann keine Map beenden. - Verspätet ankommende Starts nach Stopp, Neuanforderung oder Löschen bauen ihren Container ab. - RCON: kaputte Pakete trennen nur die Verbindung, Befehle ohne Antwort gelten als Fehler. - Organisatoren bekommen einen Stream-Schlüssel nur für ihr Match statt des globalen STREAM_KEY. - Rate-Limiter mit regelmäßiger Bereinigung und Obergrenze, alte Admin-Sitzungen werden entfernt.
63 lines
2.4 KiB
JavaScript
63 lines
2.4 KiB
JavaScript
import { parseBackupFiles } from '../rounds.js';
|
||
import { parseStatus, runRcon } from './rcon.js';
|
||
|
||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
||
/**
|
||
* Gemeinsamer Konfigurationsablauf für echte Server per RCON:
|
||
* Befehle vor dem Mapwechsel senden, warten bis die Map geladen ist, dann Match-Einstellungen setzen.
|
||
*/
|
||
export async function configureViaRcon(target, plan, { loadTimeoutMs = 120_000, pollMs = 3000 } = {}) {
|
||
try {
|
||
await runRcon(target, plan.beforeMapChange);
|
||
} catch (err) {
|
||
// Beim changelevel trennt der Server die RCON-Verbindung teilweise oder antwortet erst nach dem Laden –
|
||
// beides ist kein Fehler, ob die Map lädt, prüft die Schleife unten.
|
||
if (!/geschlossen|keine Antwort auf "changelevel"/.test(err.message)) throw new Error(`Befehle konnten nicht gesendet werden: ${err.message}`);
|
||
}
|
||
|
||
const deadline = Date.now() + loadTimeoutMs;
|
||
let lastError = null;
|
||
for (;;) {
|
||
await sleep(pollMs);
|
||
try {
|
||
const [status] = await runRcon(target, ['status']);
|
||
const parsed = parseStatus(status);
|
||
if (parsed.map === plan.map || (!parsed.map && status.includes(plan.map))) break;
|
||
lastError = `aktuell geladen: ${parsed.map ?? 'unbekannt'}`;
|
||
} catch (err) {
|
||
lastError = err.message;
|
||
}
|
||
if (Date.now() > deadline) {
|
||
throw new Error(`Map ${plan.map} wurde nicht rechtzeitig geladen (${lastError ?? 'keine Antwort'})`);
|
||
}
|
||
}
|
||
|
||
try {
|
||
await runRcon(target, plan.afterMapChange);
|
||
} catch (err) {
|
||
throw new Error(`Match-Einstellungen konnten nicht gesetzt werden: ${err.message}`);
|
||
}
|
||
}
|
||
|
||
export async function rconStatus(target) {
|
||
const [output] = await runRcon(target, ['status'], { timeoutMs: 4000 });
|
||
return parseStatus(output);
|
||
}
|
||
|
||
/** Sicherungsdateien dieses Matches vom Server holen. */
|
||
export async function listRoundBackups(target, matchId) {
|
||
const [out] = await runRcon(target, ['mp_backup_restore_list_files 60'], { timeoutMs: 8000, quietMs: 500 });
|
||
return parseBackupFiles(out, matchId);
|
||
}
|
||
|
||
/** Sicherung laden. Der Dateiname muss vorher mit isValidBackupFile geprüft sein. */
|
||
export async function loadRoundBackup(target, file) {
|
||
// mp_backup_restore_load_autopause wirkt in CS2 nicht zuverlässig, deshalb pausieren wir ausdrücklich
|
||
await runRcon(target, [
|
||
'mp_backup_restore_load_autopause 1',
|
||
`mp_backup_restore_load_file "${file}"`,
|
||
'mp_pause_match',
|
||
], { timeoutMs: 15000, quietMs: 800 });
|
||
}
|