MCSManager/core/Process/BaseMcserver.js
2021-06-15 13:56:51 +08:00

431 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const childProcess = require("child_process");
const iconv = require("iconv-lite");
const EventEmitter = require("events");
const tools = require("../tools");
const permission = require("../../helper/Permission");
const path = require("path");
const fs = require("fs");
const Docker = require("dockerode");
class ServerProcess extends EventEmitter {
constructor(args) {
super(args);
this.dataModel = undefined;
this.process = undefined;
this._run = false;
//用于异步进行的锁
this._loading = false;
}
// 自定义命令启动方式
customCommandStart() {
//暂时使用 MCSMERVER.log 目前已弃用,下版本 log4js
MCSERVER.infoLog("Minecraft Server start", this.dataModel.name);
MCSERVER.log(["服务器 [", this.dataModel.name, "] 启动进程:"].join(" "));
MCSERVER.log("-------------------------------");
MCSERVER.log(["自定义参数启动: ", this.dataModel.highCommande].join(" "));
MCSERVER.log(["根:", this.dataModel.cwd].join(" "));
MCSERVER.log("-------------------------------");
if (!this.dataModel.highCommande || this.dataModel.highCommande.trim().length <= 0) throw new Error("自定义参数非法,无法启动服务端");
let commandArray = this.dataModel.highCommande.split(" ");
let javaPath = commandArray.shift();
//过滤
let parList = [];
for (let k in commandArray) {
if (commandArray[k] == "") continue;
parList.push(commandArray[k]);
}
this.process = childProcess.spawn(javaPath, parList, this.ProcessConfig);
}
// 标准 Java 程序启动方式
templateStart(onlyCommandString = false) {
let tmpAddList = [];
let tmpShouldList = [];
this.dataModel.Xmx && tmpShouldList.push("-Xmx" + this.dataModel.Xmx);
this.dataModel.Xms && tmpShouldList.push("-Xms" + this.dataModel.Xms);
tmpShouldList.push("-Djline.terminal=jline.UnsupportedTerminal");
tmpShouldList.push("-jar");
tmpShouldList.push(this.dataModel.jarName);
tmpShouldList.push("nogui");
tmpAddList = this.dataModel.addCmd.concat(tmpShouldList);
//过滤
let parList = [];
for (let k in tmpAddList) {
if (tmpAddList[k] == "") continue;
parList.push(tmpAddList[k]);
}
let commandString = this.dataModel.java + " " + parList.toString().replace(/,/gim, " ");
//是否只获取命令字符串
if (onlyCommandString) return commandString;
//暂时使用 MCSMERVER.log 目前已弃用,下版本 log4js
MCSERVER.infoLog("Minecraft Server start", this.dataModel.name);
MCSERVER.log(["服务器 [", this.dataModel.name, "] 启动进程:"].join(" "));
MCSERVER.log("-------------------------------");
MCSERVER.log(["启动: ", commandString].join(" "));
MCSERVER.log(["根:", this.dataModel.cwd].join(" "));
MCSERVER.log("-------------------------------");
this.process = childProcess.spawn(this.dataModel.java, parList, this.ProcessConfig);
}
//使用 Docker API 启动进程
async dockerStart() {
// 命令模板与准备数据
let stdCwd = path.resolve(this.dataModel.cwd).replace(/\\/gim, "/");
// 采用 Docker API 进行启动与监控
// 启动命令解析
let startCommande = "";
if (this.dataModel.highCommande.trim() != "") startCommande = this.dataModel.highCommande;
else startCommande = this.templateStart(true);
const startCommandeArray = startCommande.split(" ");
let portmap = this.dataModel.dockerConfig.dockerPorts;
// 端口解析
var ports = portmap.split("|");
// 绑定内部暴露端口
const ExposedPortsObj = {};
// 绑定内部暴露端口与其对应的宿主机端口
const PortBindingsObj = {};
for (var portstr of ports) {
var agreement = portstr.split("/");
var protocol = "tcp";
if (agreement.length >= 2 && (agreement[1] === "udp" || agreement[1] === "tcp")) {
protocol = agreement[1];
}
var port = portstr.split(":");
if (port.length > 2) {
throw new Error("参数配置端口映射错误。");
}
if (port.length == 2) {
// 一个端口的配置项目
ExposedPortsObj[port[0] + "/" + protocol] = {};
PortBindingsObj[port[0] + "/" + protocol] = [
{
HostPort: port[1] + ""
}
];
}
}
// 输出启动消息
MCSERVER.log("实例 [", this.dataModel.name, "] 正在启动...");
MCSERVER.log("-------------------------------");
MCSERVER.log("正在使用虚拟化技术启动进程");
MCSERVER.log("命令:", startCommandeArray.join(" "));
MCSERVER.log("开放端口:", portmap);
MCSERVER.log("工作目录:", stdCwd);
MCSERVER.log("-------------------------------");
// 模拟一个正常的 Process
this.process = new EventEmitter();
const process = this.process;
const self = this;
// 基于镜像启动虚拟化容器
const docker = new Docker();
let auxContainer = null;
auxContainer = await docker.createContainer({
Image: this.dataModel.dockerConfig.dockerImageName,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: startCommandeArray,
OpenStdin: true,
StdinOnce: false,
ExposedPorts: ExposedPortsObj,
HostConfig: {
Binds: [stdCwd + ":/mcsd/"],
Memory: this.dataModel.dockerConfig.dockerXmx * 1024 * 1024 * 1024,
PortBindings: PortBindingsObj
}
});
try {
// 尝试启动容器
await auxContainer.start();
} catch (err) {
this.stop();
this.emit("exit", 0);
throw new Error("实例进程启动失败,建议检查启动参数与设置");
}
// 链接容器的输入输出流
auxContainer.attach(
{
stream: true,
stdin: true,
stdout: true
},
(err, stream) => {
if (err) throw err;
// 赋值进程容器
process.dockerContainer = auxContainer;
// 模拟 pid
process.pid = auxContainer.id;
// 对接普通进程的输入输出流
process.stdin = stream;
process.stdout = stream;
process.stderr = null;
// 模拟进程杀死功能
process.kill = () => {
auxContainer.kill().then(() => {
auxContainer.remove().then(() => {
MCSERVER.log("实例", "[", self.dataModel.name, "]", "容器已强制移除");
});
});
};
// 容器工作完毕退出事件
auxContainer.wait(() => {
self.emit("exit", 0);
self.stop();
auxContainer.remove();
});
// 容器流错误事件传递
stream.on("error", (err) => {
MCSERVER.error("服务器运行时异常,建议检查配置与环境", err);
self.printlnStdin(["Error:", err.name, "\n Error Message:", err.message, "\n 进程 PID:", self.process.pid || "启动失败,无法获取进程。"]);
self.stop();
self.emit("error", err);
});
// 判断启动是否成功
if (!process.pid) {
MCSERVER.error("服务端进程启动失败,建议检查启动命令与参数是否正确");
self.stop();
auxContainer.remove();
throw new Error("服务端进程启动失败,建议检查启动命令与参数是否正确");
}
// 开启启动状态
self._run = true;
self._loading = false;
self.dataModel.lastDate = new Date().toLocaleString();
// 输出事件的传递
process.stdout.on("data", (data) => self.emit("console", iconv.decode(data, self.dataModel.oe)));
// 产生事件开启
self.emit("open", self);
// 输出开服资料
self.printlnCommandLine("服务端 " + self.dataModel.name + " 执行开启命令.");
}
);
}
// 统一服务端开启入口
// 不论是通过哪种方式启动,必须从这个入口进入,再根据不同配置进行分支
start() {
// 服务端时间权限判断
let timeResult = this.isDealLineDate();
if (timeResult) {
throw new Error("服务端于 " + this.dataModel.timeLimitDate + " 时间已到期,拒绝启动,请咨询管理员。");
}
// 防止重复启动
if (this._run || this._loading) throw new Error("服务端进程在运行或正在加载..");
this._loading = true;
let jarPath = this.dataModel.jarName;
if (!path.isAbsolute(jarPath)) {
jarPath = this.dataModel.cwd + "/" + this.dataModel.jarName;
}
jarPath = jarPath.replace(/\/\//gim, "/");
// 选择启动方式 自定义命令与配置启动
if (!this.dataModel.highCommande) {
// 只在非自定义模式下检查参数
if (!fs.existsSync(this.dataModel.cwd)) {
this.stop();
throw new Error('服务端根目录 "' + jarPath + '" 不存在!');
}
if (this.dataModel.jarName.trim() == "") {
this.stop();
throw new Error("未设置服务端核心文件,无法启动服务器");
}
if (!fs.existsSync(jarPath)) {
this.stop();
throw new Error('服务端文件 "' + jarPath + '" 不存在或错误!');
}
} else {
// 自定义模式检查
// 检查是否准许自定义命令
if (!MCSERVER.localProperty.customize_commande) {
this.stop();
throw new Error("操作禁止!管理员禁止服务器使用自定义命令!");
}
}
this.ProcessConfig = {
cwd: this.dataModel.cwd,
stdio: "pipe"
};
try {
if (this.dataModel.dockerConfig.isDocker) {
// Docker 启动,异步函数
// 选用虚拟化技术启动后,将不再执行下面代码逻辑,由专属的进程启动方式启动。
this.dockerStart().then(undefined, (error) => {
// Docker 启动时异常处理
MCSERVER.error("此服务器启动时异常,具体错误信息:", error);
this.printlnCommandLine("进程实例启动时失败,建议检查配置文件与启动参数");
this.stop();
});
// 阻止继续运行下去
return true;
} else {
// 确定是自定义命令启动还是模板正常方式启动。
this.dataModel.highCommande ? this.customCommandStart() : this.templateStart();
}
} catch (err) {
this.stop();
throw new Error("进程启动时异常:" + err.name + ":" + err.message);
}
// 设置启动状态
this._run = true;
this._loading = false;
this.dataModel.lastDate = new Date().toLocaleString();
// 进程事件监听
this.process.on("error", (err) => {
MCSERVER.error("服务器运行时异常,建议检查配置与环境", err);
this.printlnStdin(["Error:", err.name, "\n Error Message:", err.message, "\n 进程 PID:", this.process.pid || "启动失败,无法获取进程。"]);
this.stop();
this.emit("error", err);
});
// 进程启动成功确认
if (!this.process.pid) {
MCSERVER.error("服务端进程启动失败建议检查启动命令与参数是否正确pid:", this.process.pid);
this.stop();
delete this.process;
throw new Error("服务端进程启动失败,建议检查启动命令与参数是否正确");
}
// 输出事件的传递
this.process.stdout.on("data", (data) => this.emit("console", iconv.decode(data, this.dataModel.oe)));
this.process.stderr.on("data", (data) => this.emit("console", iconv.decode(data, this.dataModel.oe)));
this.process.on("exit", (code) => {
this.emit("exit", code);
this.stop();
});
// 产生事件开启
this.emit("open", this);
// 输出开服信息
this.printlnCommandLine("服务端 " + this.dataModel.name + " 执行开启命令.");
return true;
}
// 发送指令
send(command) {
if (this._run) {
if (this.process.dockerContainer != null) {
this.process.stdin.write(iconv.encode(command, this.dataModel.ie) + "\n");
} else {
this.process.stdin.write(iconv.encode(command, this.dataModel.ie));
this.process.stdin.write("\n");
}
return true;
}
return true;
}
// 重启实例
restart() {
if (this._run == true) {
this.stopServer();
// 开始计时重启
let timeCount = 0;
let timesCan = setInterval(() => {
if (this._run == false) {
// 服务器关闭时 3 秒后立即重启
setTimeout(() => {
try {
this.start();
} catch (err) {
MCSERVER.error("服务器重启失败:", err);
}
}, 3000);
clearInterval(timesCan);
}
//90s 内服务器依然没有关闭,代表出现问题
if (timeCount >= 90) {
clearInterval(timesCan);
}
timeCount++;
}, 1000);
return true;
}
}
// 这并不是推荐的直接使用方式;
// stop 方法只适用于本类调用,因为使用此方法不管是否成功停止,都必将进入停止状态;
// 这样即有可能面板显示已经停止,但进程还在运行的情况;
// 最好的做法是通过命令来结束。
stop() {
this._run = false;
this._loading = false;
}
// 通过命令关闭服务器
stopServer() {
if (this.dataModel.stopCommand.toLocaleLowerCase() === "^c") {
this.process.kill("SIGINT");
return;
}
if (this.dataModel.stopCommand) {
this.send(this.dataModel.stopCommand);
return;
}
this.send("stop");
this.send("end");
this.send("exit");
}
// 杀死进程,若是 Docker 进程则是移除容器
kill() {
if (this._run) {
this.process.kill("SIGKILL");
this._run = false;
return true;
}
return false;
}
// 是否运行中
isRun() {
return this._run;
}
//输出一行到标准输出
printlnStdin(line) {
let str = ["[MCSMANAGER] [", tools.getFullTime(), "]:", line, "\r\n"].join(" ");
this.emit("console", str);
}
printlnCommandLine(line) {
this.emit("console", "[MCSMANAGER] -------------------------------------------------------------- \r\n");
this.printlnStdin(line);
this.emit("console", "[MCSMANAGER] -------------------------------------------------------------- \r\n");
}
isDealLineDate() {
let timeResult = permission.isTimeLimit(this.dataModel.timeLimitDate);
return timeResult;
}
}
module.exports = ServerProcess;