From e3a1ed4a6383cb85811e6dc1e10e30d77f85dd90 Mon Sep 17 00:00:00 2001 From: MrEgosar Date: Sun, 25 Oct 2026 22:16:57 +0100 Subject: [PATCH] Docker-Warteschlange: Update-Check per RCON statt nur per Logzeile erkennen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Zeile 'Starting CS2 Dedicated Server' rutscht nach dem Serverstart schnell aus den letzten Logzeilen. Der nächste Match-Container wartete dadurch bis zur Zeitgrenze (20 min). Jetzt gilt ein Container als fertig, sobald er per RCON antwortet; die Logzeile bleibt als zweite Erkennung. --- server/src/servers/dockerAdapter.js | 25 ++++++++++++++++++++----- server/test/dockerAdapter.test.js | 25 ++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/server/src/servers/dockerAdapter.js b/server/src/servers/dockerAdapter.js index 50861e3..7cc6d31 100644 --- a/server/src/servers/dockerAdapter.js +++ b/server/src/servers/dockerAdapter.js @@ -62,18 +62,24 @@ export class DockerServerAdapter { release(); throw err; } - this.waitForUpdateCheck(result.handle.container).finally(release); + this.waitForUpdateCheck(result.handle).finally(release); return result; } - /** Wartet, bis der Container SteamCMD abgeschlossen hat, beendet ist oder die Startzeit abläuft. */ - async waitForUpdateCheck(container, { pollMs = 3000 } = {}) { + /** + * Wartet, bis der Container SteamCMD abgeschlossen hat, beendet ist oder die Startzeit abläuft. + * Fertig ist er, sobald der Spielserver per RCON antwortet (RCON gibt es erst nach SteamCMD) oder + * die Startzeile im Log steht. Nur auf das Log zu schauen reicht nicht: Der Server schreibt danach + * so viel, dass die Zeile schnell aus den letzten Zeilen herausfällt. + */ + async waitForUpdateCheck(handle, { 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); + const state = await this.docker(['inspect', '-f', '{{.State.Status}}', handle.container], 15_000); if (state !== 'running' && state !== 'created') return; - const logs = await this.docker(['logs', '--tail', '200', container], 15_000); + if (await this.isReachable(handle)) return; + const logs = await this.docker(['logs', '--tail', '2000', handle.container], 15_000); if (logs.includes(UPDATE_DONE_MARKER)) return; } catch { return; // Container weg – nicht ewig blockieren @@ -82,6 +88,15 @@ export class DockerServerAdapter { } } + async isReachable(handle) { + try { + await rconStatus(this.target(handle)); + return true; + } catch { + return false; + } + } + async launch({ matchId, mode, secrets }) { const port = this.allocatePort(); const tvPort = port + 1; diff --git a/server/test/dockerAdapter.test.js b/server/test/dockerAdapter.test.js index 28e6a9b..330bda4 100644 --- a/server/test/dockerAdapter.test.js +++ b/server/test/dockerAdapter.test.js @@ -28,6 +28,7 @@ const envOf = (args) => Object.fromEntries(args.flatMap((a, i) => (args[i - 1] = test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async () => { const lan = fakeDocker(); const a = new DockerServerAdapter(options, { getUsedPorts: () => new Set(), runDocker: lan.run }); + a.isReachable = async () => false; const r = await a.start({ matchId: 'abc_DEF-1', mode: 'wingman', secrets }); lan.finishUpdate(r.handle.container); const runArgs = lan.calls.find((c) => c[0] === 'run'); @@ -43,6 +44,7 @@ test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async ( const tok = fakeDocker(); const b = new DockerServerAdapter({ ...options, gslt: 'TOKEN1' }, { getUsedPorts: () => new Set(), runDocker: tok.run }); + b.isReachable = async () => false; const r2 = await b.start({ matchId: 'x', mode: 'competitive', secrets }); tok.finishUpdate(r2.handle.container); const env2 = envOf(tok.calls.find((c) => c[0] === 'run')); @@ -56,7 +58,8 @@ test('Zweiter Container startet erst, wenn der erste seinen Update-Check abgesch const a = new DockerServerAdapter(options, { getUsedPorts: () => used, runDocker: d.run }); // Wartezeit zwischen Prüfungen für den Test verkürzen const original = a.waitForUpdateCheck.bind(a); - a.waitForUpdateCheck = (name) => original(name, { pollMs: 10 }); + a.waitForUpdateCheck = (handle) => original(handle, { pollMs: 10 }); + a.isReachable = async () => false; const first = await a.start({ matchId: 'm1', mode: 'competitive', secrets }); used.add(first.port).add(first.tvPort); @@ -73,3 +76,23 @@ test('Zweiter Container startet erst, wenn der erste seinen Update-Check abgesch assert.equal(r2.port, 27102, 'zweiter Container bekommt die nächsten Ports'); d.finishUpdate(r2.handle.container); }); + +test('Warteschlange gibt frei, sobald der Server per RCON antwortet – auch ohne Startzeile im Log', async () => { + const d = fakeDocker(); + const used = new Set(); + const a = new DockerServerAdapter(options, { getUsedPorts: () => used, runDocker: d.run }); + const original = a.waitForUpdateCheck.bind(a); + a.waitForUpdateCheck = (handle) => original(handle, { pollMs: 10 }); + const reachable = new Set(); + a.isReachable = async (handle) => reachable.has(handle.container); + + const first = await a.start({ matchId: 'r1', mode: 'wingman', secrets }); + used.add(first.port).add(first.tvPort); + const second = a.start({ matchId: 'r2', mode: 'wingman', secrets }); + await new Promise((r) => setTimeout(r, 50)); + assert.equal(d.calls.filter((c) => c[0] === 'run').length, 1, 'wartet noch'); + reachable.add(first.handle.container); // Log ohne Startzeile, aber RCON antwortet + const r2 = await second; + assert.equal(d.calls.filter((c) => c[0] === 'run').length, 2); + reachable.add(r2.handle.container); +});