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
+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"
);
}
}