Skip to content

Commit

Permalink
feat(bot): graceful shutdown
Browse files Browse the repository at this point in the history
  • Loading branch information
yurisasc committed Jan 30, 2024
1 parent ed1b590 commit 89f3582
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 8 deletions.
4 changes: 3 additions & 1 deletion discord-bot/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ CLIENT_ID='YOUR_BOT_CLIENT_ID_HERE'
CLIENT_TOKEN='YOUR_BOT_TOKEN_HERE'
SERVERS_CONFIG=[{"name": "My Private Server", "awsAccount": {"profileName": "default", "accessKeyId": "", "secretAccessKey": "", "region": "us-east-1"}}]
CLUSTER_NAME=palworld
SERVICE_NAME=palworld-server
SERVICE_NAME=palworld-server
RCON_PORT=25575
RCON_PASSWORD='YOUR_RCON_PASSWORD_HERE'
6 changes: 5 additions & 1 deletion discord-bot/lang/lang.en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
"title": "🟡 Starting {{SERVER_NAME}}...",
"description": "Please wait while the server starts. It may take up to 10 minutes."
},
"stopServer": {
"stoppingServer": {
"title": "🟡 Stopping {{SERVER_NAME}}...",
"description": "We will save your progress before stopping the server."
},
"stoppedServer": {
"title": "🔴 Server stopped",
"description": "Hope you had fun and don't forget to touch real grass 🌿"
},
"invalidServerName": {
"title": "❌ Invalid server",
"description": "{{SERVER_NAME}} does not exist. Please choose the available server."
Expand Down
6 changes: 5 additions & 1 deletion discord-bot/lang/lang.en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
"title": "🟡 Starting {{SERVER_NAME}}...",
"description": "Please wait while the server starts. It may take up to 10 minutes."
},
"stopServer": {
"stoppingServer": {
"title": "🟡 Stopping {{SERVER_NAME}}...",
"description": "We will save your progress before stopping the server."
},
"stoppedServer": {
"title": "🔴 Server stopped",
"description": "Hope you had fun and don't forget to touch real grass 🌿"
},
"invalidServerName": {
"title": "❌ Invalid server",
"description": "{{SERVER_NAME}} does not exist. Please choose the available server."
Expand Down
13 changes: 12 additions & 1 deletion discord-bot/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion discord-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"linguini": "^1.3.1",
"node-fetch": "^3.3.0",
"pino": "^8.11.0",
"pino-pretty": "^9.4.0"
"pino-pretty": "^9.4.0",
"rcon-srcds": "^2.1.0"
},
"devDependencies": {
"@types/node": "^16.4.10",
Expand Down
13 changes: 12 additions & 1 deletion discord-bot/src/commands/chat/stop-server-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,22 @@ export class StopServerCommand implements Command {
if (args.option) {
try {
// TODO: Check that the server is running before stopping it
// Saving the game and shutting down the server gracefully.
embed = Lang.getEmbed("displayEmbeds.stoppingServer", data.lang, {
SERVER_NAME: args.option,
});
await InteractionUtils.send(intr, embed);
await this.multiServersService.saveGameAndShutdownByName(args.option);

// Stopping the server instance
await this.multiServersService.stopServerByName(args.option);
embed = Lang.getEmbed("displayEmbeds.stopServer", data.lang, {
embed = Lang.getEmbed("displayEmbeds.stoppedServer", data.lang, {
SERVER_NAME: args.option,
});
await InteractionUtils.send(intr, embed);
return;
} catch (error) {
console.log(error);
embed = Lang.getEmbed("displayEmbeds.invalidServerName", data.lang, {
SERVER_NAME: args.option,
});
Expand Down
41 changes: 41 additions & 0 deletions discord-bot/src/services/aws-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,47 @@ export class AWSService {
this.setServiceDesiredCount(0);
}

public async getServerPublicIP(profile: AWSProfile): Promise<string> {
this.setProfile(profile);
try {
const service = new AWS.ECS({ region: profile.region });
const network = new AWS.EC2({ region: profile.region });

const tasks = await service
.listTasks({
cluster: process.env.CLUSTER_NAME,
serviceName: process.env.SERVICE_NAME,
})
.promise();

if (tasks.taskArns.length === 0) {
console.log("No tasks found for this service");
return;
}

const describeTasksResponse = await service
.describeTasks({
cluster: process.env.CLUSTER_NAME,
tasks: tasks.taskArns,
})
.promise();
const task = describeTasksResponse.tasks[0];
const eni = task.attachments[0].details.find(
(detail) => detail.name === "networkInterfaceId"
).value;

// Get the public IP address from the network interface
const networkInterface = await network
.describeNetworkInterfaces({
NetworkInterfaceIds: [eni],
})
.promise();
return networkInterface.NetworkInterfaces[0].Association.PublicIp;
} catch (error) {
console.error("Error finding public IP:", error);
}
}

private setServiceDesiredCount(desiredCount: number): void {
const ecs = new AWS.ECS();

Expand Down
36 changes: 35 additions & 1 deletion discord-bot/src/services/multi-servers-service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { AWSProfile, PalworldServer } from "../models/internal-models";
import { AWSService } from "./aws-service";
import { RconService } from "./rcon-service";

export class MultiServersService {
private servers: PalworldServer[];

constructor(private readonly awsService: AWSService) {
constructor(
private readonly awsService: AWSService,
private readonly rconService: RconService
) {
this.servers = readServerConfigs();
}

Expand All @@ -18,6 +22,36 @@ export class MultiServersService {
await this.awsService.stopServer(awsAccount);
}

public async saveGameAndShutdownByName(name: string): Promise<void> {
const awsAccount = this.awsAccountByName(name);
const publicIp = await this.awsService.getServerPublicIP(awsAccount);

try {
this.rconService.connect({
host: publicIp,
port: 25575,
password: process.env.RCON_PASSWORD!,
});
console.log("Authenticated");

const saveResult = await this.rconService.sendCommand("Save");

// Delay to allow the server to save
await new Promise((resolve) => setTimeout(resolve, 15000));
console.log("SaveResult:", saveResult);

const shutdownResult = await this.rconService.sendCommand(
"Shutdown 10 Shutdown executed by Discord bot..."
);

console.log("ShutdownResult:", shutdownResult);
// Delay to allow the server to shutdown
await new Promise((resolve) => setTimeout(resolve, 15000));
} catch (error) {
console.error("Error in saveGameAndShutdownByName:", error);
}
}

private awsAccountByName(name: string): AWSProfile {
const awsAccount = this.servers.find(
(server) => server.name === name
Expand Down
69 changes: 69 additions & 0 deletions discord-bot/src/services/rcon-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as net from "net";

export class RconService {
private client: net.Socket;
private requestId: number = 1;
private host: string;
private port: number;
private password: string;

connect({
host,
port,
password,
}: {
host: string;
port: number;
password: string;
}): Promise<void> {
this.host = host;
this.port = port;
this.password = password;
this.client = new net.Socket();

return new Promise((resolve, reject) => {
this.client.connect(this.port, this.host, () => {
this.sendPacket(3, this.password)
.then(() => resolve())
.catch(reject);
});

this.client.on("error", (error) => {
reject(error);
});
});
}

sendCommand(command: string): Promise<string> {
return this.sendPacket(2, command);
}

private sendPacket(type: number, body: string): Promise<string> {
return new Promise((resolve, reject) => {
const buffer = Buffer.alloc(14 + body.length);
buffer.writeInt32LE(10 + body.length, 0);
buffer.writeInt32LE(this.requestId, 4);
buffer.writeInt32LE(type, 8);
buffer.write(body, 12);
buffer.writeInt16LE(0, 12 + body.length);

this.client.write(buffer);

this.client.once("data", (data) => {
if (data.readInt32LE(8) === -1) {
reject(new Error("Authentication failed"));
return;
}
resolve(data.toString("utf-8", 12, data.length - 2));
});

this.client.once("error", (error) => {
reject(error);
});
});
}

disconnect(): void {
this.client.destroy();
}
}
4 changes: 3 additions & 1 deletion discord-bot/src/start-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
MultiServersService,
getServerNames,
} from "./services/multi-servers-service.js";
import { RconService } from "./services/rcon-service.js";

const require = createRequire(import.meta.url);
let Config = require("../config/config.json");
Expand All @@ -36,7 +37,8 @@ async function start(): Promise<void> {
// Services
let eventDataService = new EventDataService();
let awsService = new AWSService();
let multiServersService = new MultiServersService(awsService);
let rconService = new RconService();
let multiServersService = new MultiServersService(awsService, rconService);

// Client
let client = new CustomClient({
Expand Down

0 comments on commit 89f3582

Please sign in to comment.