CS2_DOCKER_HOSTS nimmt eine Liste von Docker-Hosts. Jedes Match läuft auf dem Host mit den meisten freien Ports und merkt sich seinen Host, damit Status, Konfiguration und Stopp dort landen. Ohne die Variable bleibt alles beim bisherigen einzelnen Host. deploy/docker-host-einrichten.sh richtet einen Host per SSH ein.
153 lines
6.1 KiB
JavaScript
153 lines
6.1 KiB
JavaScript
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import dotenv from 'dotenv';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
export const SERVER_ROOT = path.resolve(here, '..');
|
|
export const REPO_ROOT = path.resolve(SERVER_ROOT, '..');
|
|
|
|
dotenv.config({ path: path.join(REPO_ROOT, '.env') });
|
|
|
|
function int(name, fallback) {
|
|
const raw = process.env[name];
|
|
if (raw === undefined || raw === '') return fallback;
|
|
const value = Number.parseInt(raw, 10);
|
|
if (Number.isNaN(value)) throw new Error(`Umgebungsvariable ${name} muss eine Zahl sein (ist: "${raw}")`);
|
|
return value;
|
|
}
|
|
|
|
function bool(name, fallback) {
|
|
const raw = process.env[name];
|
|
if (raw === undefined || raw === '') return fallback;
|
|
return ['1', 'true', 'yes', 'ja', 'on'].includes(raw.toLowerCase());
|
|
}
|
|
|
|
function parseDockerHosts(raw) {
|
|
if (!raw) return null;
|
|
let list;
|
|
try {
|
|
list = JSON.parse(raw);
|
|
} catch {
|
|
throw new Error('CS2_DOCKER_HOSTS ist kein gültiges JSON');
|
|
}
|
|
if (!Array.isArray(list) || list.length === 0) throw new Error('CS2_DOCKER_HOSTS braucht mindestens einen Eintrag');
|
|
const names = new Set();
|
|
return list.map((h) => {
|
|
if (!h.name || !h.publicHost) throw new Error('Jeder Docker-Host braucht name und publicHost');
|
|
if (names.has(h.name)) throw new Error(`Docker-Host-Name "${h.name}" kommt doppelt vor`);
|
|
names.add(h.name);
|
|
return {
|
|
name: String(h.name),
|
|
dockerHost: h.dockerHost ?? null,
|
|
publicHost: String(h.publicHost),
|
|
rconHost: h.rconHost ?? h.publicHost,
|
|
portRangeStart: h.portStart == null ? undefined : Number(h.portStart),
|
|
portRangeEnd: h.portEnd == null ? undefined : Number(h.portEnd),
|
|
volume: h.volume ?? undefined,
|
|
};
|
|
});
|
|
}
|
|
|
|
function str(name, fallback = '') {
|
|
const raw = process.env[name];
|
|
return raw === undefined || raw === '' ? fallback : raw;
|
|
}
|
|
|
|
export function loadConfig(overrides = {}) {
|
|
const config = {
|
|
port: int('PORT', 3000),
|
|
publicUrl: str('PUBLIC_URL', 'http://localhost:5173').replace(/\/$/, ''),
|
|
// Adresse, unter der der CS2-Server dieses Backend für Live-Logs erreicht (Standard: PUBLIC_URL)
|
|
logReceiverUrl: str('LOG_RECEIVER_URL', str('PUBLIC_URL', 'http://localhost:3000')).replace(/\/$/, ''),
|
|
logDebug: bool('LOG_DEBUG', false),
|
|
// Sekunden nach Map-Ende bis zur nächsten Map bzw. zum Abschluss der Serie; -1 = nur manuell
|
|
autoAdvanceSeconds: int('SERIES_AUTO_ADVANCE_SECONDS', 30),
|
|
databaseFile: path.resolve(REPO_ROOT, str('DATABASE_FILE', 'data/cs2-mapveto.db')),
|
|
mapPoolFile: path.resolve(REPO_ROOT, str('MAP_POOL_FILE', 'server/config/map-pools.json')),
|
|
createMatchPassword: str('CREATE_MATCH_PASSWORD'),
|
|
// Optionaler Schlüssel für die Stream-Übersicht aller Matches (/stream?key=…); leer = offen
|
|
streamKey: str('STREAM_KEY'),
|
|
discordWebhookUrl: str('DISCORD_WEBHOOK_URL'),
|
|
// Passwort für den Admin-Modus (/admin). Leer = Admin-Modus ist aus.
|
|
adminPassword: str('ADMIN_PASSWORD'),
|
|
requireFullTeams: bool('REQUIRE_FULL_TEAMS', false),
|
|
trustProxy: bool('TRUST_PROXY', false),
|
|
|
|
serverAdapter: str('SERVER_ADAPTER', 'demo'),
|
|
idleTimeoutMinutes: int('SERVER_IDLE_TIMEOUT_MINUTES', 30),
|
|
maxLifetimeHours: int('SERVER_MAX_LIFETIME_HOURS', 8),
|
|
statusPollSeconds: int('SERVER_STATUS_POLL_SECONDS', 5),
|
|
reaperIntervalSeconds: int('SERVER_REAPER_INTERVAL_SECONDS', 60),
|
|
// Frist pro Veto-Schritt in Sekunden, danach führt das Backend den Schritt automatisch aus. 0 = keine Frist
|
|
vetoStepSeconds: int('VETO_STEP_SECONDS', 30),
|
|
|
|
match: {
|
|
tvDelaySeconds: int('CS2_TV_DELAY', 90),
|
|
tvEnabled: bool('CS2_TV_ENABLE', true),
|
|
maxRoundsCompetitive: int('CS2_MAXROUNDS_COMPETITIVE', 24),
|
|
maxRoundsWingman: int('CS2_MAXROUNDS_WINGMAN', 16),
|
|
overtime: bool('CS2_OVERTIME', true),
|
|
extraCommands: str('CS2_EXTRA_COMMANDS')
|
|
.split(';')
|
|
.map((c) => c.trim())
|
|
.filter(Boolean),
|
|
},
|
|
|
|
demo: {
|
|
startSeconds: int('DEMO_START_SECONDS', 20),
|
|
failFirstConfigure: bool('DEMO_FAIL_FIRST_CONFIGURE', false),
|
|
// Sekunden pro simulierter Runde nach "Aufwärmphase beenden"
|
|
roundSeconds: int('DEMO_ROUND_SECONDS', 4),
|
|
},
|
|
|
|
docker: {
|
|
bin: str('DOCKER_BIN', 'docker'),
|
|
image: str('CS2_DOCKER_IMAGE', 'joedwards32/cs2:latest'),
|
|
host: str('CS2_PUBLIC_HOST'),
|
|
rconHost: str('CS2_RCON_HOST', '127.0.0.1'),
|
|
portRangeStart: int('CS2_PORT_RANGE_START', 27015),
|
|
portRangeEnd: int('CS2_PORT_RANGE_END', 27115),
|
|
gslt: str('CS2_GSLT'),
|
|
volume: str('CS2_DOCKER_VOLUME', 'cs2-mapveto-gamefiles'),
|
|
extraArgs: str('CS2_DOCKER_EXTRA_ARGS'),
|
|
startTimeoutMinutes: int('CS2_START_TIMEOUT_MINUTES', 20),
|
|
// Optional: mehrere Docker-Hosts als JSON-Liste, z. B.
|
|
// [{"name":"host1","dockerHost":"ssh://cs2@192.168.178.111","publicHost":"192.168.178.111","portStart":27100,"portEnd":27139}]
|
|
hosts: parseDockerHosts(str('CS2_DOCKER_HOSTS')),
|
|
},
|
|
|
|
staticPool: {
|
|
// Format: host:port:rconPasswort[:tvPort], mehrere durch Komma getrennt
|
|
servers: str('CS2_STATIC_SERVERS')
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.map(parseStaticServer),
|
|
},
|
|
};
|
|
return deepMerge(config, overrides);
|
|
}
|
|
|
|
function parseStaticServer(entry) {
|
|
const parts = entry.split(':');
|
|
if (parts.length < 3) {
|
|
throw new Error(`CS2_STATIC_SERVERS-Eintrag "${entry}" hat nicht das Format host:port:rconPasswort[:tvPort]`);
|
|
}
|
|
const [host, port, ...rest] = parts;
|
|
let tvPort = null;
|
|
// Das Passwort darf Doppelpunkte enthalten; ein abschließender rein numerischer Teil ist der TV-Port.
|
|
if (rest.length > 1 && /^\d+$/.test(rest[rest.length - 1])) tvPort = Number(rest.pop());
|
|
return { host, port: Number(port), rconPassword: rest.join(':'), tvPort: tvPort ?? Number(port) + 5 };
|
|
}
|
|
|
|
function deepMerge(target, source) {
|
|
for (const [key, value] of Object.entries(source)) {
|
|
if (value && typeof value === 'object' && !Array.isArray(value) && typeof target[key] === 'object') {
|
|
deepMerge(target[key], value);
|
|
} else {
|
|
target[key] = value;
|
|
}
|
|
}
|
|
return target;
|
|
}
|