This repository has been archived on 2022-10-14. You can view files and clone it, but cannot push or open issues/pull-requests.
jpland-manager/manager.js

222 lines
8.4 KiB
JavaScript

require("dotenv").config();
var child_process = require("child_process");
var EventEmitter = require("events").EventEmitter;
var colors = require("colors");
var Discord = require("discord.js");
process.chdir("/srv/jpland");
String.prototype.capitalize = function() {
return this[0].toUpperCase() + this.substring(1);
};
var MAX_IDLE_MINUTES = 60;
var CMD_PREFIX = '%';
class MinecraftServer extends EventEmitter {
constructor (cwd, jar) {
super();
this.cwd = cwd;
this.java = "java";
this.jvm_args = ["-Xmx4G"];
this.jar = jar;
this.idleMinutes = 0;
this.locked = false;
// paper 1.16.2
this.listCommand = "minecraft:list";
this.listEmptyRegex = /^\[\d\d:\d\d:\d\d INFO\]: There are 0 of a max of \d{1,} players online:$/;
this.listNotEmptyRegex = /^\[\d\d:\d\d:\d\d INFO\]: There are [1-9]\d{0,} of a max of \d{1,} players online:/;
}
start() {
this._log("Starting server".green);
this.process = child_process.spawn("nice", ["-n", "1", this.java].concat(this.jvm_args).concat(["-jar", this.jar]), {cwd: this.cwd});
this.process.on("error", error => {
this.process.emit("exit");
this._log(error.stack.red, true);
});
this.process.stdout.on("data", data => {
data = data.toString().trim().split("\n");
data.forEach(data => {
this._log(data);
this._handleOutput(data);
});
});
this.process.stderr.on("data", data => {
data = data.toString().trim().split("\n");
this._log(data, true);
});
this.process.once("exit", () => {
this.process = undefined;
clearInterval(this.listInterval);
this._log("Server has exited".red);
});
this.idleMinutes = 0;
this.listInterval = setInterval(()=>{
this.process.stdin.write(this.listCommand + "\n");
}, 60000);
return this;
}
stop() {
return this.process && this.process.stdin.write("stop\n");
}
restart() {
if (!this.process) return;
this.process.on("exit", () => this.start());
return this.stop();
}
_log(msg, isError) {
return console[isError ? "error" : "log"](`[${this.cwd}]`[isError ? "red" : "green"], msg);
}
_handleOutput(line) {
if (this._testIfConsoleLineIndicatesNoPlayersOnline(line)) {
// no players are online
this._log("Detected no players online; incrementing idleMinutes".yellow);
this.idleMinutes++;
if (this.idleMinutes >= MAX_IDLE_MINUTES) {
if (!this.locked) {
this.idleMinutes = 0;
this.stop();
this._log("Shutting down due to inactivity".red);
this.emit("idle timeout");
} else {
this._log("Idle timeout exceeded but ignoring because server is locked".yellow);
}
}
} else if (this._testIfConsoleLineIndicatesPlayersOnline(line)) {
// players are online
this._log("Detected players online; resetting idle minutes".yellow);
this.idleMinutes = 0;
}
}
_testIfConsoleLineIndicatesPlayersOnline(line) {
return this.listNotEmptyRegex.test(line);
}
_testIfConsoleLineIndicatesNoPlayersOnline(line) {
return this.listEmptyRegex.test(line);
}
}
var servers = {
creative: new MinecraftServer("creative", "paper.jar"),
survival: new MinecraftServer("survival", "paper.jar"),
modded: new MinecraftServer("forge", "forge.jar"),
modded2: new MinecraftServer("modded2", "forge.jar"),
multiverse: new MinecraftServer("multiverse", "paper-1618.jar"),
};
//TODO regex but if it's not broken, don't fix it 🤷
servers.modded._testIfConsoleLineIndicatesPlayersOnline = function(line) {
var fojat = line.substr(line.indexOf("] [Server thread/INFO] [minecraft/DedicatedServer]: There are "));
return (fojat.startsWith("] [Server thread/INFO] [minecraft/DedicatedServer]: There are ") && fojat.endsWith(" players online:"));
}
servers.modded._testIfConsoleLineIndicatesNoPlayersOnline = function(line) {
return line.endsWith("] [Server thread/INFO] [minecraft/DedicatedServer]: There are 0/20 players online:");
}
servers.modded.listCommand = "list";
servers.modded.java = "/usr/bin/java"; // java 8
servers.modded2._testIfConsoleLineIndicatesPlayersOnline = servers.modded._testIfConsoleLineIndicatesPlayersOnline;
servers.modded2._testIfConsoleLineIndicatesNoPlayersOnline = servers.modded._testIfConsoleLineIndicatesNoPlayersOnline;
servers.modded2.listCommand = servers.modded.listCommand;
servers.modded2.java = "/usr/bin/java";
servers.multiverse.listEmptyRegex = /^\[\d\d:\d\d:\d\d INFO\]: There are 0\/\d{1,} players online:$/;
servers.multiverse.listNotEmptyRegex = /^\[\d\d:\d\d:\d\d INFO\]: There are [1-9]\d{0,}\/\d{1,} players online:$/;
function commandHandler(input, priviledged) {
var args = input.split(' ');
var cmd = args[0];
var unauthorized = "You are not permitted to perform this command.";
var serverName = args[1] && args[1].toLowerCase();
var server = servers[serverName];
if (cmd == "start") {
if (!server) return `Unknown server ${serverName}`
if (server.process) return `${serverName.capitalize()} server is already running.`;
server.start();
return `Starting ${serverName} server.`;
} else if (cmd == "stop") {
if (!priviledged) return unauthorized;
if (!server) return `Unknown server ${serverName}`
if (!server.process) return `${serverName.capitalize()} server is not running.`;
server.stop();
return `Stopping ${serverName} server.`;
} else if (cmd == "input") {
if (!priviledged) return unauthorized;
if (!server) return `Unknown server ${serverName}`
if (!server.process) return `${serverName.capitalize()} server is not running.`;
if (args[2] == "list" || args[2] == "minecraft:list") return "`{CMD_PREFIX}list` command is prohibited from running in console because it would interfere with idle minute counting.";
server.process.stdin.write(args.slice(2).join(" ") + '\n');
return;
} else if (cmd == "list") {
return `Servers: ${Object.keys(servers).map(x => `${x} (${servers[x].process ? 'running' : 'stopped'})`).join(', ')}`;
} else if (cmd == "lock") {
if (!priviledged) return unauthorized;
if (!server) return `Unknown server ${serverName}`;
if (server.locked = !server.locked) {
return `${serverName.capitalize()} server will be exempt from idle timeout.`;
} else {
return `${serverName.capitalize()} server will no longer be exempt from idle timeout.`;
}
} else if (cmd == "eval") {
if (!priviledged) return unauthorized;
try {
return String(eval(args.slice(1).join(' ')));
} catch (error) {
return String(error);
}
} else if (cmd == "help") {
return "JPLand Manager automatically shuts down Minecraft servers after "+MAX_IDLE_MINUTES+" minutes to save resources, and allows you to start servers again using the `{CMD_PREFIX}start <server>` command.\n" +
"Use `{CMD_PREFIX}list` to see the list of servers and their statuses.\n" +
(priviledged ? "\nYou are an admin and may also use these commands: `{CMD_PREFIX}stop <server>`, `{CMD_PREFIX}input <server> <command>` (input a command into a server's console), `{CMD_PREFIX}lock <server>` (prevent automatic shutdown), `{CMD_PREFIX}eval <code>` (evaluate javascript in the Node.js process)." : "");
} else {
return `Unknown command \`{CMD_PREFIX}${cmd}\`, use \`{CMD_PREFIX}help\` for the list of commands.`;
}
}
process.openStdin();
process.stdin.on("data", data => {
data = data.toString().trim();
var response = commandHandler(data, true);
if (response) console.log(response.replace(/{CMD_PREFIX}/g, '').blue);
});
if (process.env.DISCORD_TOKEN) {
var dClient = new Discord.Client();
dClient.login(process.env.DISCORD_TOKEN);
dClient.on("error", error => console.error(colors.red("Discord client error: " + error.message)));
function setStatus() {
dClient.user.setActivity(`${CMD_PREFIX}help`);
}
setInterval(setStatus, 1000*60*30);
dClient.on("ready", () => {
console.log("Discord client is ready.".green);
setStatus();
});
dClient.on("message", message => {
if (message.content.startsWith(CMD_PREFIX)) {
let response = commandHandler(message.content.substr(CMD_PREFIX.length), message.member && message.member.guild.id == "357038384121905152" && message.member.roles.map(x => x.name).includes("Minecraft admin"));
if (response) message.channel.send(response.replace(/{CMD_PREFIX}/g, CMD_PREFIX));
}
});
for (let serverName in servers) {
let server = servers[serverName];
server.on("idle timeout", () => {
let channel = dClient.channels.get("452025433328975872");
if (channel) channel.send(`${serverName.capitalize()} server has been shut down due to 1 hour of inactivity. Run \`${CMD_PREFIX}start ${serverName}\` when you want to play on it again.`);
});
}
}
console.log("JPLand Manager is now running.".green);