commit c19fde4126706b75466cfb514d0a2756a7438ef2 Author: Justin Bosker Date: Mon Jul 20 16:44:23 2026 +0200 Initial project structure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fde66fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +build/ +.env +*.log + +.vscode/ \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..22b23b9 --- /dev/null +++ b/README @@ -0,0 +1,12 @@ +# Server Manager + +Projekt zur Verwaltung von Dedicated Game Servern. + +## Komponenten + +- Manager +- Wrapper + +## Architektur + +Manager steuert Wrapper über RabbitMQ. \ No newline at end of file diff --git a/manager/package-lock.json b/manager/package-lock.json new file mode 100644 index 0000000..3de983e --- /dev/null +++ b/manager/package-lock.json @@ -0,0 +1,237 @@ +{ + "name": "manager", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "manager", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "amqplib": "^2.0.1" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "@types/node": "^26.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/amqplib": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/amqplib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-2.0.1.tgz", + "integrity": "sha512-a3P2MgfCf9nzVis12VxWEn0dS6hcqve7dlEAhXDtIWR27BlhtMkILOc+H9aeHjDi6i6r94dYKc2Kx2OFe3avvg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/manager/package.json b/manager/package.json new file mode 100644 index 0000000..c9afe63 --- /dev/null +++ b/manager/package.json @@ -0,0 +1,21 @@ +{ + "name": "manager", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "ts-node src/index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "amqplib": "^2.0.1" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "@types/node": "^26.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } +} diff --git a/manager/src/command.ts b/manager/src/command.ts new file mode 100644 index 0000000..d3cf39a --- /dev/null +++ b/manager/src/command.ts @@ -0,0 +1,37 @@ +import * as amqp from "amqplib"; + + +export class CommandService { + + + constructor( + private channel: amqp.Channel + ){} + + + + async send( + queue:string, + command:any + ){ + + + await this.channel.sendToQueue( + + queue, + + Buffer.from( + JSON.stringify(command) + ) + + ); + + + console.log( + "Command gesendet:", + command + ); + + } + +} \ No newline at end of file diff --git a/manager/src/index.ts b/manager/src/index.ts new file mode 100644 index 0000000..18ed8a5 --- /dev/null +++ b/manager/src/index.ts @@ -0,0 +1,105 @@ +import { Rabbit } from "./rabbit"; +import { WrapperManager } from "./wrapperManager"; +import { WrapperRegistry } from "./wrapperRegistry"; +import { ServerRegistry } from "./serverRegistry"; +import { ServerManager } from "./serverManager"; +import { PortManager } from "./portManager"; +import { ServerStatusManager } from "./serverStatusManager"; +import { ServerLifecycle } from "./serverLifecycle"; + + +const wrapperRegistry = + new WrapperRegistry(); + + + +const serverRegistry = + new ServerRegistry(); + +const portManager = + new PortManager(); + + + + + + + +async function main(){ + + + const rabbit = + new Rabbit(); + + + + await rabbit.connect(); + + + + const wrapperManager = + new WrapperManager( + + rabbit, + + wrapperRegistry, + + serverRegistry + + ); + + + + await wrapperManager.start(); + + + const serverManager = + new ServerManager( + + rabbit, + + wrapperRegistry, + + serverRegistry, + + portManager + + ); + + const lifecycle = + new ServerLifecycle( + + serverRegistry, + + serverManager + + ); + + + lifecycle.start(); + + const statusManager = + new ServerStatusManager( + + rabbit, + + serverRegistry + + ); + + + await statusManager.start(); + + + + + console.log( + "Manager gestartet" + ); + + +} + + + +main(); \ No newline at end of file diff --git a/manager/src/nameGenerator.ts b/manager/src/nameGenerator.ts new file mode 100644 index 0000000..e082514 --- /dev/null +++ b/manager/src/nameGenerator.ts @@ -0,0 +1,70 @@ +const locations = [ + + "Crimson", + "Azure", + "Emerald", + "Amber", + "Sapphire", + "Ruby", + "Obsidian", + "Pearl", + "Onyx", + "Ivory", + "Copper", + "Platinum", + "Titanium", + "Neon", + "Shadow", + "Frost", + "Solar", + "Void", + "Storm", + "Ocean" + +]; + + + +export function generateServerName( + existingNames:string[] +){ + + + for(const location of locations){ + + + const name = + "[EU] druecktieR | " + location; + + + + if(!existingNames.includes(name)){ + + + return name; + + + } + + + } + + + + /* + Falls alle Namen vergeben sind + */ + + + return ( + + "[EU] druecktieR | " + + + Math.random() + .toString(36) + .substring(2,6) + + ); + + +} diff --git a/manager/src/portManager.ts b/manager/src/portManager.ts new file mode 100644 index 0000000..2a7bae0 --- /dev/null +++ b/manager/src/portManager.ts @@ -0,0 +1,118 @@ +export class PortManager { + + + + private usedPorts = + new Set(); + + + + private usedQueryPorts = + new Set(); + + + + + + private gamePortStart = + 7777; + + + private queryPortStart = + 27015; + + + + + + allocate(){ + + + + let port = + this.gamePortStart; + + + + while( + this.usedPorts.has(port) + ){ + + port++; + + } + + + + + let queryPort = + this.queryPortStart; + + + + while( + this.usedQueryPorts.has(queryPort) + ){ + + queryPort++; + + } + + + + + + this.usedPorts.add( + port + ); + + + this.usedQueryPorts.add( + queryPort + ); + + + + + return { + + + port, + + + queryPort + + + }; + + + } + + + + + + + + release( + port:number, + queryPort:number + ){ + + + this.usedPorts.delete( + port + ); + + + this.usedQueryPorts.delete( + queryPort + ); + + + } + + + + +} diff --git a/manager/src/rabbit.ts b/manager/src/rabbit.ts new file mode 100644 index 0000000..1a216c6 --- /dev/null +++ b/manager/src/rabbit.ts @@ -0,0 +1,28 @@ +import * as amqp from "amqplib"; + + +export class Rabbit { + + connection!: amqp.ChannelModel; + + channel!: amqp.Channel; + + + async connect(){ + + this.connection = + await amqp.connect( + "amqp://manager:gigi1337@192.168.1.74" + ); + + + this.channel = + await this.connection.createChannel(); + + + console.log( + "RabbitMQ verbunden" + ); + } + +} \ No newline at end of file diff --git a/manager/src/serverLifecycle.ts b/manager/src/serverLifecycle.ts new file mode 100644 index 0000000..3313fb6 --- /dev/null +++ b/manager/src/serverLifecycle.ts @@ -0,0 +1,670 @@ +import { ServerRegistry } from "./serverRegistry"; +import { ServerManager } from "./serverManager"; + + +export class ServerLifecycle { + + + private creatingServers = 0; + + + private emptyTimeout = 300000; // 5 Minuten + + + private lastScaleUp = 0; + + + private scaleCooldown = 60000; // 1 Minute + + + + + constructor( + + private serverRegistry: ServerRegistry, + + private serverManager: ServerManager, + + private desiredServers:number = 1 + + ){} + + + + + + start(){ + + + console.log( + "Server Lifecycle gestartet" + ); + + + setInterval(()=>{ + + + this.checkServers(); + + + },10000); + + + } + + + + + + setDesiredServers(count:number){ + + + console.log( + "Neue Soll Server Anzahl:", + count + ); + + + this.desiredServers = + count; + + + } + + + + + + + + + checkServers(){ + + + const servers = + this.serverRegistry.getAll(); + + + + + + /* + STATUS AKTUALISIEREN + */ + + + for(const server of servers){ + + + + switch(server.status){ + + + + case "STARTING": + + + if(server.pid){ + + + server.status = + "RUNNING"; + + + this.serverRegistry.update( + server + ); + + + console.log( + "Server RUNNING:", + server.name, + "PID:", + server.pid + ); + + + } + + + break; + + + + + + + case "RUNNING": + + + + /* + Crash Detection + */ + + + if( + Date.now() - + server.lastSeen + > + 30000 + ){ + + + server.status = + "CRASHED"; + + + this.serverRegistry.update( + server + ); + + + console.log( + "Server abgestürzt:", + server.name + ); + + + break; + + } + + + + + + + /* + Leere Server tracken + */ + + + if(server.players === 0){ + + + + if(!server.emptySince){ + + + server.emptySince = + Date.now(); + + + this.serverRegistry.update( + server + ); + + + console.log( + "Server leer:", + server.name + ); + + + } + + + + } + else{ + + + if(server.emptySince){ + + + console.log( + "Server wieder aktiv:", + server.name + ); + + + } + + + + server.emptySince = + undefined; + + + + this.serverRegistry.update( + server + ); + + + } + + + + break; + + + + + + + case "CRASHED": + + + + console.log( + "Server abgestürzt erkannt:", + server.name + ); + + + break; + + + + } + + + } + + + + + + + + /* + AKTIVE SERVER + */ + + + const activeServers = + + this.serverRegistry + .getAll() + .filter( + + server => + server.status === "RUNNING" || + server.status === "STARTING" + + ); + + + + + + + + + /* + SLOT AUSLASTUNG + */ + + + const runningServers = + + activeServers.filter( + + server => + server.status === "RUNNING" + + ); + + + + + + const totalPlayers = + + runningServers.reduce( + + (sum,server)=> + sum + server.players, + + 0 + + ); + + + + + + + const totalSlots = + + runningServers.reduce( + + (sum,server)=> + sum + server.maxPlayers, + + 0 + + ); + + + + + + + const usage = + + totalSlots > 0 + + ? + + totalPlayers / totalSlots + + : + + 0; + + + + + + console.log( + + "Kapazität:", + + `${totalPlayers}/${totalSlots}`, + + `${(usage * 100).toFixed(1)}%` + + ); + + + + + + + + + + /* + MINDEST SERVER ANZAHL + */ + + + const totalActive = + + activeServers.length + + this.creatingServers; + + + + + + if( + totalActive < + this.desiredServers + ){ + + + + const missing = + + this.desiredServers - + totalActive; + + + + this.startServers( + missing + ); + + + } + + + + + + + + + + /* + SCALE UP BEI 90% + */ + + + const shouldScale = + + + runningServers.length > 0 && + + + this.creatingServers === 0 && + + + usage >= 0.90 && + + + Date.now() - + this.lastScaleUp + > + this.scaleCooldown; + + + + + + + + if(shouldScale){ + + + + console.log( + + "Auslastung hoch - starte zusätzlichen Server" + + ); + + + + this.lastScaleUp = + Date.now(); + + + + this.startServers(1); + + + + } + + + + + + + + + + /* + LEERE SERVER ENTFERNEN + */ + + + + const removableServers = + + + activeServers + + .filter(server => + + + server.status === "RUNNING" && + + + server.players === 0 && + + + server.emptySince && + + + Date.now() - + server.emptySince + > + this.emptyTimeout + + + ) + + .sort( + + (a,b)=> + + (a.emptySince ?? 0) - + (b.emptySince ?? 0) + + ); + + + + + + + + for(const server of removableServers){ + + + + const activeCount = + + this.serverRegistry + .getAll() + .filter( + + s => + s.status === "RUNNING" || + s.status === "STARTING" + + ) + .length; + + + + + + if( + activeCount <= + this.desiredServers + ){ + + break; + + } + + + + + + console.log( + + "Stoppe leeren Server:", + + server.name + + ); + + + + + + this.serverManager.stopServer( + + server.id + + ); + + + } + + + + + + } + + + + + + + + + private startServers(count:number){ + + + + console.log( + + "Starte Server:", + + count + + ); + + + + + + for( + + let i = 0; + + i < count; + + i++ + + ){ + + + + this.creatingServers++; + + + + + + this.serverManager + .createServer() + + .then(()=>{ + + + this.creatingServers--; + + + }) + + + .catch(err=>{ + + + + this.creatingServers--; + + + + console.error( + + "Server Erstellung fehlgeschlagen:", + + err + + ); + + + }); + + + + } + + + } + + + +} \ No newline at end of file diff --git a/manager/src/serverLifecycleManager.ts b/manager/src/serverLifecycleManager.ts new file mode 100644 index 0000000..17ec158 --- /dev/null +++ b/manager/src/serverLifecycleManager.ts @@ -0,0 +1,135 @@ +import { ServerRegistry } from "./serverRegistry"; +import { Rabbit } from "./rabbit"; + + +export class ServerLifecycleManager { + + + constructor( + + private serverRegistry: ServerRegistry, + + private rabbit: Rabbit + + ){} + + + + start(){ + + + console.log( + "Server Lifecycle Manager gestartet" + ); + + + setInterval(()=>{ + + + this.checkServers(); + + + },5000); + + + } + + + + + + checkServers(){ + + + const servers = + this.serverRegistry.getAll(); + + + + for(const server of servers){ + + + + switch(server.status){ + + + + case "STARTING": + + + this.checkStarting( + server + ); + + break; + + + + case "RUNNING": + + + this.checkRunning( + server + ); + + break; + + + + case "CRASHED": + + + this.checkOffline( + server + ); + + break; + + + } + + + + } + + + + } + + + + + + checkStarting(server:any){ + + + // kommt später + + + } + + + + + checkRunning(server:any){ + + + // kommt später + + + } + + + + + checkOffline(server:any){ + + + // kommt später + + + } + + + +} diff --git a/manager/src/serverManager.ts b/manager/src/serverManager.ts new file mode 100644 index 0000000..20f64b3 --- /dev/null +++ b/manager/src/serverManager.ts @@ -0,0 +1,355 @@ +import { Rabbit } from "./rabbit"; +import { WrapperRegistry } from "./wrapperRegistry"; +import { ServerRegistry, ServerInfo } from "./serverRegistry"; +import { randomUUID } from "crypto"; +import { generateServerName } from "./nameGenerator"; +import { PortManager } from "./portManager"; + + +export class ServerManager { + + + constructor( + + private rabbit:Rabbit, + + private wrapperRegistry:WrapperRegistry, + + private serverRegistry:ServerRegistry, + + private portManager:PortManager + + ){} + + + + async createServer(){ + + + /* + freien Wrapper suchen + */ + + + const wrapper = + this.findBestWrapper(); + + + + if(!wrapper){ + + console.log( + "Kein Wrapper verfügbar" + ); + + return; + + } + + + + + + const serverId = + "srv-" + + randomUUID() + .slice(0,8); + + + + + + const serverName = + generateServerName( + + this.serverRegistry + .getAll() + .map(server => server.name) + + ); + + + + + + /* + Ports reservieren + */ + + + const ports = + this.portManager.allocate(); + + + + + + const server:ServerInfo = { + + + id: + serverId, + + + name: + serverName, + + + wrapperId: + wrapper.id, + + + status: + "STARTING", + + + + port: + ports.port, + + + + queryPort: + ports.queryPort, + + + + players: + 0, + + + maxPlayers: + 21, + + created: + Date.now(), + + lastSeen: + Date.now() + + }; + + + + + + /* + Registry Eintrag + */ + + + this.serverRegistry.add( + server + ); + + + + + + + /* + Command an Wrapper + */ + + + const command = { + + + type: + "server.start", + + + + serverId, + + + name: + serverName, + + + + port: + ports.port, + + + + queryPort: + ports.queryPort + + + }; + + + + + + const sent = + this.rabbit.channel.sendToQueue( + + wrapper.commandQueue, + + Buffer.from( + JSON.stringify(command) + ) + + ); + + + if(!sent){ + + throw new Error( + "Server Start Command konnte nicht gesendet werden" + ); + + } + + + + + + console.log( + + "Server Start gesendet:", + + server + + ); + + + } + + + + async stopServer(serverId:string){ + + + const server = + this.serverRegistry.get( + serverId + ); + + + if(!server){ + + console.log( + "Server nicht gefunden:", + serverId + ); + + return; + + } + + + + const wrapper = + this.wrapperRegistry.get( + server.wrapperId + ); + + + if(!wrapper){ + + console.log( + "Wrapper nicht gefunden:", + server.wrapperId + ); + + return; + + } + + + + const command = { + + + type: + "server.stop", + + + serverId + + + }; + + + + this.rabbit.channel.sendToQueue( + + wrapper.commandQueue, + + Buffer.from( + JSON.stringify(command) + ) + + ); + + + + server.status = + "STOPPING"; + + + this.serverRegistry.update( + server + ); + + + + console.log( + "Server Stop gesendet:", + server.name + ); + + + } + + + + + + + + findBestWrapper(){ + + + + const wrappers = + this.wrapperRegistry.getAll() + .filter( + w=>w.online + ); + + + + if(wrappers.length===0) + return undefined; + + + + /* + Einfachste Version: + + wenigste Server + */ + + + wrappers.sort( + + (a,b)=> + + a.servers.length - + b.servers.length + + ); + + + + return wrappers[0]; + + + } + + +} diff --git a/manager/src/serverRegistry.ts b/manager/src/serverRegistry.ts new file mode 100644 index 0000000..e6eba34 --- /dev/null +++ b/manager/src/serverRegistry.ts @@ -0,0 +1,378 @@ +export interface ServerInfo { + + + id:string; + + name:string; + + wrapperId:string; + + + status: + | "STARTING" + | "RUNNING" + | "STOPPING" + | "CRASHED"; + + + port:number; + + queryPort:number; + + + pid?:number; + + + players:number; + + + maxPlayers:number; + + + emptySince?:number; + + + created:number; + + + lastSeen:number; + + +} + + + + + +export class ServerRegistry { + + + + private servers = + new Map(); + + + + + + add(server:ServerInfo){ + + + this.servers.set( + + server.id, + + server + + ); + + + console.log( + "Server registriert:", + server + ); + + + } + + + + + + + get(id:string){ + + + return this.servers.get( + id + ); + + + } + + + + + + + + getAll(){ + + + return Array.from( + + this.servers.values() + + ); + + + } + + + + + + + + update(server:ServerInfo){ + + + this.servers.set( + + server.id, + + server + + ); + + + } + + + + + + + + remove(id:string){ + + + this.servers.delete( + id + ); + + + console.log( + "Server entfernt:", + id + ); + + + } + + + + + + + + + list(){ + + + console.log( + "\n=== SERVER ÜBERSICHT ===" + ); + + + + const servers = + this.getAll(); + + + + + if(servers.length === 0){ + + + console.log( + "Keine Server vorhanden" + ); + + + return; + + } + + + + + for(const server of servers){ + + + console.log({ + + + id: + server.id, + + + name: + server.name, + + + wrapper: + server.wrapperId, + + + status: + server.status, + + + port: + server.port, + + + queryPort: + server.queryPort, + + + players: + `${server.players}/${server.maxPlayers}` + + + }); + + + } + + + } + + + syncWrapperStatus(data:any){ + + if(!data.servers) + return; + + + + const runningServerIds = + new Set( + data.servers.map( + (srv:any)=>srv.id + ) + ); + + + + for(const server of this.servers.values()){ + + + if( + server.wrapperId === data.wrapperId && + !runningServerIds.has(server.id) + ){ + + + if(server.status === "STOPPING"){ + + + console.log( + "Server sauber beendet:", + server.name + ); + + + this.remove( + server.id + ); + + + } + else if(server.status === "RUNNING"){ + + + server.status = + "CRASHED"; + + + this.servers.set( + server.id, + server + ); + + + console.log( + "Server abgestürzt:", + server.name + ); + + + } + + } + + + } + + + + + + for(const srv of data.servers){ + const server = + this.servers.get( + srv.id + ); + + + if(!server) + continue; + + + + server.pid = + srv.pid; + + server.players = + srv.players ?? 0; + + + server.maxPlayers = + srv.maxPlayers ?? 0; + + + server.lastSeen = + Date.now(); + + + + const oldStatus = + server.status; + + + + if(srv.alive){ + + server.status = + "RUNNING"; + + } + else if( + server.status === "RUNNING" + ){ + + server.status = + "CRASHED"; + + } + + + + if(oldStatus !== server.status){ + + console.log( + "Status Änderung:", + server.name, + oldStatus, + "->", + server.status + ); + + } + + + + this.servers.set( + server.id, + server + ); + } + + + +} + + +} \ No newline at end of file diff --git a/manager/src/serverStatusManager.ts b/manager/src/serverStatusManager.ts new file mode 100644 index 0000000..0413504 --- /dev/null +++ b/manager/src/serverStatusManager.ts @@ -0,0 +1,238 @@ +import { Rabbit } from "./rabbit"; +import { ServerRegistry } from "./serverRegistry"; + + + +export class ServerStatusManager { + + + + constructor( + + private rabbit:Rabbit, + + private registry:ServerRegistry + + ){} + + + + + + async start(){ + + + + const queue = + "server.status"; + + + + await this.rabbit.channel.assertQueue( + queue + ); + + + + console.log( + "Warte auf Server Status..." + ); + + + + + this.rabbit.channel.consume( + + queue, + + + msg=>{ + + + if(!msg) + return; + + + + + const data = + JSON.parse( + msg.content.toString() + ); + + + + + + + switch(data.type){ + + + + case "server.started": + + + + this.serverStarted( + data + ); + + + break; + + + + + + case "server.stopped": + + + + this.serverStopped( + data + ); + + + break; + + + + + default: + + + console.log( + "Unbekannter Server Status:", + data + ); + + + } + + + + + + this.rabbit.channel.ack( + msg + ); + + + + } + + + ); + + + + } + + + + + + + + + + serverStarted(data:any){ + + + + const server = + this.registry.get( + data.serverId + ); + + + + + if(!server) + return; + + + + + server.status = + "RUNNING"; + + + + + this.registry.update( + server + ); + + + + + console.log( + + "Server läuft:", + + server.name + + ); + + + + } + + + + + + + + + + serverStopped(data:any){ + + + + const server = + this.registry.get( + data.serverId + ); + + + + + + if(!server) + return; + + + + + + server.status = + "CRASHED"; + + + + + this.registry.update( + server + ); + + + + + console.log( + + "Server crashed:", + + server.name + + ); + + + + } + + + + +} diff --git a/manager/src/types.ts b/manager/src/types.ts new file mode 100644 index 0000000..f23145c --- /dev/null +++ b/manager/src/types.ts @@ -0,0 +1,24 @@ +export interface Wrapper { + + id: string; + + hostname: string; + + cpu: number; + + ram: number; + + lastHeartbeat: number; + + commandQueue: string; + +} + + +export interface Message { + + type: string; + + [key:string]: any; + +} diff --git a/manager/src/wrapperManager.ts b/manager/src/wrapperManager.ts new file mode 100644 index 0000000..36a3518 --- /dev/null +++ b/manager/src/wrapperManager.ts @@ -0,0 +1,615 @@ +import { Rabbit } from "./rabbit"; +import { randomUUID } from "crypto"; +import { WrapperRegistry, WrapperInfo } from "./wrapperRegistry"; +import { ServerRegistry } from "./serverRegistry"; + + +export class WrapperManager { + + + constructor( + private rabbit: Rabbit, + private registry: WrapperRegistry, + private serverRegistry: ServerRegistry + ){ + + } + + + + async start(){ + + + const queue = + "wrapper.register"; + + + + await this.rabbit.channel.assertQueue( + queue + ); + + + + console.log( + "Warte auf Wrapper..." + ); + + + + this.rabbit.channel.consume( + + queue, + + async msg => { + + + if(!msg) + return; + + + + const data = + JSON.parse( + msg.content.toString() + ); + + + + switch(data.type){ + + + + case "wrapper.register": + + + await this.register( + data + ); + + + break; + + + + case "wrapper.heartbeat": + + + this.heartbeat( + data + ); + + + break; + + + + case "wrapper.status": + + + this.registry.updateStatus( + data + ); + + this.serverRegistry.syncWrapperStatus( + data + ); + + + break; + + + + default: + + + console.log( + "Unbekannte Nachricht:", + data + ); + + + } + + + + this.rabbit.channel.ack( + msg + ); + + + } + + ); + + + + + + // Offline Check + + setInterval(()=>{ + + + this.registry.checkOffline( + 30000 + ); + + + },5000); + + + + } + + + + + + + + + async register(data:any){ + + + + let id = + data.wrapperId; + + + + + /* + Neue Wrapper bekommen eine ID + */ + + if(!id){ + + + id = + "wrp-" + + randomUUID() + .slice(0,8); + + + + console.log( + "Neue Wrapper ID:", + id + ); + + + } + + + + + + const existing = + this.registry.get( + id + ); + + + + + + /* + Wrapper war schon bekannt + */ + + if(existing){ + + + existing.online = true; + + + existing.lastHeartbeat = + Date.now(); + + + + existing.hostname = + data.hostname; + + + + existing.cpu = + data.cpu; + + + + existing.ram = + data.ram; + + + + existing.ramFree = + data.ramFree ?? data.ram; + + + + existing.servers = + data.servers ?? existing.servers; + + + + this.registry.update(existing); + + + } + + + + /* + Neuer Wrapper + */ + + else { + + + + const commandQueue = + `wrapper.${id}.commands`; + + + + + await this.rabbit.channel.assertQueue( + commandQueue + ); + + + + + + const wrapper:WrapperInfo = { + + + id, + + + hostname: + data.hostname, + + + + commandQueue, + + + + cpu: + data.cpu, + + + + ram: + data.ram, + + + + ramFree: + data.ram, + + + + servers: + [], + + + + online:true, + + + + lastHeartbeat: + Date.now() + + + + }; + + + + + + this.registry.add( + wrapper + ); + + + + } + + + + + + + + + /* + Antwort zurück an Wrapper + */ + + + const wrapper = + this.registry.get( + id + ); + + + + + if(data.replyQueue){ + + + this.rabbit.channel.sendToQueue( + + + data.replyQueue, + + + Buffer.from( + + JSON.stringify({ + + + type: + "wrapper.registered", + + + + wrapperId: + id, + + + + commandQueue: + wrapper?.commandQueue + + + }) + + ) + + ); + + + } + + + + + } + + + + + + + + + + heartbeat(data:any){ + + + + this.registry.updateHeartbeat( + + data.wrapperId + + ); + + + + console.log( + + "Heartbeat:", + + data.wrapperId + + ); + + + + } + + + + + + + + + + sendCommand( + + wrapperId:string, + + command:any + + ){ + + + + const wrapper = + this.registry.get( + wrapperId + ); + + + + + if(!wrapper){ + + + throw new Error( + + "Wrapper nicht gefunden" + + ); + + + } + + + + + + this.rabbit.channel.sendToQueue( + + + wrapper.commandQueue, + + + + Buffer.from( + + JSON.stringify( + command + ) + + ) + + ); + + + + } + + + + + + + + + + getWrappers(){ + + + return this.registry.getAll(); + + + } + + + + + + + + + + listWrappers(){ + + + + const wrappers = + this.registry.getAll(); + + + + + console.log( + + "\n======================" + + ); + + + console.log( + + " WRAPPER ÜBERSICHT " + + ); + + + console.log( + + "======================\n" + + ); + + + + + + if(wrappers.length===0){ + + + console.log( + + "Keine Wrapper registriert" + + ); + + + return; + + + } + + + + + + + for(const wrapper of wrappers){ + + + + console.log(` + +ID: +${wrapper.id} + + +Host: +${wrapper.hostname} + + +CPU: +${wrapper.cpu} Cores + + +RAM: +${wrapper.ram} MB + + +RAM frei: +${wrapper.ramFree} MB + + +Server: +${wrapper.servers.length} + + +Online: +${wrapper.online} + + +Heartbeat: +${new Date( + wrapper.lastHeartbeat +).toLocaleString()} + + +---------------------- + +`); + + + + + } + + + + } + + + + +} \ No newline at end of file diff --git a/manager/src/wrapperRegistry.ts b/manager/src/wrapperRegistry.ts new file mode 100644 index 0000000..df9da58 --- /dev/null +++ b/manager/src/wrapperRegistry.ts @@ -0,0 +1,192 @@ +export interface WrapperInfo { + + id:string; + + hostname:string; + + commandQueue:string; + + cpu:number; + + ram:number; + + + ramFree:number; + + + online:boolean; + + lastHeartbeat:number; + + + servers:string[]; + +} + + + + + +export class WrapperRegistry { + + + private wrappers = + new Map(); + + + + + + add(wrapper:WrapperInfo){ + + + this.wrappers.set( + wrapper.id, + wrapper + ); + + + console.log( + "Wrapper gespeichert:", + wrapper.id + ); + + + } + + + + + + get(id:string){ + + return this.wrappers.get(id); + + } + + + + + + getAll(){ + + return Array.from( + this.wrappers.values() + ); + + } + + + + + + updateHeartbeat(id:string){ + + + const wrapper = + this.wrappers.get(id); + + + + if(!wrapper) + return; + + + + wrapper.lastHeartbeat = + Date.now(); + + + wrapper.online = + true; + + + } + + + + + update(wrapper:WrapperInfo){ + + + this.wrappers.set( + wrapper.id, + wrapper + ); + + + } + + + checkOffline(timeout:number){ + + + const now = + Date.now(); + + + + for(const wrapper of this.wrappers.values()){ + + + if( + wrapper.online && + now - wrapper.lastHeartbeat > timeout + ){ + + + wrapper.online = false; + + + console.log( + "Wrapper offline:", + wrapper.id + ); + + + } + + + } + + + } + + updateStatus(data:any){ + + + const wrapper = + this.wrappers.get( + data.wrapperId + ); + + + + if(!wrapper) + return; + + + + wrapper.ramFree = + data.ramFree; + + + + wrapper.servers = + data.servers; + + + + wrapper.lastHeartbeat = + Date.now(); + + + + wrapper.online = + true; + + + + } + +} diff --git a/manager/tsconfig.json b/manager/tsconfig.json new file mode 100644 index 0000000..9621dbb --- /dev/null +++ b/manager/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/wrapper/config.json b/wrapper/config.json new file mode 100644 index 0000000..08b8480 --- /dev/null +++ b/wrapper/config.json @@ -0,0 +1,3 @@ +{ + "wrapperId": "wrp-8adcbbf2" +} \ No newline at end of file diff --git a/wrapper/package-lock.json b/wrapper/package-lock.json new file mode 100644 index 0000000..c0b642a --- /dev/null +++ b/wrapper/package-lock.json @@ -0,0 +1,243 @@ +{ + "name": "wrapper", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wrapper", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "amqplib": "^2.0.1", + "steam-server-query": "^1.1.3" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "@types/node": "^26.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/amqplib": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/amqplib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-2.0.1.tgz", + "integrity": "sha512-a3P2MgfCf9nzVis12VxWEn0dS6hcqve7dlEAhXDtIWR27BlhtMkILOc+H9aeHjDi6i6r94dYKc2Kx2OFe3avvg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/steam-server-query": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steam-server-query/-/steam-server-query-1.1.3.tgz", + "integrity": "sha512-mXVYpH7HhWchbpKXKc5PvwSrHpB7vCgMfZKVBadOONNfL+KEwkaopte6nF5MkyAmRieML8bg9KC0QnMqUD4rnA==" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/wrapper/package.json b/wrapper/package.json new file mode 100644 index 0000000..6aab8fe --- /dev/null +++ b/wrapper/package.json @@ -0,0 +1,22 @@ +{ + "name": "wrapper", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "ts-node src/index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "amqplib": "^2.0.1", + "steam-server-query": "^1.1.3" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "@types/node": "^26.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } +} diff --git a/wrapper/src/config.ts b/wrapper/src/config.ts new file mode 100644 index 0000000..33be789 --- /dev/null +++ b/wrapper/src/config.ts @@ -0,0 +1,54 @@ +import fs from "fs"; + + +const FILE = "./config.json"; + + +export interface WrapperConfig { + + wrapperId?: string; + +} + + + +export function loadConfig(): WrapperConfig { + + + if(!fs.existsSync(FILE)){ + + return {}; + + } + + + return JSON.parse( + fs.readFileSync( + FILE, + "utf-8" + ) + ); + +} + + + +export function saveConfig( + config: WrapperConfig +){ + + + fs.writeFileSync( + + FILE, + + JSON.stringify( + config, + null, + 4 + ) + + ); + + +} \ No newline at end of file diff --git a/wrapper/src/index.ts b/wrapper/src/index.ts new file mode 100644 index 0000000..b84cdd7 --- /dev/null +++ b/wrapper/src/index.ts @@ -0,0 +1,448 @@ +import * as amqp from "amqplib"; +import os from "os"; +import { loadConfig, saveConfig } from "./config"; +import { ProcessManager } from "./processManager"; +import { StatusReporter } from "./statusReporter"; +import { QueryManager } from "./queryManager"; + + +class Wrapper { + + processManager = + new ProcessManager(); + + queryManager = + new QueryManager( + this.processManager + ); + + connection!: amqp.ChannelModel; + channel!: amqp.Channel; + + + id?: string; + + + + async start(){ + + + const config = + loadConfig(); + + + + if(config.wrapperId){ + + + this.id = + config.wrapperId; + + + + console.log( + "Starte mit vorhandener ID:", + this.id + ); + + + }else{ + + + console.log( + "Keine Wrapper ID vorhanden - neue Registrierung" + ); + + + } + + + + + this.connection = + await amqp.connect( + "amqp://manager:gigi1337@192.168.1.74" + ); + + + + this.channel = + await this.connection.createChannel(); + + + + console.log( + "RabbitMQ verbunden" + ); + + + + this.queryManager.start(); + await this.register(); + + + } + + + + + + + + async register(){ + + + + const reply = + await this.channel.assertQueue( + "", + { + exclusive:true + } + ); + + + + const replyQueue = + reply.queue; + + + + + + + await this.channel.consume( + + replyQueue, + + msg=>{ + + + if(!msg) + return; + + + + const data = + JSON.parse( + msg.content.toString() + ); + + + + console.log( + "Antwort vom Manager:", + data + ); + + + + + if(data.type==="wrapper.registered"){ + + + + this.id = + data.wrapperId; + + + + saveConfig({ + + wrapperId: + this.id + + }); + + + + console.log( + "Wrapper ID gespeichert:", + this.id + ); + + if(!this.id){ + + console.error( + "Keine Wrapper ID erhalten!" + ); + + return; + + } + + + this.listenCommands( + data.commandQueue + ); + + const reporter = + new StatusReporter( + + this.channel, + + this.processManager, + + this.queryManager, + + this.id + + ); + + + reporter.start(); + + + } + + + + this.channel.ack(msg); + + + } + + ); + + + + + + + + + const message = { + + + + type: + "wrapper.register", + + + + // vorhandene ID mitsenden + wrapperId: + this.id, + + + + hostname: + os.hostname(), + + + + cpu: + os.cpus().length, + + + + ram: + Math.round( + os.totalmem()/1024/1024 + ), + + + + replyQueue + + + }; + + + + + + + await this.channel.sendToQueue( + + "wrapper.register", + + Buffer.from( + JSON.stringify(message) + ) + + ); + + + + + console.log( + "Registrierung gesendet", + this.id ?? "ohne ID" + ); + + + } + + + + + + + + async listenCommands(queue:string){ + + + await this.channel.consume( + + queue, + + msg=>{ + + + if(!msg) + return; + + + + const command = + JSON.parse( + msg.content.toString() + ); + + + + switch(command.type){ + + + + case "test": + + + console.log( + "TEST COMMAND:", + command.message + ); + + + break; + + + + + + case "server.start": + + + this.processManager.startServer( + command + ); + + + break; + + + case "server.stop": + + + this.processManager.stopServer( + command.serverId + ); + + + break; + + + default: + + + console.log( + "Unbekannter Command:", + command + ); + + + } + + + + this.channel.ack(msg); + + + + } + + ); + + + + console.log( + "Warte auf Commands:", + queue + ); + + + + this.heartbeat(); + + } + + + + + heartbeat(){ + + console.log("Heartbeat gestartet"); + setInterval(()=>{ + + + + if(!this.id) + return; + + + + const heartbeat = { + + + type: + "wrapper.heartbeat", + + + + wrapperId: + this.id + + + }; + + + + + this.channel.sendToQueue( + + + "wrapper.register", + + + + Buffer.from( + + JSON.stringify( + heartbeat + ) + + ) + + ); + + + + console.log( + "Heartbeat gesendet" + ); + + + + },10000); + + + } + + + +} + + + +new Wrapper().start(); \ No newline at end of file diff --git a/wrapper/src/processManager.ts b/wrapper/src/processManager.ts new file mode 100644 index 0000000..d8e749c --- /dev/null +++ b/wrapper/src/processManager.ts @@ -0,0 +1,352 @@ +import { spawn, ChildProcess } from "child_process"; + + + +interface ServerProcess { + + + id:string; + + name:string; + + port:number; + + queryPort:number; + + process:ChildProcess; + + started:number; + + alive:boolean; + + + players:number; + + maxPlayers:number; + + +} + + + +export class ProcessManager { + + + + private processes = + new Map(); + + + + + + + startServer(data:any){ + + + + console.log( + "Starte Game Server:", + data.name + ); + + + + + const args = [ + + `-Port=${data.port}`, + + `-QueryPort=${data.queryPort}`, + + `-ServerName=${data.name}` + + ]; + + + + + + const process = spawn( + "/home/heroes/HeroesOfValorServer.sh", + args, + { + cwd:"/home/heroes", + stdio:"pipe", + detached:true + } + ); + + + + + + + + process.stdout?.on( + "data", + data=>{ + + + console.log( + + `[${data.name}]`, + data.toString() + + ); + + + } + + ); + + + + + + process.stderr?.on( + + "data", + + data=>{ + + + console.error( + + "SERVER ERROR:", + data.toString() + + ); + + + } + + ); + + + + + + + + process.on("exit", code=>{ + + + const server = + this.processes.get(data.serverId); + + + + if(server){ + + + console.log( + "Server beendet:", + server.name, + code + ); + + + this.processes.delete( + data.serverId + ); + + + } + + + }); + + + + + + + + + this.processes.set( + + data.serverId, + + { + + id:data.serverId, + + name:data.name, + + port:data.port, + + queryPort:data.queryPort, + + process, + + started:Date.now(), + + alive:true, + + + players:0, + + maxPlayers:0 + + } + + ); + + + + + + + console.log( + + "Server gestartet:", + + { + + id:data.serverId, + + pid:process.pid + + } + + ); + + + + } + + + + + + + + + + stopServer(id:string){ + + + + const server = + this.processes.get(id); + + + + + if(!server){ + + + console.log( + + "Server nicht gefunden:", + + id + + ); + + + return; + + } + + + + + + + console.log( + + "Stoppe Server:", + + server.name + + ); + + + + + + process.kill( + -server.process.pid!, + "SIGTERM" + ); + + + + + + } + + + + + + + + + getServers(){ + + + return Array.from( + + this.processes.values() + + ); + + + } + + getStatus(){ + + + return Array.from( + this.processes.values() + ).map(server => ({ + + + id: + server.id, + + + name: + server.name, + + + port: + server.port, + + + queryPort: + server.queryPort, + + + pid: + server.process.pid, + + + alive: + server.alive, + + + players: + server.players, + + + maxPlayers: + server.maxPlayers, + + + started: + server.started + + + })); + + + } + + + + +} diff --git a/wrapper/src/queryManager.ts b/wrapper/src/queryManager.ts new file mode 100644 index 0000000..364b647 --- /dev/null +++ b/wrapper/src/queryManager.ts @@ -0,0 +1,131 @@ +import { queryGameServerInfo } from "steam-server-query"; +import { ProcessManager } from "./processManager"; + + +export class QueryManager { + + + constructor( + private processManager: ProcessManager + ){} + + + + + start(){ + + + console.log( + "Query Manager gestartet" + ); + + + + setInterval(()=>{ + + + this.updateQueries(); + + + },5000); + + + } + + + + + + + async updateQueries(){ + + + + const servers = + this.processManager.getServers(); + + + + + for(const server of servers){ + + + try { + + + const info = + await queryGameServerInfo(`127.0.0.1:${server.queryPort}`); + + + + + + server.players = + info.players; + + + + server.maxPlayers = + info.maxPlayers; + + + + + + console.log( + + "Query:", + server.name, + `${server.players}/${server.maxPlayers}` + + ); + + + } + catch(err){ + + + console.log( + + "Query fehlgeschlagen:", + server.name + + ); + + + } + + + } + + + } + + getStatus(){ + + + return this.processManager + .getServers() + .map(server => ({ + + + id: + server.id, + + + players: + server.players, + + + maxPlayers: + server.maxPlayers + + + })); + + + } + + + +} diff --git a/wrapper/src/statusReporter.ts b/wrapper/src/statusReporter.ts new file mode 100644 index 0000000..ee4f352 --- /dev/null +++ b/wrapper/src/statusReporter.ts @@ -0,0 +1,164 @@ +import * as amqp from "amqplib"; +import { ProcessManager } from "./processManager"; +import os from "os"; +import { QueryManager } from "./queryManager"; + + + +export class StatusReporter { + + + constructor( + + private channel: amqp.Channel, + + private processManager: ProcessManager, + + private queryManager: QueryManager, + + private wrapperId: string + + ){} + + + + start(){ + + console.log( + "Status Reporter gestartet" + ); + + + setInterval(()=>{ + + + this.sendStatus(); + + + },10000); + + + } + + + + + + + sendStatus(){ + + + + const processes = + this.processManager.getStatus(); + + + const queries = + this.queryManager.getStatus(); + + + + const servers = + processes.map(server => { + + + const query = + queries.find( + (q:any) => + q.id === server.id + ); + + + + return { + + + ...server, + + + players: + query?.players ?? 0, + + + maxPlayers: + query?.maxPlayers ?? 0 + + + }; + + + }); + + + + + + const status = { + + + type: + "wrapper.status", + + + wrapperId: + this.wrapperId, + + + servers, + + + + ramFree: + Math.round( + + os.freemem() + / + 1024 + / + 1024 + + ) + + + }; + + + + + + this.channel.sendToQueue( + + + "wrapper.register", + + + + Buffer.from( + + JSON.stringify(status) + + ) + + + ); + + + + + + console.log( + + "Status gesendet:", + + servers.length, + + "Server" + + ); + + + } + + + +} \ No newline at end of file diff --git a/wrapper/tsconfig.json b/wrapper/tsconfig.json new file mode 100644 index 0000000..9621dbb --- /dev/null +++ b/wrapper/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +}