mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-01-30 15:19:32 +08:00
429 lines
16 KiB
JavaScript
429 lines
16 KiB
JavaScript
const childProcess = require('child_process');
|
||
const iconv = require('iconv-lite');
|
||
const EventEmitter = require('events');
|
||
|
||
const DataModel = require('../DataModel');
|
||
const os = require('os');
|
||
const tools = require('../tools');
|
||
const permission = require('../../helper/Permission');
|
||
const path = require('path')
|
||
|
||
const properties = require("properties");
|
||
const fs = require('fs');
|
||
const Docker = require('dockerode');
|
||
|
||
const CODE_CONSOLE = MCSERVER.localProperty.console_encode;
|
||
|
||
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(/\\/igm, "/");
|
||
|
||
// 采用 Docker API 进行启动与监控
|
||
// 启动命令解析
|
||
let startCommande = "";
|
||
if (this.dataModel.highCommande.trim() != "")
|
||
startCommande = this.dataModel.highCommande;
|
||
else
|
||
startCommande = this.templateStart(true);
|
||
const startCommandeArray = startCommande.split(" ");
|
||
// 端口解析
|
||
const protocol = "tcp";
|
||
let portmap = this.dataModel.dockerConfig.dockerPorts;
|
||
portmap = portmap.split(":");
|
||
if (portmap.length > 2) {
|
||
throw new Error("不支持的多端口操作方法,参数配置端口数量错误。");
|
||
}
|
||
// 绑定内部暴露端口
|
||
const ExposedPortsObj = {};
|
||
// 绑定内部暴露端口与其对应的宿主机端口
|
||
const PortBindingsObj = {};
|
||
if (portmap.length == 2) {
|
||
// 一个端口的配置项目
|
||
ExposedPortsObj[portmap[0] + "/" + protocol] = {};
|
||
PortBindingsObj[portmap[0] + "/" + protocol] = [{
|
||
HostPort: portmap[1] + ""
|
||
}];
|
||
}
|
||
// 输出启动消息
|
||
MCSERVER.log('实例 [', this.dataModel.name, '] 正在启动...');
|
||
MCSERVER.log('-------------------------------');
|
||
MCSERVER.log("正在使用虚拟化技术启动进程");
|
||
MCSERVER.log("命令:", startCommandeArray.join(" "));
|
||
MCSERVER.log("开放端口:", portmap.join("->"));
|
||
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 = (() => {
|
||
docker.getContainer(auxContainer.id).kill().then(() => {
|
||
docker.getContainer(auxContainer.id).remove().then(() => {
|
||
MCSERVER.log('实例', '[', self.dataModel.name, ']', '容器已强制移除');
|
||
});
|
||
});
|
||
});
|
||
// 容器工作完毕退出事件
|
||
auxContainer.wait(() => {
|
||
self.emit('exit', 0);
|
||
self.stop();
|
||
});
|
||
// 容器流错误事件传递
|
||
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();
|
||
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(/\/\//igm, '/');
|
||
|
||
// 选择启动方式 自定义命令与配置启动
|
||
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.send('stop');
|
||
this.send('end');
|
||
this.send('exit');
|
||
|
||
// 开始计时重启
|
||
let timeCount = 0;
|
||
let timesCan = setInterval(() => {
|
||
if (this._run == false) {
|
||
// 服务器关闭时 3 秒后立即重启
|
||
setTimeout(() => {
|
||
try {
|
||
this.start();
|
||
} catch (err) {
|
||
MCSERVER.error('服务器重启失败:', err);
|
||
}
|
||
}, 3000);
|
||
clearInterval(timesCan);
|
||
}
|
||
//60s 内服务器依然没有关闭,代表出现问题
|
||
if (timeCount >= 60) {
|
||
clearInterval(timesCan)
|
||
}
|
||
timeCount++;
|
||
|
||
}, 1000);
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 这并不是推荐的直接使用方式;
|
||
// stop 方法只适用于本类调用,因为使用此方法不管是否成功停止,都必将进入停止状态;
|
||
// 这样即有可能面板显示已经停止,但进程还在运行的情况;
|
||
// 最好的做法是通过命令来结束。
|
||
stop() {
|
||
this._run = false;
|
||
this._loading = false;
|
||
|
||
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;
|