@dimzxzzx07/mc-headless 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -703
- package/dist/core/JavaChecker.d.ts +16 -3
- package/dist/core/JavaChecker.d.ts.map +1 -1
- package/dist/core/JavaChecker.js +179 -31
- package/dist/core/JavaChecker.js.map +1 -1
- package/dist/core/MinecraftServer.d.ts +61 -0
- package/dist/core/MinecraftServer.d.ts.map +1 -1
- package/dist/core/MinecraftServer.js +742 -60
- package/dist/core/MinecraftServer.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -16
- package/dist/index.js.map +1 -1
- package/dist/platforms/BedrockServer.d.ts.map +1 -1
- package/dist/platforms/BedrockServer.js +2 -0
- package/dist/platforms/BedrockServer.js.map +1 -1
- package/dist/platforms/JavaServer.d.ts.map +1 -1
- package/dist/platforms/JavaServer.js +2 -0
- package/dist/platforms/JavaServer.js.map +1 -1
- package/dist/platforms/SkinRestorer.d.ts +14 -0
- package/dist/platforms/SkinRestorer.d.ts.map +1 -0
- package/dist/platforms/SkinRestorer.js +145 -0
- package/dist/platforms/SkinRestorer.js.map +1 -0
- package/dist/platforms/SkinsRestorer.d.ts +14 -0
- package/dist/platforms/SkinsRestorer.d.ts.map +1 -0
- package/dist/platforms/SkinsRestorer.js +145 -0
- package/dist/platforms/SkinsRestorer.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/JavaChecker.ts +170 -34
- package/src/core/MinecraftServer.ts +854 -64
- package/src/index.ts +33 -17
- package/src/platforms/BedrockServer.ts +2 -0
- package/src/platforms/JavaServer.ts +2 -0
- package/src/platforms/SkinRestorer.ts +127 -0
- package/src/scripts/install-java.sh +97 -32
- package/src/types/index.ts +2 -0
|
@@ -32,14 +32,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.MinecraftServer = void 0;
|
|
40
37
|
const events_1 = require("events");
|
|
41
38
|
const child_process_1 = require("child_process");
|
|
42
|
-
const pidusage_1 = __importDefault(require("pidusage"));
|
|
43
39
|
const cron = __importStar(require("node-cron"));
|
|
44
40
|
const ConfigHandler_1 = require("./ConfigHandler");
|
|
45
41
|
const JavaChecker_1 = require("./JavaChecker");
|
|
@@ -52,7 +48,9 @@ const ForgeEngine_1 = require("../engines/ForgeEngine");
|
|
|
52
48
|
const FabricEngine_1 = require("../engines/FabricEngine");
|
|
53
49
|
const GeyserBridge_1 = require("../platforms/GeyserBridge");
|
|
54
50
|
const ViaVersion_1 = require("../platforms/ViaVersion");
|
|
51
|
+
const SkinRestorer_1 = require("../platforms/SkinRestorer");
|
|
55
52
|
const path = __importStar(require("path"));
|
|
53
|
+
const fs = __importStar(require("fs-extra"));
|
|
56
54
|
class MinecraftServer extends events_1.EventEmitter {
|
|
57
55
|
config;
|
|
58
56
|
options;
|
|
@@ -60,21 +58,66 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
60
58
|
engine;
|
|
61
59
|
geyser;
|
|
62
60
|
viaVersion;
|
|
61
|
+
skinRestorer;
|
|
63
62
|
process = null;
|
|
64
63
|
serverInfo;
|
|
65
64
|
players = new Map();
|
|
66
65
|
backupCron = null;
|
|
67
66
|
startTime = null;
|
|
67
|
+
memoryMonitorInterval = null;
|
|
68
|
+
statsInterval = null;
|
|
69
|
+
memoryUsageHistory = [];
|
|
70
|
+
worldSize = 0;
|
|
71
|
+
playerCount = 0;
|
|
72
|
+
javaCommand = 'java';
|
|
73
|
+
javaInfo = null;
|
|
74
|
+
owners = new Set();
|
|
75
|
+
ownerCommandPrefix = '!';
|
|
76
|
+
lastCpuTotal = 0;
|
|
77
|
+
lastCpuTime = 0;
|
|
78
|
+
cpuUsage = 0;
|
|
79
|
+
cgroupMemory = 0;
|
|
80
|
+
cgroupCpu = 0;
|
|
68
81
|
constructor(userConfig = {}) {
|
|
69
82
|
super();
|
|
70
83
|
this.logger = Logger_1.Logger.getInstance();
|
|
71
84
|
this.logger.banner();
|
|
72
|
-
this.options =
|
|
85
|
+
this.options = {
|
|
86
|
+
javaVersion: 'auto',
|
|
87
|
+
usePortableJava: true,
|
|
88
|
+
memoryMonitor: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
threshold: 90,
|
|
91
|
+
interval: 30000,
|
|
92
|
+
action: 'warn'
|
|
93
|
+
},
|
|
94
|
+
autoInstallJava: true,
|
|
95
|
+
networkOptimization: {
|
|
96
|
+
tcpFastOpen: true,
|
|
97
|
+
bungeeMode: false,
|
|
98
|
+
proxyProtocol: false
|
|
99
|
+
},
|
|
100
|
+
owners: [],
|
|
101
|
+
ownerCommands: {
|
|
102
|
+
prefix: '!',
|
|
103
|
+
enabled: true
|
|
104
|
+
},
|
|
105
|
+
silentMode: true,
|
|
106
|
+
statsInterval: 30000,
|
|
107
|
+
...userConfig
|
|
108
|
+
};
|
|
109
|
+
if (this.options.owners) {
|
|
110
|
+
this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
|
|
111
|
+
}
|
|
112
|
+
if (this.options.ownerCommands?.prefix) {
|
|
113
|
+
this.ownerCommandPrefix = this.options.ownerCommands.prefix;
|
|
114
|
+
}
|
|
73
115
|
const handler = new ConfigHandler_1.ConfigHandler(userConfig);
|
|
74
116
|
this.config = handler.getConfig();
|
|
75
117
|
this.engine = this.createEngine();
|
|
76
118
|
this.geyser = new GeyserBridge_1.GeyserBridge();
|
|
77
119
|
this.viaVersion = new ViaVersion_1.ViaVersionManager();
|
|
120
|
+
this.skinRestorer = new SkinRestorer_1.SkinRestorerManager();
|
|
78
121
|
this.serverInfo = {
|
|
79
122
|
pid: 0,
|
|
80
123
|
ip: this.config.network.ip,
|
|
@@ -87,8 +130,30 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
87
130
|
maxPlayers: this.config.world.maxPlayers,
|
|
88
131
|
uptime: 0,
|
|
89
132
|
memory: { used: 0, max: 0 },
|
|
133
|
+
cpu: 0,
|
|
90
134
|
status: 'stopped'
|
|
91
135
|
};
|
|
136
|
+
this.detectCgroupLimits();
|
|
137
|
+
}
|
|
138
|
+
detectCgroupLimits() {
|
|
139
|
+
try {
|
|
140
|
+
if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
|
|
141
|
+
const limit = fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
|
|
142
|
+
this.cgroupMemory = parseInt(limit) / 1024 / 1024;
|
|
143
|
+
this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
|
|
144
|
+
}
|
|
145
|
+
if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
|
|
146
|
+
const quota = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8'));
|
|
147
|
+
const period = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8'));
|
|
148
|
+
if (quota > 0 && period > 0) {
|
|
149
|
+
this.cgroupCpu = quota / period;
|
|
150
|
+
this.logger.debug(`Cgroup CPU limit: ${this.cgroupCpu} cores`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.logger.debug('Not running in cgroup environment');
|
|
156
|
+
}
|
|
92
157
|
}
|
|
93
158
|
createEngine() {
|
|
94
159
|
switch (this.config.type) {
|
|
@@ -106,13 +171,536 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
106
171
|
throw new Error(`Unsupported server type: ${this.config.type}`);
|
|
107
172
|
}
|
|
108
173
|
}
|
|
174
|
+
async detectJavaVersion() {
|
|
175
|
+
try {
|
|
176
|
+
const output = (0, child_process_1.execSync)('java -version 2>&1').toString();
|
|
177
|
+
if (output.includes('version "21')) {
|
|
178
|
+
return '21';
|
|
179
|
+
}
|
|
180
|
+
else if (output.includes('version "17')) {
|
|
181
|
+
return '17';
|
|
182
|
+
}
|
|
183
|
+
else if (output.includes('version "11')) {
|
|
184
|
+
return '11';
|
|
185
|
+
}
|
|
186
|
+
else if (output.includes('version "8')) {
|
|
187
|
+
return '8';
|
|
188
|
+
}
|
|
189
|
+
return 'unknown';
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return 'none';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async ensureJava() {
|
|
196
|
+
if (!this.options.autoInstallJava) {
|
|
197
|
+
await JavaChecker_1.JavaChecker.ensureJava();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const targetVersion = this.options.javaVersion === 'auto' ? '17' : this.options.javaVersion || '17';
|
|
201
|
+
if (this.options.usePortableJava) {
|
|
202
|
+
this.javaInfo = await JavaChecker_1.JavaChecker.getOrDownloadPortableJava(targetVersion);
|
|
203
|
+
this.javaCommand = this.javaInfo.path;
|
|
204
|
+
this.logger.success(`Using portable Java ${this.javaInfo.version} from ${this.javaInfo.path}`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const hasJava = await JavaChecker_1.JavaChecker.checkJava();
|
|
208
|
+
if (!hasJava) {
|
|
209
|
+
this.logger.info('Java not found, attempting to install system Java...');
|
|
210
|
+
const osType = SystemDetector_1.SystemDetector.getOS();
|
|
211
|
+
const distro = SystemDetector_1.SystemDetector.getDistro();
|
|
212
|
+
if (osType === 'linux' || osType === 'android') {
|
|
213
|
+
await this.installJavaLinux(distro, targetVersion);
|
|
214
|
+
}
|
|
215
|
+
else if (osType === 'darwin') {
|
|
216
|
+
await this.installJavaMac(targetVersion);
|
|
217
|
+
}
|
|
218
|
+
else if (osType === 'windows') {
|
|
219
|
+
this.logger.error('Windows detected. Please install Java manually from https://adoptium.net');
|
|
220
|
+
throw new Error('Java not installed');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const detectedVersion = await this.detectJavaVersion();
|
|
225
|
+
this.logger.info(`Detected system Java version: ${detectedVersion}`);
|
|
226
|
+
if (detectedVersion === '21' || detectedVersion === '17') {
|
|
227
|
+
this.javaCommand = 'java';
|
|
228
|
+
this.logger.success(`Using system Java ${detectedVersion}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.logger.warning(`System Java ${detectedVersion} is not optimal. Switching to portable Java...`);
|
|
232
|
+
this.javaInfo = await JavaChecker_1.JavaChecker.getOrDownloadPortableJava(targetVersion);
|
|
233
|
+
this.javaCommand = this.javaInfo.path;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const finalVersion = this.javaInfo ? this.javaInfo.version : await this.detectJavaVersion();
|
|
238
|
+
this.logger.success(`Java version: ${finalVersion}`);
|
|
239
|
+
}
|
|
240
|
+
async installJavaLinux(distro, version) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
let command = '';
|
|
243
|
+
const javaPackage = version === '21' ? 'openjdk-21-jre-headless' : 'openjdk-17-jre-headless';
|
|
244
|
+
if (distro === 'ubuntu' || distro === 'debian') {
|
|
245
|
+
command = `apt update && apt install -y ${javaPackage}`;
|
|
246
|
+
}
|
|
247
|
+
else if (distro === 'centos' || distro === 'fedora') {
|
|
248
|
+
command = `yum install -y java-${version}-openjdk-headless`;
|
|
249
|
+
}
|
|
250
|
+
else if (distro === 'arch') {
|
|
251
|
+
const archPackage = version === '21' ? 'jre21-openjdk-headless' : 'jre17-openjdk-headless';
|
|
252
|
+
command = `pacman -S --noconfirm ${archPackage}`;
|
|
253
|
+
}
|
|
254
|
+
else if (distro === 'termux') {
|
|
255
|
+
command = 'pkg install -y openjdk-17';
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
this.logger.error('Unsupported Linux distribution. Please install Java manually.');
|
|
259
|
+
reject(new Error('Unsupported distribution'));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.logger.info(`Installing Java ${version} with: ${command}`);
|
|
263
|
+
const install = (0, child_process_1.exec)(command, (error) => {
|
|
264
|
+
if (error) {
|
|
265
|
+
reject(error);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
resolve();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
if (install.stdout) {
|
|
272
|
+
install.stdout.pipe(process.stdout);
|
|
273
|
+
}
|
|
274
|
+
if (install.stderr) {
|
|
275
|
+
install.stderr.pipe(process.stderr);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async installJavaMac(version) {
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const command = `brew install openjdk@${version}`;
|
|
282
|
+
this.logger.info(`Installing Java ${version} with: ${command}`);
|
|
283
|
+
const install = (0, child_process_1.exec)(command, (error) => {
|
|
284
|
+
if (error) {
|
|
285
|
+
reject(error);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
resolve();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
if (install.stdout) {
|
|
292
|
+
install.stdout.pipe(process.stdout);
|
|
293
|
+
}
|
|
294
|
+
if (install.stderr) {
|
|
295
|
+
install.stderr.pipe(process.stderr);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async calculateWorldSize() {
|
|
300
|
+
try {
|
|
301
|
+
const worldPath = path.join(process.cwd(), this.config.folders.world);
|
|
302
|
+
if (!await fs.pathExists(worldPath))
|
|
303
|
+
return 0;
|
|
304
|
+
const getSize = async (dir) => {
|
|
305
|
+
let total = 0;
|
|
306
|
+
const files = await fs.readdir(dir);
|
|
307
|
+
for (const file of files) {
|
|
308
|
+
const filePath = path.join(dir, file);
|
|
309
|
+
const stat = await fs.stat(filePath);
|
|
310
|
+
if (stat.isDirectory()) {
|
|
311
|
+
total += await getSize(filePath);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
total += stat.size;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return total;
|
|
318
|
+
};
|
|
319
|
+
return await getSize(worldPath);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
buildJavaArgs() {
|
|
326
|
+
if (this.options.customJavaArgs && this.options.customJavaArgs.length > 0) {
|
|
327
|
+
return this.options.customJavaArgs;
|
|
328
|
+
}
|
|
329
|
+
const memMax = this.parseMemory(this.config.memory.max);
|
|
330
|
+
const javaVersion = this.options.javaVersion || 'auto';
|
|
331
|
+
let gcArgs = [];
|
|
332
|
+
if (memMax >= 16384) {
|
|
333
|
+
gcArgs = [
|
|
334
|
+
'-XX:+UseG1GC',
|
|
335
|
+
'-XX:+ParallelRefProcEnabled',
|
|
336
|
+
'-XX:MaxGCPauseMillis=100',
|
|
337
|
+
'-XX:+UnlockExperimentalVMOptions',
|
|
338
|
+
'-XX:+DisableExplicitGC',
|
|
339
|
+
'-XX:+AlwaysPreTouch',
|
|
340
|
+
'-XX:G1NewSizePercent=40',
|
|
341
|
+
'-XX:G1MaxNewSizePercent=50',
|
|
342
|
+
'-XX:G1HeapRegionSize=16M',
|
|
343
|
+
'-XX:G1ReservePercent=15',
|
|
344
|
+
'-XX:G1HeapWastePercent=5',
|
|
345
|
+
'-XX:G1MixedGCCountTarget=4',
|
|
346
|
+
'-XX:InitiatingHeapOccupancyPercent=20',
|
|
347
|
+
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
348
|
+
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
349
|
+
'-XX:SurvivorRatio=32',
|
|
350
|
+
'-XX:+PerfDisableSharedMem',
|
|
351
|
+
'-XX:MaxTenuringThreshold=1'
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
else if (memMax >= 8192) {
|
|
355
|
+
gcArgs = [
|
|
356
|
+
'-XX:+UseG1GC',
|
|
357
|
+
'-XX:+ParallelRefProcEnabled',
|
|
358
|
+
'-XX:MaxGCPauseMillis=150',
|
|
359
|
+
'-XX:+UnlockExperimentalVMOptions',
|
|
360
|
+
'-XX:+DisableExplicitGC',
|
|
361
|
+
'-XX:+AlwaysPreTouch',
|
|
362
|
+
'-XX:G1NewSizePercent=30',
|
|
363
|
+
'-XX:G1MaxNewSizePercent=40',
|
|
364
|
+
'-XX:G1HeapRegionSize=8M',
|
|
365
|
+
'-XX:G1ReservePercent=10',
|
|
366
|
+
'-XX:G1HeapWastePercent=5',
|
|
367
|
+
'-XX:G1MixedGCCountTarget=4',
|
|
368
|
+
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
369
|
+
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
370
|
+
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
371
|
+
'-XX:SurvivorRatio=32',
|
|
372
|
+
'-XX:+PerfDisableSharedMem',
|
|
373
|
+
'-XX:MaxTenuringThreshold=1'
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
gcArgs = this.config.memory.useAikarsFlags ? [
|
|
378
|
+
'-XX:+UseG1GC',
|
|
379
|
+
'-XX:+ParallelRefProcEnabled',
|
|
380
|
+
'-XX:MaxGCPauseMillis=200',
|
|
381
|
+
'-XX:+UnlockExperimentalVMOptions',
|
|
382
|
+
'-XX:+DisableExplicitGC',
|
|
383
|
+
'-XX:+AlwaysPreTouch',
|
|
384
|
+
'-XX:G1HeapWastePercent=5',
|
|
385
|
+
'-XX:G1MixedGCCountTarget=4',
|
|
386
|
+
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
387
|
+
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
388
|
+
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
389
|
+
'-XX:SurvivorRatio=32',
|
|
390
|
+
'-XX:+PerfDisableSharedMem',
|
|
391
|
+
'-XX:MaxTenuringThreshold=1',
|
|
392
|
+
'-Dusing.aikars.flags=https://mcflags.emc.gs',
|
|
393
|
+
'-Daikars.new.flags=true'
|
|
394
|
+
] : [];
|
|
395
|
+
}
|
|
396
|
+
if (javaVersion === '21') {
|
|
397
|
+
gcArgs.push('--enable-preview');
|
|
398
|
+
}
|
|
399
|
+
const baseArgs = [
|
|
400
|
+
`-Xms${this.config.memory.init}`,
|
|
401
|
+
`-Xmx${this.config.memory.max}`
|
|
402
|
+
];
|
|
403
|
+
return [...baseArgs, ...gcArgs];
|
|
404
|
+
}
|
|
405
|
+
buildEnvironment() {
|
|
406
|
+
const env = { ...process.env };
|
|
407
|
+
env.MALLOC_ARENA_MAX = '2';
|
|
408
|
+
env._JAVA_OPTIONS = `-Xmx${this.config.memory.max}`;
|
|
409
|
+
if (this.javaInfo && this.javaInfo.type === 'portable') {
|
|
410
|
+
env.JAVA_HOME = path.dirname(path.dirname(this.javaInfo.path));
|
|
411
|
+
env.PATH = `${path.dirname(this.javaInfo.path)}:${env.PATH}`;
|
|
412
|
+
}
|
|
413
|
+
if (this.cgroupMemory > 0) {
|
|
414
|
+
const memLimit = Math.min(this.parseMemory(this.config.memory.max), this.cgroupMemory);
|
|
415
|
+
env._JAVA_OPTIONS += ` -XX:MaxRAM=${memLimit}M`;
|
|
416
|
+
}
|
|
417
|
+
if (this.cgroupCpu > 0) {
|
|
418
|
+
env._JAVA_OPTIONS += ` -XX:ActiveProcessorCount=${Math.floor(this.cgroupCpu)}`;
|
|
419
|
+
}
|
|
420
|
+
return env;
|
|
421
|
+
}
|
|
422
|
+
processOwnerCommand(player, command) {
|
|
423
|
+
if (!this.options.ownerCommands?.enabled)
|
|
424
|
+
return;
|
|
425
|
+
if (!this.owners.has(player.toLowerCase()))
|
|
426
|
+
return;
|
|
427
|
+
const cmd = command.toLowerCase().trim();
|
|
428
|
+
const args = cmd.split(' ');
|
|
429
|
+
switch (args[0]) {
|
|
430
|
+
case 'gamemode':
|
|
431
|
+
case 'gm':
|
|
432
|
+
this.handleGamemodeCommand(player, args);
|
|
433
|
+
break;
|
|
434
|
+
case 'tp':
|
|
435
|
+
case 'teleport':
|
|
436
|
+
this.handleTeleportCommand(player, args);
|
|
437
|
+
break;
|
|
438
|
+
case 'give':
|
|
439
|
+
this.handleGiveCommand(player, args);
|
|
440
|
+
break;
|
|
441
|
+
case 'time':
|
|
442
|
+
this.handleTimeCommand(player, args);
|
|
443
|
+
break;
|
|
444
|
+
case 'weather':
|
|
445
|
+
this.handleWeatherCommand(player, args);
|
|
446
|
+
break;
|
|
447
|
+
case 'kill':
|
|
448
|
+
this.handleKillCommand(player, args);
|
|
449
|
+
break;
|
|
450
|
+
case 'ban':
|
|
451
|
+
this.handleBanCommand(player, args);
|
|
452
|
+
break;
|
|
453
|
+
case 'kick':
|
|
454
|
+
this.handleKickCommand(player, args);
|
|
455
|
+
break;
|
|
456
|
+
case 'op':
|
|
457
|
+
this.handleOpCommand(player, args);
|
|
458
|
+
break;
|
|
459
|
+
case 'deop':
|
|
460
|
+
this.handleDeopCommand(player, args);
|
|
461
|
+
break;
|
|
462
|
+
case 'reload':
|
|
463
|
+
this.sendCommand('reload');
|
|
464
|
+
this.logger.info(`${player} reloaded the server`);
|
|
465
|
+
break;
|
|
466
|
+
case 'save':
|
|
467
|
+
this.sendCommand('save-all');
|
|
468
|
+
this.logger.info(`${player} saved the world`);
|
|
469
|
+
break;
|
|
470
|
+
case 'list':
|
|
471
|
+
this.sendCommand('list');
|
|
472
|
+
break;
|
|
473
|
+
case 'help':
|
|
474
|
+
this.sendOwnerHelp(player);
|
|
475
|
+
break;
|
|
476
|
+
default:
|
|
477
|
+
this.sendCommand(command);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
handleGamemodeCommand(player, args) {
|
|
481
|
+
if (args.length < 2) {
|
|
482
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}gamemode <survival|creative|adventure|spectator> [player]","color":"red"}`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const gamemode = args[1];
|
|
486
|
+
const target = args.length > 2 ? args[2] : player;
|
|
487
|
+
let gamemodeNum = 0;
|
|
488
|
+
switch (gamemode) {
|
|
489
|
+
case 'survival':
|
|
490
|
+
case '0':
|
|
491
|
+
gamemodeNum = 0;
|
|
492
|
+
break;
|
|
493
|
+
case 'creative':
|
|
494
|
+
case '1':
|
|
495
|
+
gamemodeNum = 1;
|
|
496
|
+
break;
|
|
497
|
+
case 'adventure':
|
|
498
|
+
case '2':
|
|
499
|
+
gamemodeNum = 2;
|
|
500
|
+
break;
|
|
501
|
+
case 'spectator':
|
|
502
|
+
case '3':
|
|
503
|
+
gamemodeNum = 3;
|
|
504
|
+
break;
|
|
505
|
+
default:
|
|
506
|
+
this.sendCommand(`tellraw ${player} {"text":"Invalid gamemode. Use: survival, creative, adventure, spectator","color":"red"}`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.sendCommand(`gamemode ${gamemodeNum} ${target}`);
|
|
510
|
+
this.logger.info(`${player} set gamemode of ${target} to ${gamemode}`);
|
|
511
|
+
}
|
|
512
|
+
handleTeleportCommand(player, args) {
|
|
513
|
+
if (args.length < 2) {
|
|
514
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}tp <player> [x y z]","color":"red"}`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const target = args[1];
|
|
518
|
+
if (args.length >= 4) {
|
|
519
|
+
const x = args[2];
|
|
520
|
+
const y = args[3];
|
|
521
|
+
const z = args[4] || '0';
|
|
522
|
+
this.sendCommand(`tp ${target} ${x} ${y} ${z}`);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
this.sendCommand(`tp ${player} ${target}`);
|
|
526
|
+
}
|
|
527
|
+
this.logger.info(`${player} teleported to ${target}`);
|
|
528
|
+
}
|
|
529
|
+
handleGiveCommand(player, args) {
|
|
530
|
+
if (args.length < 3) {
|
|
531
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}give <player> <item> [amount]","color":"red"}`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const target = args[1];
|
|
535
|
+
const item = args[2];
|
|
536
|
+
const amount = args.length > 3 ? args[3] : '1';
|
|
537
|
+
this.sendCommand(`give ${target} ${item} ${amount}`);
|
|
538
|
+
this.logger.info(`${player} gave ${amount} x ${item} to ${target}`);
|
|
539
|
+
}
|
|
540
|
+
handleTimeCommand(player, args) {
|
|
541
|
+
if (args.length < 2) {
|
|
542
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}time <set|add|query> <value>","color":"red"}`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const subCmd = args[1];
|
|
546
|
+
const value = args.length > 2 ? args[2] : '';
|
|
547
|
+
if (subCmd === 'set') {
|
|
548
|
+
if (value === 'day') {
|
|
549
|
+
this.sendCommand('time set day');
|
|
550
|
+
}
|
|
551
|
+
else if (value === 'night') {
|
|
552
|
+
this.sendCommand('time set night');
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
this.sendCommand(`time set ${value}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else if (subCmd === 'add') {
|
|
559
|
+
this.sendCommand(`time add ${value}`);
|
|
560
|
+
}
|
|
561
|
+
else if (subCmd === 'query') {
|
|
562
|
+
this.sendCommand('time query daytime');
|
|
563
|
+
}
|
|
564
|
+
this.logger.info(`${player} changed time: ${subCmd} ${value}`);
|
|
565
|
+
}
|
|
566
|
+
handleWeatherCommand(player, args) {
|
|
567
|
+
if (args.length < 2) {
|
|
568
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}weather <clear|rain|thunder> [duration]","color":"red"}`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const weather = args[1];
|
|
572
|
+
const duration = args.length > 2 ? args[2] : '';
|
|
573
|
+
if (weather === 'clear') {
|
|
574
|
+
this.sendCommand('weather clear');
|
|
575
|
+
}
|
|
576
|
+
else if (weather === 'rain') {
|
|
577
|
+
this.sendCommand('weather rain');
|
|
578
|
+
}
|
|
579
|
+
else if (weather === 'thunder') {
|
|
580
|
+
this.sendCommand('weather thunder');
|
|
581
|
+
}
|
|
582
|
+
if (duration) {
|
|
583
|
+
this.sendCommand(`weather ${weather} ${duration}`);
|
|
584
|
+
}
|
|
585
|
+
this.logger.info(`${player} changed weather to ${weather}`);
|
|
586
|
+
}
|
|
587
|
+
handleKillCommand(player, args) {
|
|
588
|
+
const target = args.length > 1 ? args[1] : player;
|
|
589
|
+
this.sendCommand(`kill ${target}`);
|
|
590
|
+
this.logger.info(`${player} killed ${target}`);
|
|
591
|
+
}
|
|
592
|
+
handleBanCommand(player, args) {
|
|
593
|
+
if (args.length < 2) {
|
|
594
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}ban <player> [reason]","color":"red"}`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const target = args[1];
|
|
598
|
+
const reason = args.slice(2).join(' ') || 'Banned by owner';
|
|
599
|
+
this.sendCommand(`ban ${target} ${reason}`);
|
|
600
|
+
this.logger.info(`${player} banned ${target}: ${reason}`);
|
|
601
|
+
}
|
|
602
|
+
handleKickCommand(player, args) {
|
|
603
|
+
if (args.length < 2) {
|
|
604
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}kick <player> [reason]","color":"red"}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const target = args[1];
|
|
608
|
+
const reason = args.slice(2).join(' ') || 'Kicked by owner';
|
|
609
|
+
this.sendCommand(`kick ${target} ${reason}`);
|
|
610
|
+
this.logger.info(`${player} kicked ${target}: ${reason}`);
|
|
611
|
+
}
|
|
612
|
+
handleOpCommand(player, args) {
|
|
613
|
+
if (args.length < 2) {
|
|
614
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}op <player>","color":"red"}`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const target = args[1];
|
|
618
|
+
this.sendCommand(`op ${target}`);
|
|
619
|
+
this.logger.info(`${player} opped ${target}`);
|
|
620
|
+
}
|
|
621
|
+
handleDeopCommand(player, args) {
|
|
622
|
+
if (args.length < 2) {
|
|
623
|
+
this.sendCommand(`tellraw ${player} {"text":"Usage: ${this.ownerCommandPrefix}deop <player>","color":"red"}`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const target = args[1];
|
|
627
|
+
this.sendCommand(`deop ${target}`);
|
|
628
|
+
this.logger.info(`${player} deopped ${target}`);
|
|
629
|
+
}
|
|
630
|
+
sendOwnerHelp(player) {
|
|
631
|
+
const commands = [
|
|
632
|
+
`{"text":"\\n=== Owner Commands ===\\n","color":"gold","bold":true}`,
|
|
633
|
+
`{"text":"${this.ownerCommandPrefix}gamemode <mode> [player] - Change gamemode\\n","color":"yellow"}`,
|
|
634
|
+
`{"text":"${this.ownerCommandPrefix}tp <player> [x y z] - Teleport\\n","color":"yellow"}`,
|
|
635
|
+
`{"text":"${this.ownerCommandPrefix}give <player> <item> [amount] - Give items\\n","color":"yellow"}`,
|
|
636
|
+
`{"text":"${this.ownerCommandPrefix}time <set|add> <value> - Change time\\n","color":"yellow"}`,
|
|
637
|
+
`{"text":"${this.ownerCommandPrefix}weather <clear|rain|thunder> - Change weather\\n","color":"yellow"}`,
|
|
638
|
+
`{"text":"${this.ownerCommandPrefix}kill [player] - Kill player\\n","color":"yellow"}`,
|
|
639
|
+
`{"text":"${this.ownerCommandPrefix}ban <player> [reason] - Ban player\\n","color":"yellow"}`,
|
|
640
|
+
`{"text":"${this.ownerCommandPrefix}kick <player> [reason] - Kick player\\n","color":"yellow"}`,
|
|
641
|
+
`{"text":"${this.ownerCommandPrefix}op <player> - Give operator\\n","color":"yellow"}`,
|
|
642
|
+
`{"text":"${this.ownerCommandPrefix}deop <player> - Remove operator\\n","color":"yellow"}`,
|
|
643
|
+
`{"text":"${this.ownerCommandPrefix}reload - Reload server\\n","color":"yellow"}`,
|
|
644
|
+
`{"text":"${this.ownerCommandPrefix}save - Save world\\n","color":"yellow"}`,
|
|
645
|
+
`{"text":"${this.ownerCommandPrefix}list - List players\\n","color":"yellow"}`,
|
|
646
|
+
`{"text":"${this.ownerCommandPrefix}help - Show this help\\n","color":"yellow"}`
|
|
647
|
+
];
|
|
648
|
+
commands.forEach(cmd => {
|
|
649
|
+
this.sendCommand(`tellraw ${player} ${cmd}`);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
async updateStats() {
|
|
653
|
+
if (!this.process || this.serverInfo.status !== 'running')
|
|
654
|
+
return;
|
|
655
|
+
try {
|
|
656
|
+
const memMax = this.parseMemory(this.config.memory.max);
|
|
657
|
+
if (fs.existsSync('/sys/fs/cgroup/memory/memory.usage_in_bytes')) {
|
|
658
|
+
const usage = parseInt(fs.readFileSync('/sys/fs/cgroup/memory/memory.usage_in_bytes', 'utf8'));
|
|
659
|
+
this.serverInfo.memory.used = Math.round(usage / 1024 / 1024);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
const stats = await Promise.resolve().then(() => __importStar(require('pidusage')));
|
|
663
|
+
const usage = await stats.default(this.process.pid);
|
|
664
|
+
this.serverInfo.memory.used = Math.round(usage.memory / 1024 / 1024);
|
|
665
|
+
}
|
|
666
|
+
this.serverInfo.memory.max = memMax;
|
|
667
|
+
if (fs.existsSync('/sys/fs/cgroup/cpuacct/cpuacct.usage')) {
|
|
668
|
+
const cpuTotal = parseInt(fs.readFileSync('/sys/fs/cgroup/cpuacct/cpuacct.usage', 'utf8'));
|
|
669
|
+
const now = Date.now();
|
|
670
|
+
if (this.lastCpuTotal > 0) {
|
|
671
|
+
const cpuDiff = cpuTotal - this.lastCpuTotal;
|
|
672
|
+
const timeDiff = now - this.lastCpuTime;
|
|
673
|
+
this.cpuUsage = (cpuDiff / timeDiff / 1e6) * 100;
|
|
674
|
+
if (this.cgroupCpu > 0) {
|
|
675
|
+
this.cpuUsage = this.cpuUsage / this.cgroupCpu;
|
|
676
|
+
}
|
|
677
|
+
this.serverInfo.cpu = Math.min(100, Math.max(0, Math.round(this.cpuUsage)));
|
|
678
|
+
}
|
|
679
|
+
this.lastCpuTotal = cpuTotal;
|
|
680
|
+
this.lastCpuTime = now;
|
|
681
|
+
}
|
|
682
|
+
this.serverInfo.uptime = Math.floor((Date.now() - (this.startTime?.getTime() || 0)) / 1000);
|
|
683
|
+
this.serverInfo.players = this.players.size;
|
|
684
|
+
this.emit('resource', this.serverInfo);
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
this.logger.error('Stats update error:', error);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
109
690
|
async start() {
|
|
110
691
|
this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
|
|
111
|
-
|
|
692
|
+
if (this.owners.size > 0) {
|
|
693
|
+
this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
|
|
694
|
+
}
|
|
695
|
+
await this.ensureJava();
|
|
112
696
|
const systemInfo = SystemDetector_1.SystemDetector.getSystemInfo();
|
|
113
697
|
this.logger.debug('System info:', systemInfo);
|
|
114
698
|
const serverDir = process.cwd();
|
|
115
699
|
await FileUtils_1.FileUtils.ensureServerStructure(this.config);
|
|
700
|
+
this.worldSize = await this.calculateWorldSize();
|
|
701
|
+
if (this.worldSize > 10 * 1024 * 1024 * 1024) {
|
|
702
|
+
this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
|
|
703
|
+
}
|
|
116
704
|
const jarPath = await this.engine.download(this.config, serverDir);
|
|
117
705
|
if (this.config.type === 'forge') {
|
|
118
706
|
await this.engine.prepare(this.config, serverDir, jarPath);
|
|
@@ -136,7 +724,12 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
136
724
|
this.logger.info('ViaRewind will be installed');
|
|
137
725
|
}
|
|
138
726
|
}
|
|
139
|
-
|
|
727
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
728
|
+
this.logger.info('Enabling SkinRestorer for player skins...');
|
|
729
|
+
await FileUtils_1.FileUtils.ensureDir(this.config.folders.plugins);
|
|
730
|
+
await this.skinRestorer.setup(this.config);
|
|
731
|
+
}
|
|
732
|
+
const javaArgs = this.buildJavaArgs();
|
|
140
733
|
const serverJar = this.engine.getServerJar(jarPath);
|
|
141
734
|
const serverArgs = this.engine.getServerArgs();
|
|
142
735
|
const fullArgs = [
|
|
@@ -145,44 +738,63 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
145
738
|
serverJar,
|
|
146
739
|
...serverArgs
|
|
147
740
|
];
|
|
148
|
-
this.
|
|
149
|
-
|
|
741
|
+
if (this.options.networkOptimization?.tcpFastOpen) {
|
|
742
|
+
fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
|
|
743
|
+
}
|
|
744
|
+
if (this.options.networkOptimization?.bungeeMode) {
|
|
745
|
+
fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
|
|
746
|
+
}
|
|
747
|
+
const env = this.buildEnvironment();
|
|
748
|
+
this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
|
|
749
|
+
this.process = (0, child_process_1.spawn)(this.javaCommand, fullArgs, {
|
|
150
750
|
cwd: serverDir,
|
|
151
|
-
|
|
751
|
+
env: env,
|
|
752
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
152
753
|
});
|
|
153
754
|
this.serverInfo.pid = this.process.pid;
|
|
154
755
|
this.serverInfo.status = 'starting';
|
|
155
756
|
this.startTime = new Date();
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
process.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
757
|
+
if (this.options.silentMode) {
|
|
758
|
+
this.process.stdout.pipe(process.stdout);
|
|
759
|
+
this.process.stderr.pipe(process.stderr);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
this.process.stdout.on('data', (data) => {
|
|
763
|
+
const output = data.toString();
|
|
764
|
+
process.stdout.write(output);
|
|
765
|
+
if (output.includes('joined the game')) {
|
|
766
|
+
const match = output.match(/(\w+) joined the game/);
|
|
767
|
+
if (match) {
|
|
768
|
+
this.playerCount++;
|
|
769
|
+
this.handlePlayerJoin(match[1]);
|
|
770
|
+
if (this.owners.has(match[1].toLowerCase())) {
|
|
771
|
+
this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
164
774
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
775
|
+
if (output.includes('left the game')) {
|
|
776
|
+
const match = output.match(/(\w+) left the game/);
|
|
777
|
+
if (match) {
|
|
778
|
+
this.playerCount--;
|
|
779
|
+
this.handlePlayerLeave(match[1]);
|
|
780
|
+
}
|
|
171
781
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
782
|
+
if (output.includes('<') && output.includes('>')) {
|
|
783
|
+
const chatMatch = output.match(/<(\w+)>\s+(.+)/);
|
|
784
|
+
if (chatMatch) {
|
|
785
|
+
const player = chatMatch[1];
|
|
786
|
+
const message = chatMatch[2];
|
|
787
|
+
if (message.startsWith(this.ownerCommandPrefix)) {
|
|
788
|
+
const command = message.substring(this.ownerCommandPrefix.length);
|
|
789
|
+
this.processOwnerCommand(player, command);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
177
792
|
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
this.process.stderr.on('data', (data) => {
|
|
184
|
-
process.stderr.write(data.toString());
|
|
185
|
-
});
|
|
793
|
+
});
|
|
794
|
+
this.process.stderr.on('data', (data) => {
|
|
795
|
+
process.stderr.write(data.toString());
|
|
796
|
+
});
|
|
797
|
+
}
|
|
186
798
|
this.process.on('exit', (code) => {
|
|
187
799
|
this.serverInfo.status = 'stopped';
|
|
188
800
|
this.logger.warning(`Server stopped with code ${code}`);
|
|
@@ -192,12 +804,94 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
192
804
|
}
|
|
193
805
|
this.emit('stop', { code });
|
|
194
806
|
});
|
|
807
|
+
if (this.options.statsInterval && this.options.statsInterval > 0) {
|
|
808
|
+
this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
|
|
809
|
+
}
|
|
195
810
|
this.monitorResources();
|
|
196
811
|
if (this.config.backup.enabled) {
|
|
197
812
|
this.setupBackups();
|
|
198
813
|
}
|
|
814
|
+
setTimeout(() => {
|
|
815
|
+
if (this.serverInfo.status === 'starting') {
|
|
816
|
+
this.serverInfo.status = 'running';
|
|
817
|
+
this.logger.success('Server started successfully!');
|
|
818
|
+
if (this.options.enableViaVersion !== false) {
|
|
819
|
+
this.logger.info('ViaVersion is active - players from older versions can connect');
|
|
820
|
+
}
|
|
821
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
822
|
+
this.logger.info('SkinRestorer is active - player skins will be restored');
|
|
823
|
+
}
|
|
824
|
+
if (this.worldSize > 0) {
|
|
825
|
+
this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
826
|
+
}
|
|
827
|
+
if (this.owners.size > 0) {
|
|
828
|
+
this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
|
|
829
|
+
}
|
|
830
|
+
this.emit('ready', this.serverInfo);
|
|
831
|
+
this.startMemoryMonitor();
|
|
832
|
+
}
|
|
833
|
+
}, 10000);
|
|
199
834
|
return this.serverInfo;
|
|
200
835
|
}
|
|
836
|
+
startMemoryMonitor() {
|
|
837
|
+
if (!this.options.memoryMonitor?.enabled)
|
|
838
|
+
return;
|
|
839
|
+
const threshold = this.options.memoryMonitor.threshold || 90;
|
|
840
|
+
const interval = this.options.memoryMonitor.interval || 30000;
|
|
841
|
+
const action = this.options.memoryMonitor.action || 'warn';
|
|
842
|
+
this.memoryMonitorInterval = setInterval(async () => {
|
|
843
|
+
if (this.serverInfo.status !== 'running' || !this.process)
|
|
844
|
+
return;
|
|
845
|
+
try {
|
|
846
|
+
await this.updateStats();
|
|
847
|
+
const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
|
|
848
|
+
this.memoryUsageHistory.push(memPercent);
|
|
849
|
+
if (this.memoryUsageHistory.length > 10) {
|
|
850
|
+
this.memoryUsageHistory.shift();
|
|
851
|
+
}
|
|
852
|
+
if (memPercent > threshold) {
|
|
853
|
+
this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
|
|
854
|
+
const isIncreasing = this.memoryUsageHistory.length > 5 &&
|
|
855
|
+
this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
|
|
856
|
+
this.memoryUsageHistory[0] * 1.2;
|
|
857
|
+
if (isIncreasing) {
|
|
858
|
+
this.logger.warning('Memory leak detected!');
|
|
859
|
+
switch (action) {
|
|
860
|
+
case 'restart':
|
|
861
|
+
this.logger.info('Restarting server due to memory leak...');
|
|
862
|
+
await this.gracefulRestart();
|
|
863
|
+
break;
|
|
864
|
+
case 'stop':
|
|
865
|
+
this.logger.info('Stopping server due to memory leak...');
|
|
866
|
+
await this.stop();
|
|
867
|
+
break;
|
|
868
|
+
case 'warn':
|
|
869
|
+
default:
|
|
870
|
+
this.logger.warning('Please restart server to free memory');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
this.logger.error('Memory monitor error:', error);
|
|
877
|
+
}
|
|
878
|
+
}, interval);
|
|
879
|
+
}
|
|
880
|
+
async gracefulRestart() {
|
|
881
|
+
this.logger.info('Initiating graceful restart...');
|
|
882
|
+
this.sendCommand('say Server restarting in 30 seconds');
|
|
883
|
+
this.sendCommand('save-all');
|
|
884
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
885
|
+
this.sendCommand('say Server restarting in 20 seconds');
|
|
886
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
887
|
+
this.sendCommand('say Server restarting in 10 seconds');
|
|
888
|
+
this.sendCommand('save-all');
|
|
889
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
890
|
+
this.sendCommand('say Server restarting in 5 seconds');
|
|
891
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
892
|
+
await this.stop();
|
|
893
|
+
await this.start();
|
|
894
|
+
}
|
|
201
895
|
async stop() {
|
|
202
896
|
if (!this.process) {
|
|
203
897
|
this.logger.warning('Server not running');
|
|
@@ -205,8 +899,9 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
205
899
|
}
|
|
206
900
|
this.logger.info('Stopping server...');
|
|
207
901
|
this.serverInfo.status = 'stopping';
|
|
902
|
+
this.sendCommand('save-all');
|
|
208
903
|
this.sendCommand('stop');
|
|
209
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
904
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
210
905
|
if (this.process) {
|
|
211
906
|
this.process.kill();
|
|
212
907
|
this.process = null;
|
|
@@ -214,6 +909,12 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
214
909
|
if (this.backupCron) {
|
|
215
910
|
this.backupCron.stop();
|
|
216
911
|
}
|
|
912
|
+
if (this.memoryMonitorInterval) {
|
|
913
|
+
clearInterval(this.memoryMonitorInterval);
|
|
914
|
+
}
|
|
915
|
+
if (this.statsInterval) {
|
|
916
|
+
clearInterval(this.statsInterval);
|
|
917
|
+
}
|
|
217
918
|
if (this.config.platform === 'all') {
|
|
218
919
|
this.geyser.stop();
|
|
219
920
|
}
|
|
@@ -227,17 +928,7 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
227
928
|
this.logger.debug(`Command sent: ${command}`);
|
|
228
929
|
}
|
|
229
930
|
async getInfo() {
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
const stats = await (0, pidusage_1.default)(this.process.pid);
|
|
233
|
-
this.serverInfo.memory = {
|
|
234
|
-
used: Math.round(stats.memory / 1024 / 1024),
|
|
235
|
-
max: this.parseMemory(this.config.memory.max)
|
|
236
|
-
};
|
|
237
|
-
this.serverInfo.uptime = Math.floor((Date.now() - (this.startTime?.getTime() || 0)) / 1000);
|
|
238
|
-
}
|
|
239
|
-
catch { }
|
|
240
|
-
}
|
|
931
|
+
await this.updateStats();
|
|
241
932
|
return this.serverInfo;
|
|
242
933
|
}
|
|
243
934
|
getPlayers() {
|
|
@@ -266,20 +957,11 @@ class MinecraftServer extends events_1.EventEmitter {
|
|
|
266
957
|
setInterval(async () => {
|
|
267
958
|
if (this.serverInfo.status === 'running' && this.process) {
|
|
268
959
|
try {
|
|
269
|
-
|
|
270
|
-
this.serverInfo.memory = {
|
|
271
|
-
used: Math.round(stats.memory / 1024 / 1024),
|
|
272
|
-
max: this.parseMemory(this.config.memory.max)
|
|
273
|
-
};
|
|
274
|
-
this.serverInfo.uptime = Math.floor((Date.now() - (this.startTime?.getTime() || 0)) / 1000);
|
|
275
|
-
if (stats.cpu > 80) {
|
|
276
|
-
this.logger.warning(`High CPU usage: ${stats.cpu}%`);
|
|
277
|
-
}
|
|
278
|
-
this.emit('resource', this.serverInfo);
|
|
960
|
+
await this.updateStats();
|
|
279
961
|
}
|
|
280
962
|
catch { }
|
|
281
963
|
}
|
|
282
|
-
},
|
|
964
|
+
}, 30000);
|
|
283
965
|
}
|
|
284
966
|
setupBackups() {
|
|
285
967
|
const cronExpression = this.convertIntervalToCron(this.config.backup.interval);
|