Initial project structure

This commit is contained in:
2026-07-20 16:44:23 +02:00
commit c19fde4126
27 changed files with 4679 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
node_modules/
dist/
build/
.env
*.log
.vscode/
+12
View File
@@ -0,0 +1,12 @@
# Server Manager
Projekt zur Verwaltung von Dedicated Game Servern.
## Komponenten
- Manager
- Wrapper
## Architektur
Manager steuert Wrapper über RabbitMQ.
+237
View File
@@ -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"
}
}
}
}
+21
View File
@@ -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"
}
}
+37
View File
@@ -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
);
}
}
+105
View File
@@ -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();
+70
View File
@@ -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)
);
}
+118
View File
@@ -0,0 +1,118 @@
export class PortManager {
private usedPorts =
new Set<number>();
private usedQueryPorts =
new Set<number>();
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
);
}
}
+28
View File
@@ -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"
);
}
}
+670
View File
@@ -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
);
});
}
}
}
+135
View File
@@ -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
}
}
+355
View File
@@ -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];
}
}
+378
View File
@@ -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<string, ServerInfo>();
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
);
}
}
}
+238
View File
@@ -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
);
}
}
+24
View File
@@ -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;
}
+615
View File
@@ -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()}
----------------------
`);
}
}
}
+192
View File
@@ -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<string, WrapperInfo>();
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;
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"wrapperId": "wrp-8adcbbf2"
}
+243
View File
@@ -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"
}
}
}
}
+22
View File
@@ -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"
}
}
+54
View File
@@ -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
)
);
}
+448
View File
@@ -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();
+352
View File
@@ -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<string, ServerProcess>();
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
}));
}
}
+131
View File
@@ -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
}));
}
}
+164
View File
@@ -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"
);
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}