@dimzxzzx07/mc-headless 2.2.1 → 2.2.3
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 +658 -765
- package/dist/core/JavaChecker.d.ts +8 -2
- package/dist/core/JavaChecker.d.ts.map +1 -1
- package/dist/core/JavaChecker.js +219 -104
- package/dist/core/JavaChecker.js.map +1 -1
- package/dist/core/MinecraftServer.d.ts +17 -32
- package/dist/core/MinecraftServer.d.ts.map +1 -1
- package/dist/core/MinecraftServer.js +449 -186
- package/dist/core/MinecraftServer.js.map +1 -1
- package/dist/index.d.ts +19 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -18
- package/dist/index.js.map +1 -1
- package/dist/platforms/GeyserBridge.d.ts +21 -3
- package/dist/platforms/GeyserBridge.d.ts.map +1 -1
- package/dist/platforms/GeyserBridge.js +160 -60
- package/dist/platforms/GeyserBridge.js.map +1 -1
- package/dist/platforms/PluginManager.d.ts +16 -0
- package/dist/platforms/PluginManager.d.ts.map +1 -0
- package/dist/platforms/PluginManager.js +208 -0
- package/dist/platforms/PluginManager.js.map +1 -0
- package/dist/platforms/ViaVersion.d.ts +1 -7
- package/dist/platforms/ViaVersion.d.ts.map +1 -1
- package/dist/platforms/ViaVersion.js +23 -87
- package/dist/platforms/ViaVersion.js.map +1 -1
- package/dist/types/index.d.ts +189 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/core/JavaChecker.ts +224 -108
- package/src/core/MinecraftServer.ts +540 -254
- package/src/index.ts +19 -19
- package/src/platforms/GeyserBridge.ts +196 -80
- package/src/platforms/PluginManager.ts +206 -0
- package/src/platforms/ViaVersion.ts +26 -107
- package/src/types/index.ts +206 -0
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import * as cron from 'node-cron';
|
|
4
|
-
import
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs-extra';
|
|
6
|
+
import {
|
|
7
|
+
MinecraftConfig,
|
|
8
|
+
ServerInfo,
|
|
9
|
+
Player,
|
|
10
|
+
MinecraftServerOptions,
|
|
11
|
+
MemoryMonitorConfig,
|
|
12
|
+
NetworkOptimizationConfig,
|
|
13
|
+
OwnerConfig,
|
|
14
|
+
RamdiskConfig
|
|
15
|
+
} from '../types';
|
|
5
16
|
import { ConfigHandler } from './ConfigHandler';
|
|
6
17
|
import { JavaChecker, JavaInfo } from './JavaChecker';
|
|
7
18
|
import { FileUtils } from '../utils/FileUtils';
|
|
@@ -15,38 +26,7 @@ import { ServerEngine } from '../engines/ServerEngine';
|
|
|
15
26
|
import { GeyserBridge } from '../platforms/GeyserBridge';
|
|
16
27
|
import { ViaVersionManager } from '../platforms/ViaVersion';
|
|
17
28
|
import { SkinRestorerManager } from '../platforms/SkinRestorer';
|
|
18
|
-
import
|
|
19
|
-
import * as fs from 'fs-extra';
|
|
20
|
-
|
|
21
|
-
export interface MinecraftServerOptions extends Partial<MinecraftConfig> {
|
|
22
|
-
enableViaVersion?: boolean;
|
|
23
|
-
enableViaBackwards?: boolean;
|
|
24
|
-
enableViaRewind?: boolean;
|
|
25
|
-
enableSkinRestorer?: boolean;
|
|
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;
|
|
49
|
-
}
|
|
29
|
+
import { PluginManager } from '../platforms/PluginManager';
|
|
50
30
|
|
|
51
31
|
export class MinecraftServer extends EventEmitter {
|
|
52
32
|
private config: MinecraftConfig;
|
|
@@ -56,10 +36,13 @@ export class MinecraftServer extends EventEmitter {
|
|
|
56
36
|
private geyser: GeyserBridge;
|
|
57
37
|
private viaVersion: ViaVersionManager;
|
|
58
38
|
private skinRestorer: SkinRestorerManager;
|
|
39
|
+
private pluginManager: PluginManager;
|
|
59
40
|
private process: any = null;
|
|
60
41
|
private serverInfo: ServerInfo;
|
|
61
42
|
private players: Map<string, Player> = new Map();
|
|
62
43
|
private backupCron: cron.ScheduledTask | null = null;
|
|
44
|
+
private cleanLogsCron: cron.ScheduledTask | null = null;
|
|
45
|
+
private ramdiskBackupCron: cron.ScheduledTask | null = null;
|
|
63
46
|
private startTime: Date | null = null;
|
|
64
47
|
private memoryMonitorInterval: NodeJS.Timeout | null = null;
|
|
65
48
|
private statsInterval: NodeJS.Timeout | null = null;
|
|
@@ -75,39 +58,84 @@ export class MinecraftServer extends EventEmitter {
|
|
|
75
58
|
private cpuUsage: number = 0;
|
|
76
59
|
private cgroupMemory: number = 0;
|
|
77
60
|
private cgroupCpu: number = 0;
|
|
61
|
+
private isPterodactyl: boolean = false;
|
|
62
|
+
private ramdiskPaths: Map<string, string> = new Map();
|
|
63
|
+
private serverReady: boolean = false;
|
|
78
64
|
|
|
79
65
|
constructor(userConfig: MinecraftServerOptions = {}) {
|
|
80
66
|
super();
|
|
81
67
|
this.logger = Logger.getInstance();
|
|
82
68
|
this.logger.banner();
|
|
83
69
|
|
|
70
|
+
this.detectEnvironment();
|
|
71
|
+
|
|
72
|
+
const defaultMemoryMonitor: MemoryMonitorConfig = {
|
|
73
|
+
enabled: true,
|
|
74
|
+
threshold: 90,
|
|
75
|
+
interval: 30000,
|
|
76
|
+
action: 'warn',
|
|
77
|
+
maxMemoryLeakRestarts: 3,
|
|
78
|
+
gcOnWarning: true
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const defaultNetworkOptimization: NetworkOptimizationConfig = {
|
|
82
|
+
tcpFastOpen: true,
|
|
83
|
+
tcpNoDelay: true,
|
|
84
|
+
preferIPv4: true,
|
|
85
|
+
compressionThreshold: 64,
|
|
86
|
+
bungeeMode: false,
|
|
87
|
+
proxyProtocol: false,
|
|
88
|
+
velocity: {
|
|
89
|
+
enabled: false,
|
|
90
|
+
secret: '',
|
|
91
|
+
forwardingMode: 'modern'
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const defaultOwnerCommands: OwnerConfig = {
|
|
96
|
+
prefix: '!',
|
|
97
|
+
enabled: true,
|
|
98
|
+
usernames: [],
|
|
99
|
+
permissions: []
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const defaultRamdisk: RamdiskConfig = {
|
|
103
|
+
enabled: false,
|
|
104
|
+
world: false,
|
|
105
|
+
plugins: false,
|
|
106
|
+
addons: false,
|
|
107
|
+
backupInterval: 300000,
|
|
108
|
+
masterStorage: '/home/minecraft-master',
|
|
109
|
+
size: '1G',
|
|
110
|
+
mountPoint: '/dev/shm/minecraft'
|
|
111
|
+
};
|
|
112
|
+
|
|
84
113
|
this.options = {
|
|
85
114
|
javaVersion: 'auto',
|
|
86
|
-
usePortableJava:
|
|
87
|
-
memoryMonitor:
|
|
88
|
-
enabled: true,
|
|
89
|
-
threshold: 90,
|
|
90
|
-
interval: 30000,
|
|
91
|
-
action: 'warn'
|
|
92
|
-
},
|
|
115
|
+
usePortableJava: !this.isPterodactyl,
|
|
116
|
+
memoryMonitor: defaultMemoryMonitor,
|
|
93
117
|
autoInstallJava: true,
|
|
94
|
-
networkOptimization:
|
|
95
|
-
tcpFastOpen: true,
|
|
96
|
-
bungeeMode: false,
|
|
97
|
-
proxyProtocol: false
|
|
98
|
-
},
|
|
118
|
+
networkOptimization: defaultNetworkOptimization,
|
|
99
119
|
owners: [],
|
|
100
|
-
ownerCommands:
|
|
101
|
-
prefix: '!',
|
|
102
|
-
enabled: true
|
|
103
|
-
},
|
|
120
|
+
ownerCommands: defaultOwnerCommands,
|
|
104
121
|
silentMode: true,
|
|
105
122
|
statsInterval: 30000,
|
|
123
|
+
cleanLogsInterval: 3 * 60 * 60 * 1000,
|
|
124
|
+
optimizeForPterodactyl: true,
|
|
125
|
+
ramdisk: defaultRamdisk,
|
|
126
|
+
symlinkMaster: '/home/minecraft-master',
|
|
127
|
+
enableProtocolLib: false,
|
|
128
|
+
enableTCPShield: false,
|
|
129
|
+
enableSpigotOptimizers: false,
|
|
130
|
+
enableSpark: false,
|
|
131
|
+
enableViewDistanceTweaks: false,
|
|
132
|
+
enableFarmControl: false,
|
|
133
|
+
enableEntityDetection: false,
|
|
106
134
|
...userConfig
|
|
107
135
|
};
|
|
108
136
|
|
|
109
137
|
if (this.options.owners) {
|
|
110
|
-
this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
|
|
138
|
+
this.options.owners.forEach((owner: string) => this.owners.add(owner.toLowerCase()));
|
|
111
139
|
}
|
|
112
140
|
|
|
113
141
|
if (this.options.ownerCommands?.prefix) {
|
|
@@ -117,10 +145,15 @@ export class MinecraftServer extends EventEmitter {
|
|
|
117
145
|
const handler = new ConfigHandler(userConfig);
|
|
118
146
|
this.config = handler.getConfig();
|
|
119
147
|
|
|
148
|
+
if (this.isPterodactyl && this.options.optimizeForPterodactyl) {
|
|
149
|
+
this.optimizeForPterodactylPanel();
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
this.engine = this.createEngine();
|
|
121
153
|
this.geyser = new GeyserBridge();
|
|
122
154
|
this.viaVersion = new ViaVersionManager();
|
|
123
155
|
this.skinRestorer = new SkinRestorerManager();
|
|
156
|
+
this.pluginManager = new PluginManager();
|
|
124
157
|
|
|
125
158
|
this.serverInfo = {
|
|
126
159
|
pid: 0,
|
|
@@ -141,6 +174,33 @@ export class MinecraftServer extends EventEmitter {
|
|
|
141
174
|
this.detectCgroupLimits();
|
|
142
175
|
}
|
|
143
176
|
|
|
177
|
+
private detectEnvironment(): void {
|
|
178
|
+
this.isPterodactyl = !!(
|
|
179
|
+
process.env.PTERODACTYL ||
|
|
180
|
+
process.env.SERVER_PORT ||
|
|
181
|
+
fs.existsSync('/home/container')
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (this.isPterodactyl) {
|
|
185
|
+
this.logger.info('Pterodactyl environment detected');
|
|
186
|
+
|
|
187
|
+
const serverPort = process.env.SERVER_PORT;
|
|
188
|
+
if (serverPort) {
|
|
189
|
+
this.config.network.port = parseInt(serverPort);
|
|
190
|
+
this.logger.info(`Using Pterodactyl Java port: ${serverPort}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const bedrockPort = process.env.BEDROCK_PORT;
|
|
194
|
+
if (bedrockPort) {
|
|
195
|
+
this.config.network.bedrockPort = parseInt(bedrockPort);
|
|
196
|
+
this.logger.info(`Using Pterodactyl Bedrock port: ${bedrockPort}`);
|
|
197
|
+
} else {
|
|
198
|
+
this.logger.warning('No BEDROCK_PORT environment variable found. Geyser will share Java port.');
|
|
199
|
+
this.logger.warning('Make sure UDP is enabled for this port in Pterodactyl panel');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
144
204
|
private detectCgroupLimits(): void {
|
|
145
205
|
try {
|
|
146
206
|
if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
|
|
@@ -162,6 +222,29 @@ export class MinecraftServer extends EventEmitter {
|
|
|
162
222
|
}
|
|
163
223
|
}
|
|
164
224
|
|
|
225
|
+
private optimizeForPterodactylPanel(): void {
|
|
226
|
+
const pterodactylPort = process.env.SERVER_PORT;
|
|
227
|
+
if (pterodactylPort) {
|
|
228
|
+
this.config.network.port = parseInt(pterodactylPort);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const pterodactylMemory = process.env.SERVER_MEMORY_MAX;
|
|
232
|
+
if (pterodactylMemory) {
|
|
233
|
+
const memValue = parseInt(pterodactylMemory);
|
|
234
|
+
const safeMemory = Math.floor(memValue * 0.9);
|
|
235
|
+
this.config.memory.max = `${safeMemory}M`;
|
|
236
|
+
this.logger.info(`Pterodactyl memory limit detected, using 90% safe value: ${safeMemory}MB`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pterodactylInitMemory = process.env.SERVER_MEMORY_INIT;
|
|
240
|
+
if (pterodactylInitMemory) {
|
|
241
|
+
this.config.memory.init = pterodactylInitMemory;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.config.world.viewDistance = 6;
|
|
245
|
+
this.config.world.simulationDistance = 4;
|
|
246
|
+
}
|
|
247
|
+
|
|
165
248
|
private createEngine(): ServerEngine {
|
|
166
249
|
switch (this.config.type) {
|
|
167
250
|
case 'paper':
|
|
@@ -179,21 +262,226 @@ export class MinecraftServer extends EventEmitter {
|
|
|
179
262
|
}
|
|
180
263
|
}
|
|
181
264
|
|
|
265
|
+
private async setupSymlinkMaster(): Promise<void> {
|
|
266
|
+
if (!this.options.symlinkMaster) return;
|
|
267
|
+
|
|
268
|
+
const masterDir = this.options.symlinkMaster;
|
|
269
|
+
await fs.ensureDir(masterDir);
|
|
270
|
+
|
|
271
|
+
const subDirs = ['worlds', 'plugins', 'addons', 'mods'];
|
|
272
|
+
for (const dir of subDirs) {
|
|
273
|
+
const masterSubDir = path.join(masterDir, dir);
|
|
274
|
+
await fs.ensureDir(masterSubDir);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.logger.info(`Symlink master directory created at ${masterDir}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async restoreFromMaster(): Promise<void> {
|
|
281
|
+
if (!this.options.ramdisk?.enabled) return;
|
|
282
|
+
|
|
283
|
+
const ramdiskBase = '/dev/shm/minecraft';
|
|
284
|
+
const items = [
|
|
285
|
+
{ name: 'world', target: this.config.folders.world },
|
|
286
|
+
{ name: 'plugins', target: this.config.folders.plugins },
|
|
287
|
+
{ name: 'addons', target: this.config.folders.addons }
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
for (const item of items) {
|
|
291
|
+
const ramdiskPath = path.join(ramdiskBase, item.name);
|
|
292
|
+
const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
|
|
293
|
+
|
|
294
|
+
if (fs.existsSync(ramdiskPath)) {
|
|
295
|
+
const ramdiskFiles = fs.readdirSync(ramdiskPath);
|
|
296
|
+
if (ramdiskFiles.length > 0) {
|
|
297
|
+
this.logger.debug(`RAM disk ${item.name} already has data, skipping restore`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (fs.existsSync(masterPath)) {
|
|
303
|
+
const masterFiles = fs.readdirSync(masterPath);
|
|
304
|
+
if (masterFiles.length > 0) {
|
|
305
|
+
this.logger.info(`Restoring ${item.name} from master storage...`);
|
|
306
|
+
await fs.ensureDir(ramdiskPath);
|
|
307
|
+
await fs.copy(masterPath, ramdiskPath, { overwrite: true });
|
|
308
|
+
|
|
309
|
+
if (!fs.existsSync(item.target)) {
|
|
310
|
+
await fs.symlink(ramdiskPath, item.target, 'dir');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async setupRamdisk(): Promise<void> {
|
|
318
|
+
if (!this.options.ramdisk?.enabled) return;
|
|
319
|
+
|
|
320
|
+
const ramdiskBase = '/dev/shm/minecraft';
|
|
321
|
+
await fs.ensureDir(ramdiskBase);
|
|
322
|
+
|
|
323
|
+
await this.restoreFromMaster();
|
|
324
|
+
|
|
325
|
+
const items = [
|
|
326
|
+
{ name: 'world', enabled: this.options.ramdisk.world, target: this.config.folders.world },
|
|
327
|
+
{ name: 'plugins', enabled: this.options.ramdisk.plugins, target: this.config.folders.plugins },
|
|
328
|
+
{ name: 'addons', enabled: this.options.ramdisk.addons, target: this.config.folders.addons }
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
for (const item of items) {
|
|
332
|
+
if (!item.enabled) continue;
|
|
333
|
+
|
|
334
|
+
const ramdiskPath = path.join(ramdiskBase, item.name);
|
|
335
|
+
const masterPath = path.join(this.options.ramdisk.masterStorage, item.name);
|
|
336
|
+
|
|
337
|
+
await fs.ensureDir(masterPath);
|
|
338
|
+
|
|
339
|
+
if (fs.existsSync(item.target)) {
|
|
340
|
+
const stat = await fs.lstat(item.target);
|
|
341
|
+
if (stat.isSymbolicLink()) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
await fs.copy(item.target, masterPath, { overwrite: true });
|
|
345
|
+
await fs.remove(item.target);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await fs.ensureDir(ramdiskPath);
|
|
349
|
+
await fs.symlink(ramdiskPath, item.target, 'dir');
|
|
350
|
+
this.ramdiskPaths.set(item.name, ramdiskPath);
|
|
351
|
+
|
|
352
|
+
this.logger.info(`RAM disk created for ${item.name} at ${ramdiskPath}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (this.options.ramdisk.backupInterval > 0) {
|
|
356
|
+
const minutes = Math.floor(this.options.ramdisk.backupInterval / 60000);
|
|
357
|
+
this.ramdiskBackupCron = cron.schedule(`*/${minutes} * * * *`, () => {
|
|
358
|
+
this.backupRamdiskToMaster();
|
|
359
|
+
});
|
|
360
|
+
this.logger.info(`RAM disk backup scheduled every ${minutes} minutes`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async backupRamdiskToMaster(): Promise<void> {
|
|
365
|
+
if (!this.options.ramdisk?.enabled) return;
|
|
366
|
+
|
|
367
|
+
for (const [name, ramdiskPath] of this.ramdiskPaths) {
|
|
368
|
+
const masterPath = path.join(this.options.ramdisk.masterStorage, name);
|
|
369
|
+
try {
|
|
370
|
+
await fs.copy(ramdiskPath, masterPath, { overwrite: true });
|
|
371
|
+
this.logger.debug(`RAM disk ${name} backed up to master storage`);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
374
|
+
this.logger.error(`Failed to backup ${name} RAM disk: ${errorMessage}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private cleanOldLogs(): void {
|
|
380
|
+
const logsDir = path.join(process.cwd(), 'logs');
|
|
381
|
+
if (!fs.existsSync(logsDir)) return;
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const files = fs.readdirSync(logsDir);
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
const threeHours = 3 * 60 * 60 * 1000;
|
|
387
|
+
|
|
388
|
+
files.forEach(file => {
|
|
389
|
+
const filePath = path.join(logsDir, file);
|
|
390
|
+
const stats = fs.statSync(filePath);
|
|
391
|
+
if (now - stats.mtimeMs > threeHours) {
|
|
392
|
+
fs.unlinkSync(filePath);
|
|
393
|
+
this.logger.debug(`Cleaned old log: ${file}`);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
this.logger.info('Old logs cleaned');
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
399
|
+
this.logger.warning(`Failed to clean logs: ${errorMessage}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private async calculateWorldSize(): Promise<number> {
|
|
404
|
+
try {
|
|
405
|
+
const worldPath = path.join(process.cwd(), this.config.folders.world);
|
|
406
|
+
if (!await fs.pathExists(worldPath)) return 0;
|
|
407
|
+
|
|
408
|
+
const getSize = async (dir: string): Promise<number> => {
|
|
409
|
+
let total = 0;
|
|
410
|
+
const files = await fs.readdir(dir);
|
|
411
|
+
|
|
412
|
+
for (const file of files) {
|
|
413
|
+
const filePath = path.join(dir, file);
|
|
414
|
+
const stat = await fs.stat(filePath);
|
|
415
|
+
|
|
416
|
+
if (stat.isDirectory()) {
|
|
417
|
+
total += await getSize(filePath);
|
|
418
|
+
} else {
|
|
419
|
+
total += stat.size;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return total;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return await getSize(worldPath);
|
|
427
|
+
} catch {
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
182
432
|
private buildJavaArgs(): string[] {
|
|
183
433
|
if (this.options.customJavaArgs && this.options.customJavaArgs.length > 0) {
|
|
184
434
|
return this.options.customJavaArgs;
|
|
185
435
|
}
|
|
186
436
|
|
|
187
|
-
|
|
437
|
+
let memMax = this.parseMemory(this.config.memory.max);
|
|
438
|
+
|
|
439
|
+
if (this.isPterodactyl && this.cgroupMemory > 0) {
|
|
440
|
+
const safeMem = Math.floor(this.cgroupMemory * 0.9);
|
|
441
|
+
memMax = Math.min(memMax, safeMem);
|
|
442
|
+
this.logger.info(`Adjusted memory to ${memMax}MB (90% of cgroup limit)`);
|
|
443
|
+
}
|
|
444
|
+
|
|
188
445
|
const javaVersion = this.options.javaVersion || 'auto';
|
|
189
446
|
|
|
447
|
+
const baseArgs = [
|
|
448
|
+
`-Xms${this.config.memory.init}`,
|
|
449
|
+
`-Xmx${memMax}M`,
|
|
450
|
+
'-XX:+UseG1GC',
|
|
451
|
+
'-XX:MaxGCPauseMillis=50',
|
|
452
|
+
'-XX:+UseStringDeduplication',
|
|
453
|
+
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
454
|
+
'-Dio.netty.recycler.maxCapacity.default=0',
|
|
455
|
+
'-Dio.netty.leakDetectionLevel=disabled',
|
|
456
|
+
'-Dterminal.jline=false',
|
|
457
|
+
'-Dterminal.ansi=true'
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
if (this.options.networkOptimization?.preferIPv4) {
|
|
461
|
+
baseArgs.unshift('-Djava.net.preferIPv4Stack=true');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (this.options.networkOptimization?.tcpFastOpen) {
|
|
465
|
+
baseArgs.unshift('-Djava.net.tcpFastOpen=true');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (this.options.networkOptimization?.tcpNoDelay) {
|
|
469
|
+
baseArgs.unshift('-Djava.net.tcp.nodelay=true');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (this.options.networkOptimization?.compressionThreshold) {
|
|
473
|
+
baseArgs.push(`-Dnetwork-compression-threshold=${this.options.networkOptimization.compressionThreshold}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (this.options.networkOptimization?.velocity?.enabled) {
|
|
477
|
+
baseArgs.push('-Dvelocity.secret=' + this.options.networkOptimization.velocity.secret);
|
|
478
|
+
}
|
|
479
|
+
|
|
190
480
|
let gcArgs: string[] = [];
|
|
191
481
|
|
|
192
482
|
if (memMax >= 16384) {
|
|
193
483
|
gcArgs = [
|
|
194
|
-
'-XX:+UseG1GC',
|
|
195
484
|
'-XX:+ParallelRefProcEnabled',
|
|
196
|
-
'-XX:MaxGCPauseMillis=100',
|
|
197
485
|
'-XX:+UnlockExperimentalVMOptions',
|
|
198
486
|
'-XX:+DisableExplicitGC',
|
|
199
487
|
'-XX:+AlwaysPreTouch',
|
|
@@ -204,7 +492,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
204
492
|
'-XX:G1HeapWastePercent=5',
|
|
205
493
|
'-XX:G1MixedGCCountTarget=4',
|
|
206
494
|
'-XX:InitiatingHeapOccupancyPercent=20',
|
|
207
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
208
495
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
209
496
|
'-XX:SurvivorRatio=32',
|
|
210
497
|
'-XX:+PerfDisableSharedMem',
|
|
@@ -212,9 +499,7 @@ export class MinecraftServer extends EventEmitter {
|
|
|
212
499
|
];
|
|
213
500
|
} else if (memMax >= 8192) {
|
|
214
501
|
gcArgs = [
|
|
215
|
-
'-XX:+UseG1GC',
|
|
216
502
|
'-XX:+ParallelRefProcEnabled',
|
|
217
|
-
'-XX:MaxGCPauseMillis=150',
|
|
218
503
|
'-XX:+UnlockExperimentalVMOptions',
|
|
219
504
|
'-XX:+DisableExplicitGC',
|
|
220
505
|
'-XX:+AlwaysPreTouch',
|
|
@@ -225,42 +510,36 @@ export class MinecraftServer extends EventEmitter {
|
|
|
225
510
|
'-XX:G1HeapWastePercent=5',
|
|
226
511
|
'-XX:G1MixedGCCountTarget=4',
|
|
227
512
|
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
228
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
229
513
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
230
514
|
'-XX:SurvivorRatio=32',
|
|
231
515
|
'-XX:+PerfDisableSharedMem',
|
|
232
516
|
'-XX:MaxTenuringThreshold=1'
|
|
233
517
|
];
|
|
234
518
|
} else {
|
|
235
|
-
gcArgs =
|
|
236
|
-
'-XX:+UseG1GC',
|
|
519
|
+
gcArgs = [
|
|
237
520
|
'-XX:+ParallelRefProcEnabled',
|
|
238
|
-
'-XX:MaxGCPauseMillis=200',
|
|
239
521
|
'-XX:+UnlockExperimentalVMOptions',
|
|
240
522
|
'-XX:+DisableExplicitGC',
|
|
241
523
|
'-XX:+AlwaysPreTouch',
|
|
242
524
|
'-XX:G1HeapWastePercent=5',
|
|
243
525
|
'-XX:G1MixedGCCountTarget=4',
|
|
244
526
|
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
245
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
246
527
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
247
528
|
'-XX:SurvivorRatio=32',
|
|
248
529
|
'-XX:+PerfDisableSharedMem',
|
|
249
|
-
'-XX:MaxTenuringThreshold=1'
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
530
|
+
'-XX:MaxTenuringThreshold=1'
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
if (this.config.memory.useAikarsFlags) {
|
|
534
|
+
gcArgs.push('-Dusing.aikars.flags=https://mcflags.emc.gs');
|
|
535
|
+
gcArgs.push('-Daikars.new.flags=true');
|
|
536
|
+
}
|
|
253
537
|
}
|
|
254
538
|
|
|
255
539
|
if (javaVersion === '21') {
|
|
256
540
|
gcArgs.push('--enable-preview');
|
|
257
541
|
}
|
|
258
542
|
|
|
259
|
-
const baseArgs = [
|
|
260
|
-
`-Xms${this.config.memory.init}`,
|
|
261
|
-
`-Xmx${this.config.memory.max}`
|
|
262
|
-
];
|
|
263
|
-
|
|
264
543
|
return [...baseArgs, ...gcArgs];
|
|
265
544
|
}
|
|
266
545
|
|
|
@@ -268,7 +547,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
268
547
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
269
548
|
|
|
270
549
|
env.MALLOC_ARENA_MAX = '2';
|
|
271
|
-
|
|
272
550
|
env._JAVA_OPTIONS = `-Xmx${this.config.memory.max}`;
|
|
273
551
|
|
|
274
552
|
if (this.javaInfo && this.javaInfo.type === 'portable') {
|
|
@@ -288,6 +566,81 @@ export class MinecraftServer extends EventEmitter {
|
|
|
288
566
|
return env;
|
|
289
567
|
}
|
|
290
568
|
|
|
569
|
+
private setupLogHandlers(): void {
|
|
570
|
+
this.process.stdout.on('data', (data: Buffer) => {
|
|
571
|
+
const output = data.toString();
|
|
572
|
+
process.stdout.write(output);
|
|
573
|
+
|
|
574
|
+
if (!this.serverReady) {
|
|
575
|
+
if (output.includes('Done') || output.includes('For help, type "help"')) {
|
|
576
|
+
this.serverReady = true;
|
|
577
|
+
this.serverInfo.status = 'running';
|
|
578
|
+
this.logger.success('Server started successfully!');
|
|
579
|
+
|
|
580
|
+
if (this.options.enableViaVersion !== false) {
|
|
581
|
+
this.logger.info('ViaVersion is active - players from older versions can connect');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
585
|
+
this.logger.info('SkinRestorer is active - player skins will be restored');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (this.worldSize > 0) {
|
|
589
|
+
this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (this.owners.size > 0) {
|
|
593
|
+
this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (this.options.ramdisk?.enabled) {
|
|
597
|
+
this.logger.info('RAM disk active - worlds/plugins running in memory');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this.emit('ready', this.serverInfo);
|
|
601
|
+
this.startMemoryMonitor();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (output.includes('joined the game')) {
|
|
606
|
+
const match = output.match(/(\w+) joined the game/);
|
|
607
|
+
if (match) {
|
|
608
|
+
this.playerCount++;
|
|
609
|
+
this.handlePlayerJoin(match[1]);
|
|
610
|
+
|
|
611
|
+
if (this.owners.has(match[1].toLowerCase())) {
|
|
612
|
+
this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (output.includes('left the game')) {
|
|
618
|
+
const match = output.match(/(\w+) left the game/);
|
|
619
|
+
if (match) {
|
|
620
|
+
this.playerCount--;
|
|
621
|
+
this.handlePlayerLeave(match[1]);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (output.includes('<') && output.includes('>')) {
|
|
626
|
+
const chatMatch = output.match(/<(\w+)>\s+(.+)/);
|
|
627
|
+
if (chatMatch) {
|
|
628
|
+
const player = chatMatch[1];
|
|
629
|
+
const message = chatMatch[2];
|
|
630
|
+
|
|
631
|
+
if (message.startsWith(this.ownerCommandPrefix)) {
|
|
632
|
+
const command = message.substring(this.ownerCommandPrefix.length);
|
|
633
|
+
this.processOwnerCommand(player, command);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
this.process.stderr.on('data', (data: Buffer) => {
|
|
640
|
+
process.stderr.write(data.toString());
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
291
644
|
private processOwnerCommand(player: string, command: string): void {
|
|
292
645
|
if (!this.options.ownerCommands?.enabled) return;
|
|
293
646
|
if (!this.owners.has(player.toLowerCase())) return;
|
|
@@ -584,13 +937,98 @@ export class MinecraftServer extends EventEmitter {
|
|
|
584
937
|
this.emit('resource', this.serverInfo);
|
|
585
938
|
|
|
586
939
|
} catch (error) {
|
|
587
|
-
|
|
940
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
941
|
+
this.logger.error(`Stats update error: ${errorMessage}`);
|
|
588
942
|
}
|
|
589
943
|
}
|
|
590
944
|
|
|
945
|
+
private startMemoryMonitor(): void {
|
|
946
|
+
if (!this.options.memoryMonitor?.enabled) return;
|
|
947
|
+
|
|
948
|
+
const threshold = this.options.memoryMonitor.threshold || 90;
|
|
949
|
+
const interval = this.options.memoryMonitor.interval || 30000;
|
|
950
|
+
const action = this.options.memoryMonitor.action || 'warn';
|
|
951
|
+
|
|
952
|
+
this.memoryMonitorInterval = setInterval(async () => {
|
|
953
|
+
if (this.serverInfo.status !== 'running' || !this.process) return;
|
|
954
|
+
|
|
955
|
+
try {
|
|
956
|
+
await this.updateStats();
|
|
957
|
+
|
|
958
|
+
const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
|
|
959
|
+
|
|
960
|
+
this.memoryUsageHistory.push(memPercent);
|
|
961
|
+
if (this.memoryUsageHistory.length > 10) {
|
|
962
|
+
this.memoryUsageHistory.shift();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (memPercent > threshold) {
|
|
966
|
+
this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
|
|
967
|
+
|
|
968
|
+
const isIncreasing = this.memoryUsageHistory.length > 5 &&
|
|
969
|
+
this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
|
|
970
|
+
this.memoryUsageHistory[0] * 1.2;
|
|
971
|
+
|
|
972
|
+
if (isIncreasing) {
|
|
973
|
+
this.logger.warning('Memory leak detected!');
|
|
974
|
+
|
|
975
|
+
if (action === 'restart' && this.players.size === 0) {
|
|
976
|
+
this.logger.info('No players online, restarting server due to memory leak...');
|
|
977
|
+
await this.gracefulRestart();
|
|
978
|
+
} else if (action === 'stop' && this.players.size === 0) {
|
|
979
|
+
this.logger.info('No players online, stopping server due to memory leak...');
|
|
980
|
+
await this.stop();
|
|
981
|
+
} else if (action === 'warn') {
|
|
982
|
+
this.logger.warning('Please restart server to free memory');
|
|
983
|
+
} else {
|
|
984
|
+
this.logger.warning('Cannot restart: players online');
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
} catch (error) {
|
|
990
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
991
|
+
this.logger.error(`Memory monitor error: ${errorMessage}`);
|
|
992
|
+
}
|
|
993
|
+
}, interval);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private async gracefulRestart(): Promise<void> {
|
|
997
|
+
this.logger.info('Initiating graceful restart...');
|
|
998
|
+
|
|
999
|
+
this.sendCommand('say Server restarting in 30 seconds');
|
|
1000
|
+
this.sendCommand('save-all');
|
|
1001
|
+
|
|
1002
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
1003
|
+
|
|
1004
|
+
this.sendCommand('say Server restarting in 20 seconds');
|
|
1005
|
+
|
|
1006
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
1007
|
+
|
|
1008
|
+
this.sendCommand('say Server restarting in 10 seconds');
|
|
1009
|
+
this.sendCommand('save-all');
|
|
1010
|
+
|
|
1011
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
1012
|
+
|
|
1013
|
+
this.sendCommand('say Server restarting in 5 seconds');
|
|
1014
|
+
|
|
1015
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
1016
|
+
|
|
1017
|
+
await this.stop();
|
|
1018
|
+
await this.start();
|
|
1019
|
+
}
|
|
1020
|
+
|
|
591
1021
|
public async start(): Promise<ServerInfo> {
|
|
592
1022
|
this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
|
|
593
1023
|
|
|
1024
|
+
await this.setupSymlinkMaster();
|
|
1025
|
+
await this.setupRamdisk();
|
|
1026
|
+
|
|
1027
|
+
if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
|
|
1028
|
+
this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
|
|
1029
|
+
this.logger.info('Log cleanup scheduled every 3 hours');
|
|
1030
|
+
}
|
|
1031
|
+
|
|
594
1032
|
if (this.owners.size > 0) {
|
|
595
1033
|
this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
|
|
596
1034
|
}
|
|
@@ -623,7 +1061,23 @@ export class MinecraftServer extends EventEmitter {
|
|
|
623
1061
|
|
|
624
1062
|
if (this.config.platform === 'all') {
|
|
625
1063
|
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
626
|
-
await this.geyser.setup(this.config);
|
|
1064
|
+
await this.geyser.setup(this.config, this.isPterodactyl);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const pluginsToInstall = [
|
|
1068
|
+
{ name: 'ProtocolLib', enabled: this.options.enableProtocolLib, url: 'https://github.com/dmulloy2/ProtocolLib/releases/latest/download/ProtocolLib.jar' },
|
|
1069
|
+
{ name: 'TCPShield', enabled: this.options.enableTCPShield, url: 'https://tcpshield.com/api/download/plugin' },
|
|
1070
|
+
{ name: 'SpigotOptimizers', enabled: this.options.enableSpigotOptimizers, url: 'https://github.com/SpigotOptimizers/SpigotOptimizers/releases/latest/download/SpigotOptimizers.jar' },
|
|
1071
|
+
{ name: 'Spark', enabled: this.options.enableSpark, url: 'https://spark.lucko.me/download' },
|
|
1072
|
+
{ name: 'ViewDistanceTweaks', enabled: this.options.enableViewDistanceTweaks, url: 'https://github.com/ViewDistanceTweaks/ViewDistanceTweaks/releases/latest/download/ViewDistanceTweaks.jar' },
|
|
1073
|
+
{ name: 'FarmControl', enabled: this.options.enableFarmControl, url: 'https://github.com/FarmControl/FarmControl/releases/latest/download/FarmControl.jar' },
|
|
1074
|
+
{ name: 'EntityDetection', enabled: this.options.enableEntityDetection, url: 'https://github.com/EntityDetection/EntityDetection/releases/latest/download/EntityDetection.jar' }
|
|
1075
|
+
];
|
|
1076
|
+
|
|
1077
|
+
for (const plugin of pluginsToInstall) {
|
|
1078
|
+
if (plugin.enabled) {
|
|
1079
|
+
await this.pluginManager.installPlugin(plugin.name, plugin.url, this.config.folders.plugins);
|
|
1080
|
+
}
|
|
627
1081
|
}
|
|
628
1082
|
|
|
629
1083
|
if (this.options.enableViaVersion !== false) {
|
|
@@ -657,14 +1111,6 @@ export class MinecraftServer extends EventEmitter {
|
|
|
657
1111
|
...serverArgs
|
|
658
1112
|
];
|
|
659
1113
|
|
|
660
|
-
if (this.options.networkOptimization?.tcpFastOpen) {
|
|
661
|
-
fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (this.options.networkOptimization?.bungeeMode) {
|
|
665
|
-
fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
|
|
666
|
-
}
|
|
667
|
-
|
|
668
1114
|
const env = this.buildEnvironment();
|
|
669
1115
|
|
|
670
1116
|
this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
|
|
@@ -683,47 +1129,7 @@ export class MinecraftServer extends EventEmitter {
|
|
|
683
1129
|
this.process.stdout.pipe(process.stdout);
|
|
684
1130
|
this.process.stderr.pipe(process.stderr);
|
|
685
1131
|
} else {
|
|
686
|
-
this.
|
|
687
|
-
const output = data.toString();
|
|
688
|
-
process.stdout.write(output);
|
|
689
|
-
|
|
690
|
-
if (output.includes('joined the game')) {
|
|
691
|
-
const match = output.match(/(\w+) joined the game/);
|
|
692
|
-
if (match) {
|
|
693
|
-
this.playerCount++;
|
|
694
|
-
this.handlePlayerJoin(match[1]);
|
|
695
|
-
|
|
696
|
-
if (this.owners.has(match[1].toLowerCase())) {
|
|
697
|
-
this.sendCommand(`tellraw ${match[1]} {"text":"Welcome Owner! Use ${this.ownerCommandPrefix}help for commands","color":"gold"}`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (output.includes('left the game')) {
|
|
703
|
-
const match = output.match(/(\w+) left the game/);
|
|
704
|
-
if (match) {
|
|
705
|
-
this.playerCount--;
|
|
706
|
-
this.handlePlayerLeave(match[1]);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (output.includes('<') && output.includes('>')) {
|
|
711
|
-
const chatMatch = output.match(/<(\w+)>\s+(.+)/);
|
|
712
|
-
if (chatMatch) {
|
|
713
|
-
const player = chatMatch[1];
|
|
714
|
-
const message = chatMatch[2];
|
|
715
|
-
|
|
716
|
-
if (message.startsWith(this.ownerCommandPrefix)) {
|
|
717
|
-
const command = message.substring(this.ownerCommandPrefix.length);
|
|
718
|
-
this.processOwnerCommand(player, command);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
this.process.stderr.on('data', (data: Buffer) => {
|
|
725
|
-
process.stderr.write(data.toString());
|
|
726
|
-
});
|
|
1132
|
+
this.setupLogHandlers();
|
|
727
1133
|
}
|
|
728
1134
|
|
|
729
1135
|
this.process.on('exit', (code: number) => {
|
|
@@ -748,112 +1154,9 @@ export class MinecraftServer extends EventEmitter {
|
|
|
748
1154
|
this.setupBackups();
|
|
749
1155
|
}
|
|
750
1156
|
|
|
751
|
-
setTimeout(() => {
|
|
752
|
-
if (this.serverInfo.status === 'starting') {
|
|
753
|
-
this.serverInfo.status = 'running';
|
|
754
|
-
this.logger.success('Server started successfully!');
|
|
755
|
-
|
|
756
|
-
if (this.options.enableViaVersion !== false) {
|
|
757
|
-
this.logger.info('ViaVersion is active - players from older versions can connect');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
if (this.options.enableSkinRestorer !== false) {
|
|
761
|
-
this.logger.info('SkinRestorer is active - player skins will be restored');
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (this.worldSize > 0) {
|
|
765
|
-
this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (this.owners.size > 0) {
|
|
769
|
-
this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
this.emit('ready', this.serverInfo);
|
|
773
|
-
this.startMemoryMonitor();
|
|
774
|
-
}
|
|
775
|
-
}, 10000);
|
|
776
|
-
|
|
777
1157
|
return this.serverInfo;
|
|
778
1158
|
}
|
|
779
1159
|
|
|
780
|
-
private startMemoryMonitor(): void {
|
|
781
|
-
if (!this.options.memoryMonitor?.enabled) return;
|
|
782
|
-
|
|
783
|
-
const threshold = this.options.memoryMonitor.threshold || 90;
|
|
784
|
-
const interval = this.options.memoryMonitor.interval || 30000;
|
|
785
|
-
const action = this.options.memoryMonitor.action || 'warn';
|
|
786
|
-
|
|
787
|
-
this.memoryMonitorInterval = setInterval(async () => {
|
|
788
|
-
if (this.serverInfo.status !== 'running' || !this.process) return;
|
|
789
|
-
|
|
790
|
-
try {
|
|
791
|
-
await this.updateStats();
|
|
792
|
-
|
|
793
|
-
const memPercent = (this.serverInfo.memory.used / this.serverInfo.memory.max) * 100;
|
|
794
|
-
|
|
795
|
-
this.memoryUsageHistory.push(memPercent);
|
|
796
|
-
if (this.memoryUsageHistory.length > 10) {
|
|
797
|
-
this.memoryUsageHistory.shift();
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (memPercent > threshold) {
|
|
801
|
-
this.logger.warning(`High memory usage: ${memPercent.toFixed(1)}%`);
|
|
802
|
-
|
|
803
|
-
const isIncreasing = this.memoryUsageHistory.length > 5 &&
|
|
804
|
-
this.memoryUsageHistory[this.memoryUsageHistory.length - 1] >
|
|
805
|
-
this.memoryUsageHistory[0] * 1.2;
|
|
806
|
-
|
|
807
|
-
if (isIncreasing) {
|
|
808
|
-
this.logger.warning('Memory leak detected!');
|
|
809
|
-
|
|
810
|
-
switch (action) {
|
|
811
|
-
case 'restart':
|
|
812
|
-
this.logger.info('Restarting server due to memory leak...');
|
|
813
|
-
await this.gracefulRestart();
|
|
814
|
-
break;
|
|
815
|
-
case 'stop':
|
|
816
|
-
this.logger.info('Stopping server due to memory leak...');
|
|
817
|
-
await this.stop();
|
|
818
|
-
break;
|
|
819
|
-
case 'warn':
|
|
820
|
-
default:
|
|
821
|
-
this.logger.warning('Please restart server to free memory');
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
} catch (error) {
|
|
827
|
-
this.logger.error('Memory monitor error:', error);
|
|
828
|
-
}
|
|
829
|
-
}, interval);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
private async gracefulRestart(): Promise<void> {
|
|
833
|
-
this.logger.info('Initiating graceful restart...');
|
|
834
|
-
|
|
835
|
-
this.sendCommand('say Server restarting in 30 seconds');
|
|
836
|
-
this.sendCommand('save-all');
|
|
837
|
-
|
|
838
|
-
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
839
|
-
|
|
840
|
-
this.sendCommand('say Server restarting in 20 seconds');
|
|
841
|
-
|
|
842
|
-
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
843
|
-
|
|
844
|
-
this.sendCommand('say Server restarting in 10 seconds');
|
|
845
|
-
this.sendCommand('save-all');
|
|
846
|
-
|
|
847
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
848
|
-
|
|
849
|
-
this.sendCommand('say Server restarting in 5 seconds');
|
|
850
|
-
|
|
851
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
852
|
-
|
|
853
|
-
await this.stop();
|
|
854
|
-
await this.start();
|
|
855
|
-
}
|
|
856
|
-
|
|
857
1160
|
public async stop(): Promise<void> {
|
|
858
1161
|
if (!this.process) {
|
|
859
1162
|
this.logger.warning('Server not running');
|
|
@@ -877,6 +1180,14 @@ export class MinecraftServer extends EventEmitter {
|
|
|
877
1180
|
this.backupCron.stop();
|
|
878
1181
|
}
|
|
879
1182
|
|
|
1183
|
+
if (this.cleanLogsCron) {
|
|
1184
|
+
this.cleanLogsCron.stop();
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (this.ramdiskBackupCron) {
|
|
1188
|
+
this.ramdiskBackupCron.stop();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
880
1191
|
if (this.memoryMonitorInterval) {
|
|
881
1192
|
clearInterval(this.memoryMonitorInterval);
|
|
882
1193
|
}
|
|
@@ -889,6 +1200,10 @@ export class MinecraftServer extends EventEmitter {
|
|
|
889
1200
|
this.geyser.stop();
|
|
890
1201
|
}
|
|
891
1202
|
|
|
1203
|
+
if (this.options.ramdisk?.enabled) {
|
|
1204
|
+
await this.backupRamdiskToMaster();
|
|
1205
|
+
}
|
|
1206
|
+
|
|
892
1207
|
this.logger.success('Server stopped');
|
|
893
1208
|
}
|
|
894
1209
|
|
|
@@ -999,33 +1314,4 @@ export class MinecraftServer extends EventEmitter {
|
|
|
999
1314
|
}
|
|
1000
1315
|
return value;
|
|
1001
1316
|
}
|
|
1002
|
-
|
|
1003
|
-
private async calculateWorldSize(): Promise<number> {
|
|
1004
|
-
try {
|
|
1005
|
-
const worldPath = path.join(process.cwd(), this.config.folders.world);
|
|
1006
|
-
if (!await fs.pathExists(worldPath)) return 0;
|
|
1007
|
-
|
|
1008
|
-
const getSize = async (dir: string): Promise<number> => {
|
|
1009
|
-
let total = 0;
|
|
1010
|
-
const files = await fs.readdir(dir);
|
|
1011
|
-
|
|
1012
|
-
for (const file of files) {
|
|
1013
|
-
const filePath = path.join(dir, file);
|
|
1014
|
-
const stat = await fs.stat(filePath);
|
|
1015
|
-
|
|
1016
|
-
if (stat.isDirectory()) {
|
|
1017
|
-
total += await getSize(filePath);
|
|
1018
|
-
} else {
|
|
1019
|
-
total += stat.size;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
return total;
|
|
1024
|
-
};
|
|
1025
|
-
|
|
1026
|
-
return await getSize(worldPath);
|
|
1027
|
-
} catch {
|
|
1028
|
-
return 0;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
1317
|
}
|