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