Server im LAN-Reiter des Steam-Serverbrowsers: LAN-Ports, Host-Netz, Teamnamen, CSTV +100
This commit is contained in:
1 parent
af58156209
commit
b5fa27fbd3
14 files changed
+269
-37
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import express from 'express';
|
||||
import { formatPortList } from './config.js';
|
||||
import { HttpError } from './matchService.js';
|
||||
import { createRateLimiter, randomToken, safeEqual } from './security.js';
|
||||
|
||||
@@ -96,6 +97,7 @@ export function createAdminRouter({ service, manager, config, settings }) {
|
||||
portStart: h.portRangeStart,
|
||||
portEnd: h.portRangeEnd,
|
||||
volume: h.volume,
|
||||
lanPorts: formatPortList(h.lanPorts),
|
||||
})),
|
||||
overridden: settings.hostsOverridden(),
|
||||
editable: !!manager.adapter.setHosts,
|
||||
|
||||
@@ -22,6 +22,34 @@ function bool(name, fallback) {
|
||||
return ['1', 'true', 'yes', 'ja', 'on'].includes(raw.toLowerCase());
|
||||
}
|
||||
|
||||
/** Portliste aus Text wie "27015-27020" oder "27016,27018-27019". Leer = keine Ports. */
|
||||
export function parsePortList(value, label = 'LAN-Ports') {
|
||||
if (Array.isArray(value)) value = value.join(',');
|
||||
const ports = [];
|
||||
for (const part of String(value ?? '').split(',').map((p) => p.trim()).filter(Boolean)) {
|
||||
const m = /^(\d+)(?:\s*-\s*(\d+))?$/.exec(part);
|
||||
if (!m) throw new Error(`${label}: "${part}" ist kein Port oder Bereich`);
|
||||
const from = Number(m[1]);
|
||||
const to = Number(m[2] ?? m[1]);
|
||||
if (from < 1024 || to > 65535 || to < from || to - from > 100) throw new Error(`${label}: Bereich ${part} ist ungültig`);
|
||||
for (let p = from; p <= to; p += 1) if (!ports.includes(p)) ports.push(p);
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
/** Portliste kompakt als Text, z. B. [27015, 27016, 27017] -> "27015-27017". */
|
||||
export function formatPortList(ports = []) {
|
||||
const sorted = [...ports].sort((a, b) => a - b);
|
||||
const parts = [];
|
||||
for (let i = 0; i < sorted.length; i += 1) {
|
||||
let j = i;
|
||||
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1) j += 1;
|
||||
parts.push(i === j ? String(sorted[i]) : `${sorted[i]}-${sorted[j]}`);
|
||||
i = j;
|
||||
}
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
export function parseDockerHosts(raw) {
|
||||
if (!raw) return null;
|
||||
let list;
|
||||
@@ -44,6 +72,7 @@ export function parseDockerHosts(raw) {
|
||||
portRangeStart: h.portStart == null ? undefined : Number(h.portStart),
|
||||
portRangeEnd: h.portEnd == null ? undefined : Number(h.portEnd),
|
||||
volume: h.volume ?? undefined,
|
||||
lanPorts: h.lanPorts === undefined ? undefined : parsePortList(h.lanPorts, `LAN-Ports von ${h.name}`),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -111,6 +140,13 @@ export function loadConfig(overrides = {}) {
|
||||
volume: str('CS2_DOCKER_VOLUME', 'cs2-mapveto-gamefiles'),
|
||||
extraArgs: str('CS2_DOCKER_EXTRA_ARGS'),
|
||||
startTimeoutMinutes: int('CS2_START_TIMEOUT_MINUTES', 20),
|
||||
// Spielports, die der Steam-Serverbrowser im LAN-Reiter absucht (Steam fragt nur 27015–27020 ab).
|
||||
// Diese Ports werden zuerst vergeben, CSTV kommt dann aus dem normalen Portbereich. Leer = aus.
|
||||
lanPorts: parsePortList(str('CS2_LAN_PORTS', '27015-27020'), 'CS2_LAN_PORTS'),
|
||||
// Abstand des CSTV-Ports zum Spielport, z. B. Spiel 27015 -> CSTV 27115
|
||||
tvPortOffset: int('CS2_TV_PORT_OFFSET', 100),
|
||||
// Anfang des Servernamens im Serverbrowser, danach folgen die Teamnamen
|
||||
serverNamePrefix: str('CS2_SERVERNAME_PREFIX', 'MaxLAN'),
|
||||
// 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')),
|
||||
|
||||
@@ -160,6 +160,12 @@ export class ServerSettings {
|
||||
throw new HttpError(400, `Ungültige Adresse für ${h.name}`, 'invalid');
|
||||
}
|
||||
if (h.volume && !/^[\w.-]+$/.test(h.volume)) throw new HttpError(400, `Ungültiger Volume-Name für ${h.name}`, 'invalid');
|
||||
const lan = h.lanPorts ?? this.config.docker.lanPorts ?? [];
|
||||
const rangeStart = h.portRangeStart ?? this.config.docker.portRangeStart;
|
||||
const rangeEnd = h.portRangeEnd ?? this.config.docker.portRangeEnd;
|
||||
if (lan.some((p) => p >= rangeStart && p <= rangeEnd)) {
|
||||
throw new HttpError(400, `LAN-Ports von ${h.name} dürfen nicht im normalen Portbereich liegen`, 'invalid');
|
||||
}
|
||||
const start = h.portRangeStart ?? this.config.docker.portRangeStart;
|
||||
const end = h.portRangeEnd ?? this.config.docker.portRangeEnd;
|
||||
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1024 || end > 65535 || end - start < 1) {
|
||||
|
||||
@@ -2,8 +2,39 @@ import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { configureViaRcon, listRoundBackups, loadRoundBackup, rconStatus } from './rconConfigure.js';
|
||||
import { runRcon } from './rcon.js';
|
||||
import { sanitizeConsoleText } from './cs2Config.js';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Plant Spiel- und CSTV-Port für einen neuen Server. Der CSTV-Port liegt immer `tvPortOffset` (Standard 100)
|
||||
* über dem Spielport. Spielports kommen zuerst aus den LAN-Ports (damit der Server im LAN-Reiter des
|
||||
* Steam-Serverbrowsers auftaucht), danach aus dem normalen Bereich. null = kein Platz.
|
||||
*/
|
||||
export function planPorts(host, used) {
|
||||
const offset = host.tvPortOffset ?? 100;
|
||||
const free = (p) => !used.has(p) && p <= 65535;
|
||||
const candidates = [...(host.lanPorts ?? [])];
|
||||
for (let p = host.portRangeStart; p <= host.portRangeEnd; p += 1) candidates.push(p);
|
||||
const port = candidates.find((p) => free(p) && free(p + offset));
|
||||
return port === undefined ? null : { port, tvPort: port + offset };
|
||||
}
|
||||
|
||||
/** Wie viele Server passen noch auf einen Host, wenn bereits `used` belegt ist. */
|
||||
export function countSlots(host, used) {
|
||||
const taken = new Set(used);
|
||||
let count = 0;
|
||||
for (;;) {
|
||||
const plan = planPorts(host, taken);
|
||||
if (!plan) return count;
|
||||
taken.add(plan.port);
|
||||
taken.add(plan.tvPort);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fehler von "docker run", wenn ein Port auf dem Host schon von etwas anderem belegt ist. */
|
||||
const PORT_BUSY = /port is already allocated|address already in use/i;
|
||||
const DISK_CACHE_MS = 60_000;
|
||||
|
||||
/** Liest Gesamt- und freien Speicher aus einer Zeile von `df -B1` (Spalten: Filesystem, 1B-blocks, Used, Available …). */
|
||||
@@ -52,12 +83,19 @@ export class DockerServerAdapter {
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
allocatePort() {
|
||||
const used = this.getUsedPorts();
|
||||
for (let p = this.options.portRangeStart; p + 1 <= this.options.portRangeEnd; p += 2) {
|
||||
if (!used.has(p) && !used.has(p + 1)) return p;
|
||||
}
|
||||
throw new Error('Kein freier Port im Bereich CS2_PORT_RANGE_START–CS2_PORT_RANGE_END');
|
||||
allocatePorts(blocked = new Set()) {
|
||||
const used = new Set([...this.getUsedPorts(), ...blocked]);
|
||||
const plan = planPorts(this.options, used);
|
||||
if (!plan) throw new Error('Kein freier Port im Bereich CS2_PORT_RANGE_START–CS2_PORT_RANGE_END');
|
||||
return plan;
|
||||
}
|
||||
|
||||
/** Servername im Steam-Serverbrowser, z. B. "MaxLAN | Team A vs. Team B". */
|
||||
serverName(matchId, teamNames) {
|
||||
const prefix = this.options.serverNamePrefix ? sanitizeConsoleText(this.options.serverNamePrefix, 24) : '';
|
||||
if (!teamNames) return `${prefix} ${matchId}`.trim();
|
||||
const title = `${sanitizeConsoleText(teamNames.A)} vs. ${sanitizeConsoleText(teamNames.B)}`;
|
||||
return (prefix ? `${prefix} | ${title}` : title).slice(0, 63);
|
||||
}
|
||||
|
||||
async start(args) {
|
||||
@@ -108,13 +146,34 @@ export class DockerServerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async launch({ matchId, mode, secrets }) {
|
||||
const port = this.allocatePort();
|
||||
const tvPort = port + 1;
|
||||
/**
|
||||
* Im Host-Netz meldet Docker keinen Portkonflikt, CS2 startet dann einfach nicht. Deshalb vorher per SSH
|
||||
* nachsehen, ob ein fremder Dienst den Port belegt. Ohne SSH-Ziel (oder in Tests) gilt der Port als frei.
|
||||
*/
|
||||
async hostPortsFree(ports) {
|
||||
if (this.runDocker || !this.options.dockerHost?.startsWith('ssh://')) return true;
|
||||
try {
|
||||
const filter = ports.map((p) => `sport = :${Number(p)}`).join(' or ');
|
||||
const { stdout } = await exec('ssh', [
|
||||
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', this.options.dockerHost.slice('ssh://'.length),
|
||||
`ss -Hlnut '( ${filter} )'`,
|
||||
], { timeout: 10_000 });
|
||||
return stdout.trim() === '';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async launch({ matchId, mode, secrets, teamNames }, blocked = new Set()) {
|
||||
const { port, tvPort } = this.allocatePorts(blocked);
|
||||
const lan = (this.options.lanPorts ?? []).includes(port);
|
||||
if (lan && blocked.size < 10 && !(await this.hostPortsFree([port, tvPort]))) {
|
||||
return this.launch({ matchId, mode, secrets, teamNames }, new Set([...blocked, port, tvPort]));
|
||||
}
|
||||
const container = `cs2mv-${matchId.replace(/[^a-zA-Z0-9_-]/g, '')}`.toLowerCase();
|
||||
const env = {
|
||||
...(this.lanMode ? { CS2_LAN: '1' } : { SRCDS_TOKEN: this.options.gslt, CS2_LAN: '0' }),
|
||||
CS2_SERVERNAME: `CS2 Mapveto ${matchId}`,
|
||||
CS2_SERVERNAME: this.serverName(matchId, teamNames),
|
||||
CS2_PORT: String(port),
|
||||
CS2_RCONPW: secrets.rconPassword,
|
||||
CS2_PW: secrets.serverPassword,
|
||||
@@ -133,19 +192,32 @@ export class DockerServerAdapter {
|
||||
};
|
||||
const args = ['run', '-d', '--name', container, '--label', 'cs2-mapveto=1'];
|
||||
for (const [k, v] of Object.entries(env)) args.push('-e', `${k}=${v}`);
|
||||
args.push(
|
||||
'-p', `${port}:${port}/tcp`,
|
||||
'-p', `${port}:${port}/udp`,
|
||||
'-p', `${tvPort}:${tvPort}/udp`,
|
||||
'-v', `${this.options.volume}:/home/steam/cs2-dedicated`,
|
||||
);
|
||||
if (lan) {
|
||||
// Der LAN-Reiter von Steam sucht per Broadcast. Die Port-Weiterleitung von Docker reicht Broadcasts
|
||||
// nicht an den Container weiter, deshalb laufen Server auf LAN-Ports direkt im Netz des Hosts.
|
||||
args.push('--network', 'host');
|
||||
} else {
|
||||
args.push(
|
||||
'-p', `${port}:${port}/tcp`,
|
||||
'-p', `${port}:${port}/udp`,
|
||||
'-p', `${tvPort}:${tvPort}/udp`,
|
||||
);
|
||||
}
|
||||
args.push('-v', `${this.options.volume}:/home/steam/cs2-dedicated`);
|
||||
if (this.options.extraArgs) args.push(...this.options.extraArgs.split(' ').filter(Boolean));
|
||||
args.push(this.options.image);
|
||||
|
||||
// Ein evtl. übrig gebliebener Container gleichen Namens (z. B. nach Retry) wird ersetzt.
|
||||
await this.docker(['rm', '-f', container]).catch(() => {});
|
||||
// Das erste "docker run" kann das Image laden und dauert dann länger.
|
||||
await this.docker(args, 15 * 60_000);
|
||||
try {
|
||||
await this.docker(args, 15 * 60_000);
|
||||
} catch (err) {
|
||||
// Port von einem fremden Dienst belegt (die App kennt nur ihre eigenen Server): mit anderen Ports erneut versuchen
|
||||
if (!PORT_BUSY.test(`${err.message} ${err.stderr ?? ''}`) || blocked.size >= 10) throw err;
|
||||
await this.docker(['rm', '-f', container]).catch(() => {});
|
||||
return this.launch({ matchId, mode, secrets, teamNames }, new Set([...blocked, port, tvPort]));
|
||||
}
|
||||
|
||||
return {
|
||||
handle: {
|
||||
@@ -250,6 +322,8 @@ export class DockerHostPool {
|
||||
portRangeStart: h.portRangeStart ?? options.portRangeStart,
|
||||
portRangeEnd: h.portRangeEnd ?? options.portRangeEnd,
|
||||
volume: h.volume ?? options.volume,
|
||||
lanPorts: h.lanPorts ?? options.lanPorts ?? [],
|
||||
tvPortOffset: options.tvPortOffset ?? 100,
|
||||
};
|
||||
const merged = { ...options, ...host, host: host.publicHost };
|
||||
// Die Startzeit kann der Admin zur Laufzeit ändern, deshalb immer aus den gemeinsamen Einstellungen lesen
|
||||
@@ -340,20 +414,15 @@ export class DockerHostPool {
|
||||
return this.diskInflight.get(name);
|
||||
}
|
||||
|
||||
/** Freie Port-Paare auf einem Host. */
|
||||
/** Wie viele Server noch auf einen Host passen. */
|
||||
freeSlots(host) {
|
||||
const used = this.getUsedPorts(host.publicHost);
|
||||
let free = 0;
|
||||
for (let p = host.portRangeStart; p + 1 <= host.portRangeEnd; p += 2) {
|
||||
if (!used.has(p) && !used.has(p + 1)) free += 1;
|
||||
}
|
||||
return free;
|
||||
return countSlots(host, this.getUsedPorts(host.publicHost));
|
||||
}
|
||||
|
||||
/** Auslastung pro Host: Anzahl Port-Paare insgesamt und belegt. */
|
||||
/** Auslastung pro Host: Anzahl Serverplätze insgesamt und belegt. */
|
||||
hostStats() {
|
||||
return this.hosts.map((h) => {
|
||||
const total = Math.floor((h.portRangeEnd - h.portRangeStart + 1) / 2);
|
||||
const total = countSlots(h, new Set());
|
||||
return { name: h.name, publicHost: h.publicHost, total, used: total - this.freeSlots(h) };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -314,7 +314,12 @@ export class ServerManager {
|
||||
|
||||
const match = this.service.getMatch(matchId);
|
||||
try {
|
||||
const result = await this.adapter.start({ matchId, mode: match.mode, secrets });
|
||||
const result = await this.adapter.start({
|
||||
matchId,
|
||||
mode: match.mode,
|
||||
secrets,
|
||||
teamNames: { A: match.team_a_name, B: match.team_b_name },
|
||||
});
|
||||
this.update(matchId, {
|
||||
handle: JSON.stringify(result.handle),
|
||||
host: result.host,
|
||||
|
||||
@@ -39,7 +39,7 @@ test('Ohne GSLT startet der Container im LAN-Modus, mit GSLT mit Token', async (
|
||||
assert.equal(env.CS2_MAXPLAYERS, '6', '4 Spieler + CSTV + Reserve');
|
||||
assert.equal(env.CS2_ADDITIONAL_ARGS, '-allow_round_backup_files', 'Rundensicherung für Rollback');
|
||||
assert.equal(env.CS2_PORT, '27100');
|
||||
assert.equal(env.TV_PORT, '27101');
|
||||
assert.equal(env.TV_PORT, '27200');
|
||||
assert.ok(runArgs.includes('cs2-mapveto-gamefiles:/home/steam/cs2-dedicated'));
|
||||
assert.equal(r.host, '192.168.178.111');
|
||||
|
||||
@@ -74,7 +74,7 @@ test('Zweiter Container startet erst, wenn der erste seinen Update-Check abgesch
|
||||
d.finishUpdate(first.handle.container);
|
||||
const r2 = await second;
|
||||
assert.equal(d.calls.filter((c) => c[0] === 'run').length, 2);
|
||||
assert.equal(r2.port, 27102, 'zweiter Container bekommt die nächsten Ports');
|
||||
assert.equal(r2.port, 27101, 'zweiter Container bekommt den nächsten Port');
|
||||
d.finishUpdate(r2.handle.container);
|
||||
});
|
||||
|
||||
|
||||
@@ -81,11 +81,11 @@ test('Alte Handles ohne Host-Namen laufen auf dem ersten Host', () => {
|
||||
assert.equal(p.adapterFor({ container: 'x' }), p.adapters.get('a'));
|
||||
});
|
||||
|
||||
test('Auslastung pro Host: Port-Paare insgesamt und belegt', () => {
|
||||
test('Auslastung pro Host: Serverplätze insgesamt und belegt', () => {
|
||||
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': [27100] });
|
||||
assert.deepEqual(p.hostStats(), [
|
||||
{ name: 'a', publicHost: '10.0.0.1', total: 2, used: 1 },
|
||||
{ name: 'b', publicHost: '10.0.0.2', total: 20, used: 0 },
|
||||
{ name: 'a', publicHost: '10.0.0.1', total: 4, used: 1 },
|
||||
{ name: 'b', publicHost: '10.0.0.2', total: 40, used: 0 },
|
||||
]);
|
||||
});
|
||||
test('df-Ausgabe wird in Gesamt- und freien Speicher zerlegt', async () => {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { formatPortList, parsePortList } from '../src/config.js';
|
||||
import { countSlots, DockerHostPool, planPorts } from '../src/servers/dockerAdapter.js';
|
||||
|
||||
const host = { portRangeStart: 27100, portRangeEnd: 27105, lanPorts: [27015, 27016] };
|
||||
const secrets = { rconPassword: 'Rcon123', serverPassword: 'Pw123', tvPassword: 'Tv123' };
|
||||
|
||||
test('Portlisten werden gelesen und kompakt geschrieben', () => {
|
||||
assert.deepEqual(parsePortList('27015-27017, 27020'), [27015, 27016, 27017, 27020]);
|
||||
assert.deepEqual(parsePortList(''), []);
|
||||
assert.throws(() => parsePortList('abc'), /kein Port/);
|
||||
assert.throws(() => parsePortList('80-90'), /ungültig/);
|
||||
assert.equal(formatPortList([27020, 27015, 27016, 27017]), '27015-27017,27020');
|
||||
});
|
||||
|
||||
test('Spielport kommt zuerst aus den LAN-Ports, CSTV liegt immer 100 darüber', () => {
|
||||
assert.deepEqual(planPorts(host, new Set()), { port: 27015, tvPort: 27115 });
|
||||
assert.deepEqual(planPorts(host, new Set([27015, 27115])), { port: 27016, tvPort: 27116 });
|
||||
// LAN-Ports voll: weiter im normalen Bereich, CSTV ebenfalls +100
|
||||
assert.deepEqual(planPorts(host, new Set([27015, 27016, 27115, 27116])), { port: 27100, tvPort: 27200 });
|
||||
// Spielport frei, aber sein CSTV-Port belegt: nächster Spielport
|
||||
assert.deepEqual(planPorts({ ...host, lanPorts: [] }, new Set([27200])), { port: 27101, tvPort: 27201 });
|
||||
assert.equal(planPorts({ ...host, lanPorts: [] }, new Set([27100, 27101, 27102, 27103, 27104, 27105])), null);
|
||||
});
|
||||
|
||||
test('Abstand zwischen Spiel- und CSTV-Port ist einstellbar', () => {
|
||||
assert.deepEqual(planPorts({ ...host, tvPortOffset: 5 }, new Set()), { port: 27015, tvPort: 27020 });
|
||||
});
|
||||
|
||||
test('Plätze pro Host: 2 LAN-Ports plus 6 Spielports im normalen Bereich', () => {
|
||||
assert.equal(countSlots(host, new Set()), 8);
|
||||
assert.equal(countSlots({ ...host, lanPorts: [] }, new Set()), 6);
|
||||
});
|
||||
|
||||
function fakePool({ busy = [] } = {}) {
|
||||
const calls = [];
|
||||
const runDocker = async (args) => {
|
||||
calls.push(args);
|
||||
if (args[0] === 'run') {
|
||||
const ports = args.filter((a, i) => args[i - 1] === '-p').map((a) => Number(a.split(':')[0]));
|
||||
const portEnv = args.find((a) => a.startsWith('CS2_PORT='));
|
||||
if (args.includes('--network') && portEnv) ports.push(Number(portEnv.split('=')[1]));
|
||||
if (ports.some((p) => busy.includes(p))) {
|
||||
const err = new Error('Command failed');
|
||||
err.stderr = 'Bind for 0.0.0.0:27015 failed: port is already allocated';
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (args[0] === 'inspect') return 'running';
|
||||
return '';
|
||||
};
|
||||
const p = new DockerHostPool({
|
||||
bin: 'docker', image: 'joedwards32/cs2:latest', host: '10.0.0.9', rconHost: '10.0.0.9',
|
||||
portRangeStart: 27100, portRangeEnd: 27139, gslt: '', volume: 'v', extraArgs: '',
|
||||
startTimeoutMinutes: 1, lanPorts: [27015, 27016], serverNamePrefix: 'MaxLAN',
|
||||
hosts: [{ name: 'a', publicHost: '10.0.0.1' }],
|
||||
}, { runDocker });
|
||||
for (const a of p.adapters.values()) a.waitForUpdateCheck = async () => {};
|
||||
return { pool: p, calls };
|
||||
}
|
||||
|
||||
test('Servername zeigt die Teams, Spielport ist ein LAN-Port', async () => {
|
||||
const { pool: p, calls } = fakePool();
|
||||
const r = await p.start({ matchId: 'm1', mode: 'wingman', secrets, teamNames: { A: 'Rote "Füchse"', B: 'Blau' } });
|
||||
assert.equal(r.port, 27015);
|
||||
assert.equal(r.tvPort, 27115);
|
||||
const run = calls.find((c) => c[0] === 'run');
|
||||
assert.ok(run.includes('CS2_SERVERNAME=MaxLAN | Rote Füchse vs. Blau'));
|
||||
// Auf LAN-Ports im Host-Netz, damit Broadcasts des Serverbrowsers ankommen; keine Port-Weiterleitung
|
||||
assert.equal(run[run.indexOf('--network') + 1], 'host');
|
||||
assert.ok(!run.includes('-p'));
|
||||
});
|
||||
|
||||
test('Server außerhalb der LAN-Ports laufen weiter mit Port-Weiterleitung', async () => {
|
||||
const { pool: p, calls } = fakePool();
|
||||
await p.start({ matchId: 'x1', mode: 'wingman', secrets, teamNames: { A: 'A', B: 'B' } });
|
||||
await p.start({ matchId: 'x2', mode: 'wingman', secrets, teamNames: { A: 'A', B: 'B' } });
|
||||
calls.length = 0;
|
||||
// Die App kennt ihre Ports aus der Datenbank; hier sind beide LAN-Ports belegt
|
||||
const adapter = p.adapters.get('a');
|
||||
adapter.getUsedPorts = () => new Set([27015, 27016, 27115, 27116]);
|
||||
await adapter.launch({ matchId: 'x3', mode: 'wingman', secrets });
|
||||
const run = calls.find((c) => c[0] === 'run');
|
||||
assert.ok(!run.includes('--network'));
|
||||
assert.ok(run.includes('27100:27100/udp'));
|
||||
assert.ok(run.includes('27200:27200/udp'));
|
||||
});
|
||||
|
||||
test('Ist ein LAN-Port von einem fremden Dienst belegt, nimmt der Start den nächsten', async () => {
|
||||
const { pool: p, calls } = fakePool({ busy: [27015] });
|
||||
const r = await p.start({ matchId: 'm2', mode: 'competitive', secrets, teamNames: { A: 'A', B: 'B' } });
|
||||
assert.equal(r.port, 27016);
|
||||
assert.equal(calls.filter((c) => c[0] === 'run').length, 2);
|
||||
});
|
||||
@@ -52,9 +52,9 @@ test('Docker-Hosts: Prüfung der Liste und Übernahme in den Pool, Startzeit wir
|
||||
{ name: 'a', publicHost: '10.0.0.1', dockerHost: 'ssh://server@10.0.0.1', portStart: 27100, portEnd: 27139 },
|
||||
{ name: 'b', publicHost: '10.0.0.2', dockerHost: 'ssh://b', portStart: 27100, portEnd: 27119 },
|
||||
]);
|
||||
const pool = new DockerHostPool({ ...config.docker, startTimeoutMinutes: 20, hosts: null });
|
||||
const pool = new DockerHostPool({ ...config.docker, startTimeoutMinutes: 20, lanPorts: [], hosts: null });
|
||||
pool.setHosts(hosts);
|
||||
assert.deepEqual(pool.hostStats().map((h) => [h.name, h.total]), [['a', 20], ['b', 10]]);
|
||||
assert.deepEqual(pool.hostStats().map((h) => [h.name, h.total]), [['a', 40], ['b', 20]]);
|
||||
|
||||
pool.options.startTimeoutMinutes = 45;
|
||||
assert.equal(pool.adapters.get('b').options.startTimeoutMinutes, 45);
|
||||
|
||||
Reference in new issue
Block a user