Docker-Adapter: LAN-Modus ohne GSLT, Container starten nacheinander
Ohne CS2_GSLT starten Match-Container mit CS2_LAN=1. Neue Container warten, bis der vorige seinen SteamCMD-Update-Check abgeschlossen hat, damit nie zwei gleichzeitig das gemeinsame Volume aktualisieren. Anleitung: Docker per SSH (DOCKER_HOST), eindeutige Passwort-Beispiele mit Warnung vor Sonderzeichen (Startskript nutzt eval).
This commit is contained in:
1 parent
85041b2bff
commit
3a109883b3
5 files changed
+178
-22
No files matched your search
@@ -4,26 +4,39 @@ import { configureViaRcon, rconStatus } from './rconConfigure.js';
|
||||
import { runRcon } from './rcon.js';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Diese Zeile schreibt das Image, sobald SteamCMD fertig ist und der Spielserver startet.
|
||||
const UPDATE_DONE_MARKER = 'Starting CS2 Dedicated Server';
|
||||
|
||||
/**
|
||||
* Startet pro Match einen eigenen CS2-Container (Standard-Image: joedwards32/cs2, Nachfolger von cm2network/cs2).
|
||||
* Benötigt Docker auf dem Backend-Host, ein Game Server Login Token (CS2_GSLT)
|
||||
* und eine öffentlich erreichbare Adresse (CS2_PUBLIC_HOST).
|
||||
* Benötigt Docker (lokal oder per DOCKER_HOST, z. B. ssh://…) und die Adresse, unter der Spieler
|
||||
* den Docker-Host erreichen (CS2_PUBLIC_HOST).
|
||||
*
|
||||
* Ohne CS2_GSLT laufen die Server im LAN-Modus (sv_lan 1): nur aus dem lokalen Netz erreichbar.
|
||||
*
|
||||
* Pro Match werden zwei aufeinanderfolgende Ports belegt: Spielport (TCP+UDP, auch RCON)
|
||||
* und CSTV-Port (UDP).
|
||||
*
|
||||
* Alle Container teilen sich das Volume mit den Spieldateien. Jeder prüft beim Start per SteamCMD
|
||||
* auf Updates; damit nie zwei gleichzeitig schreiben, starten Container nacheinander: der nächste
|
||||
* erst, wenn der vorige seinen Update-Check abgeschlossen hat.
|
||||
*/
|
||||
export class DockerServerAdapter {
|
||||
constructor(options, { getUsedPorts }) {
|
||||
constructor(options, { getUsedPorts, runDocker } = {}) {
|
||||
this.name = 'docker';
|
||||
this.simulated = false;
|
||||
this.options = options;
|
||||
this.getUsedPorts = getUsedPorts;
|
||||
this.runDocker = runDocker; // nur für Tests
|
||||
this.lanMode = !options.gslt;
|
||||
this.startQueue = Promise.resolve();
|
||||
if (!options.host) throw new Error('SERVER_ADAPTER=docker benötigt CS2_PUBLIC_HOST');
|
||||
if (!options.gslt) throw new Error('SERVER_ADAPTER=docker benötigt CS2_GSLT (Game Server Login Token)');
|
||||
}
|
||||
|
||||
async docker(args, timeout = 60_000) {
|
||||
if (this.runDocker) return this.runDocker(args);
|
||||
const { stdout } = await exec(this.options.bin, args, { timeout, maxBuffer: 4 * 1024 * 1024 });
|
||||
return stdout.trim();
|
||||
}
|
||||
@@ -36,12 +49,45 @@ export class DockerServerAdapter {
|
||||
throw new Error('Kein freier Port im Bereich CS2_PORT_RANGE_START–CS2_PORT_RANGE_END');
|
||||
}
|
||||
|
||||
async start({ matchId, mode, secrets }) {
|
||||
async start(args) {
|
||||
// In die Warteschlange: erst starten, wenn der vorige Container seinen Update-Check hinter sich hat.
|
||||
const previous = this.startQueue;
|
||||
let release;
|
||||
this.startQueue = new Promise((resolve) => { release = resolve; });
|
||||
await previous;
|
||||
let result;
|
||||
try {
|
||||
result = await this.launch(args);
|
||||
} catch (err) {
|
||||
release();
|
||||
throw err;
|
||||
}
|
||||
this.waitForUpdateCheck(result.handle.container).finally(release);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Wartet, bis der Container SteamCMD abgeschlossen hat, beendet ist oder die Startzeit abläuft. */
|
||||
async waitForUpdateCheck(container, { pollMs = 3000 } = {}) {
|
||||
const deadline = Date.now() + this.options.startTimeoutMinutes * 60_000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const state = await this.docker(['inspect', '-f', '{{.State.Status}}', container], 15_000);
|
||||
if (state !== 'running' && state !== 'created') return;
|
||||
const logs = await this.docker(['logs', '--tail', '200', container], 15_000);
|
||||
if (logs.includes(UPDATE_DONE_MARKER)) return;
|
||||
} catch {
|
||||
return; // Container weg – nicht ewig blockieren
|
||||
}
|
||||
await sleep(pollMs);
|
||||
}
|
||||
}
|
||||
|
||||
async launch({ matchId, mode, secrets }) {
|
||||
const port = this.allocatePort();
|
||||
const tvPort = port + 1;
|
||||
const container = `cs2mv-${matchId.replace(/[^a-zA-Z0-9_-]/g, '')}`.toLowerCase();
|
||||
const env = {
|
||||
SRCDS_TOKEN: this.options.gslt,
|
||||
...(this.lanMode ? { CS2_LAN: '1' } : { SRCDS_TOKEN: this.options.gslt, CS2_LAN: '0' }),
|
||||
CS2_SERVERNAME: `CS2 Mapveto ${matchId}`,
|
||||
CS2_PORT: String(port),
|
||||
CS2_RCONPW: secrets.rconPassword,
|
||||
@@ -104,7 +150,7 @@ export class DockerServerAdapter {
|
||||
if (minutes >= this.options.startTimeoutMinutes) {
|
||||
return { state: 'failed', message: `Server nach ${minutes} Minuten nicht per RCON erreichbar` };
|
||||
}
|
||||
return { state: 'starting', message: 'Container läuft, Spielserver lädt (Download/Update möglich)' };
|
||||
return { state: 'starting', message: 'Container läuft, Spielserver lädt (Update-Prüfung oder Start)' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user