@dimzxzzx07/mc-headless 2.2.2 → 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 +652 -776
- package/dist/core/MinecraftServer.d.ts +13 -38
- package/dist/core/MinecraftServer.d.ts.map +1 -1
- package/dist/core/MinecraftServer.js +417 -333
- 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/MinecraftServer.ts +496 -411
- 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
|
@@ -3,7 +3,16 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import * as cron from 'node-cron';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as fs from 'fs-extra';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MinecraftConfig,
|
|
8
|
+
ServerInfo,
|
|
9
|
+
Player,
|
|
10
|
+
MinecraftServerOptions,
|
|
11
|
+
MemoryMonitorConfig,
|
|
12
|
+
NetworkOptimizationConfig,
|
|
13
|
+
OwnerConfig,
|
|
14
|
+
RamdiskConfig
|
|
15
|
+
} from '../types';
|
|
7
16
|
import { ConfigHandler } from './ConfigHandler';
|
|
8
17
|
import { JavaChecker, JavaInfo } from './JavaChecker';
|
|
9
18
|
import { FileUtils } from '../utils/FileUtils';
|
|
@@ -17,38 +26,7 @@ import { ServerEngine } from '../engines/ServerEngine';
|
|
|
17
26
|
import { GeyserBridge } from '../platforms/GeyserBridge';
|
|
18
27
|
import { ViaVersionManager } from '../platforms/ViaVersion';
|
|
19
28
|
import { SkinRestorerManager } from '../platforms/SkinRestorer';
|
|
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
|
-
cleanLogsInterval?: number;
|
|
50
|
-
optimizeForPterodactyl?: boolean;
|
|
51
|
-
}
|
|
29
|
+
import { PluginManager } from '../platforms/PluginManager';
|
|
52
30
|
|
|
53
31
|
export class MinecraftServer extends EventEmitter {
|
|
54
32
|
private config: MinecraftConfig;
|
|
@@ -58,11 +36,13 @@ export class MinecraftServer extends EventEmitter {
|
|
|
58
36
|
private geyser: GeyserBridge;
|
|
59
37
|
private viaVersion: ViaVersionManager;
|
|
60
38
|
private skinRestorer: SkinRestorerManager;
|
|
39
|
+
private pluginManager: PluginManager;
|
|
61
40
|
private process: any = null;
|
|
62
41
|
private serverInfo: ServerInfo;
|
|
63
42
|
private players: Map<string, Player> = new Map();
|
|
64
43
|
private backupCron: cron.ScheduledTask | null = null;
|
|
65
44
|
private cleanLogsCron: cron.ScheduledTask | null = null;
|
|
45
|
+
private ramdiskBackupCron: cron.ScheduledTask | null = null;
|
|
66
46
|
private startTime: Date | null = null;
|
|
67
47
|
private memoryMonitorInterval: NodeJS.Timeout | null = null;
|
|
68
48
|
private statsInterval: NodeJS.Timeout | null = null;
|
|
@@ -79,6 +59,8 @@ export class MinecraftServer extends EventEmitter {
|
|
|
79
59
|
private cgroupMemory: number = 0;
|
|
80
60
|
private cgroupCpu: number = 0;
|
|
81
61
|
private isPterodactyl: boolean = false;
|
|
62
|
+
private ramdiskPaths: Map<string, string> = new Map();
|
|
63
|
+
private serverReady: boolean = false;
|
|
82
64
|
|
|
83
65
|
constructor(userConfig: MinecraftServerOptions = {}) {
|
|
84
66
|
super();
|
|
@@ -87,35 +69,73 @@ export class MinecraftServer extends EventEmitter {
|
|
|
87
69
|
|
|
88
70
|
this.detectEnvironment();
|
|
89
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
|
+
|
|
90
113
|
this.options = {
|
|
91
114
|
javaVersion: 'auto',
|
|
92
115
|
usePortableJava: !this.isPterodactyl,
|
|
93
|
-
memoryMonitor:
|
|
94
|
-
enabled: true,
|
|
95
|
-
threshold: 90,
|
|
96
|
-
interval: 30000,
|
|
97
|
-
action: 'warn'
|
|
98
|
-
},
|
|
116
|
+
memoryMonitor: defaultMemoryMonitor,
|
|
99
117
|
autoInstallJava: true,
|
|
100
|
-
networkOptimization:
|
|
101
|
-
tcpFastOpen: true,
|
|
102
|
-
bungeeMode: false,
|
|
103
|
-
proxyProtocol: false
|
|
104
|
-
},
|
|
118
|
+
networkOptimization: defaultNetworkOptimization,
|
|
105
119
|
owners: [],
|
|
106
|
-
ownerCommands:
|
|
107
|
-
prefix: '!',
|
|
108
|
-
enabled: true
|
|
109
|
-
},
|
|
120
|
+
ownerCommands: defaultOwnerCommands,
|
|
110
121
|
silentMode: true,
|
|
111
122
|
statsInterval: 30000,
|
|
112
123
|
cleanLogsInterval: 3 * 60 * 60 * 1000,
|
|
113
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,
|
|
114
134
|
...userConfig
|
|
115
135
|
};
|
|
116
136
|
|
|
117
137
|
if (this.options.owners) {
|
|
118
|
-
this.options.owners.forEach(owner => this.owners.add(owner.toLowerCase()));
|
|
138
|
+
this.options.owners.forEach((owner: string) => this.owners.add(owner.toLowerCase()));
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
if (this.options.ownerCommands?.prefix) {
|
|
@@ -133,6 +153,7 @@ export class MinecraftServer extends EventEmitter {
|
|
|
133
153
|
this.geyser = new GeyserBridge();
|
|
134
154
|
this.viaVersion = new ViaVersionManager();
|
|
135
155
|
this.skinRestorer = new SkinRestorerManager();
|
|
156
|
+
this.pluginManager = new PluginManager();
|
|
136
157
|
|
|
137
158
|
this.serverInfo = {
|
|
138
159
|
pid: 0,
|
|
@@ -151,19 +172,53 @@ export class MinecraftServer extends EventEmitter {
|
|
|
151
172
|
};
|
|
152
173
|
|
|
153
174
|
this.detectCgroupLimits();
|
|
154
|
-
this.cleanPaperCache();
|
|
155
175
|
}
|
|
156
176
|
|
|
157
177
|
private detectEnvironment(): void {
|
|
158
178
|
this.isPterodactyl = !!(
|
|
159
179
|
process.env.PTERODACTYL ||
|
|
160
180
|
process.env.SERVER_PORT ||
|
|
161
|
-
process.env.SERVER_MEMORY_MAX ||
|
|
162
181
|
fs.existsSync('/home/container')
|
|
163
182
|
);
|
|
164
183
|
|
|
165
184
|
if (this.isPterodactyl) {
|
|
166
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
|
+
|
|
204
|
+
private detectCgroupLimits(): void {
|
|
205
|
+
try {
|
|
206
|
+
if (fs.existsSync('/sys/fs/cgroup/memory/memory.limit_in_bytes')) {
|
|
207
|
+
const limit = fs.readFileSync('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
|
|
208
|
+
this.cgroupMemory = parseInt(limit) / 1024 / 1024;
|
|
209
|
+
this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && fs.existsSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) {
|
|
213
|
+
const quota = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8'));
|
|
214
|
+
const period = parseInt(fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8'));
|
|
215
|
+
if (quota > 0 && period > 0) {
|
|
216
|
+
this.cgroupCpu = quota / period;
|
|
217
|
+
this.logger.debug(`Cgroup CPU limit: ${this.cgroupCpu} cores`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.logger.debug('Not running in cgroup environment');
|
|
167
222
|
}
|
|
168
223
|
}
|
|
169
224
|
|
|
@@ -171,13 +226,14 @@ export class MinecraftServer extends EventEmitter {
|
|
|
171
226
|
const pterodactylPort = process.env.SERVER_PORT;
|
|
172
227
|
if (pterodactylPort) {
|
|
173
228
|
this.config.network.port = parseInt(pterodactylPort);
|
|
174
|
-
this.logger.info(`Using Pterodactyl port: ${pterodactylPort}`);
|
|
175
229
|
}
|
|
176
230
|
|
|
177
231
|
const pterodactylMemory = process.env.SERVER_MEMORY_MAX;
|
|
178
232
|
if (pterodactylMemory) {
|
|
179
|
-
|
|
180
|
-
|
|
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`);
|
|
181
237
|
}
|
|
182
238
|
|
|
183
239
|
const pterodactylInitMemory = process.env.SERVER_MEMORY_INIT;
|
|
@@ -187,29 +243,137 @@ export class MinecraftServer extends EventEmitter {
|
|
|
187
243
|
|
|
188
244
|
this.config.world.viewDistance = 6;
|
|
189
245
|
this.config.world.simulationDistance = 4;
|
|
190
|
-
|
|
191
|
-
this.logger.info('Applied Pterodactyl optimizations');
|
|
192
246
|
}
|
|
193
247
|
|
|
194
|
-
private
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
248
|
+
private createEngine(): ServerEngine {
|
|
249
|
+
switch (this.config.type) {
|
|
250
|
+
case 'paper':
|
|
251
|
+
case 'purpur':
|
|
252
|
+
case 'spigot':
|
|
253
|
+
return new PaperEngine();
|
|
254
|
+
case 'vanilla':
|
|
255
|
+
return new VanillaEngine();
|
|
256
|
+
case 'forge':
|
|
257
|
+
return new ForgeEngine();
|
|
258
|
+
case 'fabric':
|
|
259
|
+
return new FabricEngine();
|
|
260
|
+
default:
|
|
261
|
+
throw new Error(`Unsupported server type: ${this.config.type}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
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 }
|
|
200
288
|
];
|
|
201
289
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.logger.
|
|
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;
|
|
210
299
|
}
|
|
211
300
|
}
|
|
212
|
-
|
|
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
|
+
}
|
|
213
377
|
}
|
|
214
378
|
|
|
215
379
|
private cleanOldLogs(): void {
|
|
@@ -230,139 +394,38 @@ export class MinecraftServer extends EventEmitter {
|
|
|
230
394
|
}
|
|
231
395
|
});
|
|
232
396
|
this.logger.info('Old logs cleaned');
|
|
233
|
-
} catch (err
|
|
234
|
-
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
399
|
+
this.logger.warning(`Failed to clean logs: ${errorMessage}`);
|
|
235
400
|
}
|
|
236
401
|
}
|
|
237
402
|
|
|
238
|
-
private
|
|
239
|
-
const serverDir = process.cwd();
|
|
240
|
-
|
|
241
|
-
const paperGlobalYml = path.join(serverDir, 'paper-global.yml');
|
|
242
|
-
if (!fs.existsSync(paperGlobalYml)) {
|
|
243
|
-
const paperGlobalConfig = `# Paper Global Configuration
|
|
244
|
-
# Optimized by MC-Headless
|
|
245
|
-
|
|
246
|
-
chunk-loading:
|
|
247
|
-
autoconfig-send-distance: false
|
|
248
|
-
enable-frustum-priority: false
|
|
249
|
-
global-max-chunk-load-rate: 100
|
|
250
|
-
global-max-chunk-send-rate: 50
|
|
251
|
-
|
|
252
|
-
entities:
|
|
253
|
-
despawn-ranges:
|
|
254
|
-
monster:
|
|
255
|
-
soft: 28
|
|
256
|
-
hard: 96
|
|
257
|
-
animal:
|
|
258
|
-
soft: 16
|
|
259
|
-
hard: 32
|
|
260
|
-
spawn-limits:
|
|
261
|
-
ambient: 5
|
|
262
|
-
axolotls: 5
|
|
263
|
-
creature: 10
|
|
264
|
-
monster: 15
|
|
265
|
-
water_ambient: 5
|
|
266
|
-
water_creature: 5
|
|
267
|
-
|
|
268
|
-
misc:
|
|
269
|
-
max-entity-speed: 100
|
|
270
|
-
redstone-implementation: ALTERNATIVE_CURRENT
|
|
271
|
-
|
|
272
|
-
player-auto-save: 6000
|
|
273
|
-
player-auto-save-rate: 1200
|
|
274
|
-
no-tick-view-distance: 12
|
|
275
|
-
`;
|
|
276
|
-
fs.writeFileSync(paperGlobalYml, paperGlobalConfig);
|
|
277
|
-
this.logger.debug('Generated paper-global.yml');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const paperWorldDefaultsYml = path.join(serverDir, 'paper-world-defaults.yml');
|
|
281
|
-
if (!fs.existsSync(paperWorldDefaultsYml)) {
|
|
282
|
-
const paperWorldConfig = `# Paper World Defaults
|
|
283
|
-
# Optimized by MC-Headless
|
|
284
|
-
|
|
285
|
-
view-distance: 6
|
|
286
|
-
no-tick-view-distance: 12
|
|
287
|
-
|
|
288
|
-
despawn-ranges:
|
|
289
|
-
monster:
|
|
290
|
-
soft: 28
|
|
291
|
-
hard: 96
|
|
292
|
-
animal:
|
|
293
|
-
soft: 16
|
|
294
|
-
hard: 32
|
|
295
|
-
ambient:
|
|
296
|
-
soft: 16
|
|
297
|
-
hard: 32
|
|
298
|
-
`;
|
|
299
|
-
fs.writeFileSync(paperWorldDefaultsYml, paperWorldConfig);
|
|
300
|
-
this.logger.debug('Generated paper-world-defaults.yml');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const spigotYml = path.join(serverDir, 'spigot.yml');
|
|
304
|
-
if (!fs.existsSync(spigotYml)) {
|
|
305
|
-
const spigotConfig = `# Spigot Configuration
|
|
306
|
-
# Optimized by MC-Headless
|
|
307
|
-
|
|
308
|
-
entity-activation-range:
|
|
309
|
-
animals: 16
|
|
310
|
-
monsters: 24
|
|
311
|
-
misc: 8
|
|
312
|
-
water: 8
|
|
313
|
-
|
|
314
|
-
mob-spawn-range: 4
|
|
315
|
-
|
|
316
|
-
world-settings:
|
|
317
|
-
default:
|
|
318
|
-
view-distance: 6
|
|
319
|
-
mob-spawn-range: 4
|
|
320
|
-
entity-activation-range:
|
|
321
|
-
animals: 16
|
|
322
|
-
monsters: 24
|
|
323
|
-
misc: 8
|
|
324
|
-
water: 8
|
|
325
|
-
`;
|
|
326
|
-
fs.writeFileSync(spigotYml, spigotConfig);
|
|
327
|
-
this.logger.debug('Generated spigot.yml');
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private detectCgroupLimits(): void {
|
|
403
|
+
private async calculateWorldSize(): Promise<number> {
|
|
332
404
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
this.cgroupMemory = parseInt(limit) / 1024 / 1024;
|
|
336
|
-
this.logger.debug(`Cgroup memory limit: ${this.cgroupMemory} MB`);
|
|
337
|
-
}
|
|
405
|
+
const worldPath = path.join(process.cwd(), this.config.folders.world);
|
|
406
|
+
if (!await fs.pathExists(worldPath)) return 0;
|
|
338
407
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
}
|
|
345
421
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
}
|
|
422
|
+
|
|
423
|
+
return total;
|
|
424
|
+
};
|
|
351
425
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
case 'purpur':
|
|
356
|
-
case 'spigot':
|
|
357
|
-
return new PaperEngine();
|
|
358
|
-
case 'vanilla':
|
|
359
|
-
return new VanillaEngine();
|
|
360
|
-
case 'forge':
|
|
361
|
-
return new ForgeEngine();
|
|
362
|
-
case 'fabric':
|
|
363
|
-
return new FabricEngine();
|
|
364
|
-
default:
|
|
365
|
-
throw new Error(`Unsupported server type: ${this.config.type}`);
|
|
426
|
+
return await getSize(worldPath);
|
|
427
|
+
} catch {
|
|
428
|
+
return 0;
|
|
366
429
|
}
|
|
367
430
|
}
|
|
368
431
|
|
|
@@ -371,21 +434,54 @@ world-settings:
|
|
|
371
434
|
return this.options.customJavaArgs;
|
|
372
435
|
}
|
|
373
436
|
|
|
374
|
-
|
|
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
|
+
|
|
375
445
|
const javaVersion = this.options.javaVersion || 'auto';
|
|
376
446
|
|
|
377
447
|
const baseArgs = [
|
|
378
448
|
`-Xms${this.config.memory.init}`,
|
|
379
|
-
`-Xmx${
|
|
380
|
-
'-XX:+UseG1GC'
|
|
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'
|
|
381
458
|
];
|
|
382
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
|
+
|
|
383
480
|
let gcArgs: string[] = [];
|
|
384
481
|
|
|
385
482
|
if (memMax >= 16384) {
|
|
386
483
|
gcArgs = [
|
|
387
484
|
'-XX:+ParallelRefProcEnabled',
|
|
388
|
-
'-XX:MaxGCPauseMillis=100',
|
|
389
485
|
'-XX:+UnlockExperimentalVMOptions',
|
|
390
486
|
'-XX:+DisableExplicitGC',
|
|
391
487
|
'-XX:+AlwaysPreTouch',
|
|
@@ -396,7 +492,6 @@ world-settings:
|
|
|
396
492
|
'-XX:G1HeapWastePercent=5',
|
|
397
493
|
'-XX:G1MixedGCCountTarget=4',
|
|
398
494
|
'-XX:InitiatingHeapOccupancyPercent=20',
|
|
399
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
400
495
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
401
496
|
'-XX:SurvivorRatio=32',
|
|
402
497
|
'-XX:+PerfDisableSharedMem',
|
|
@@ -405,7 +500,6 @@ world-settings:
|
|
|
405
500
|
} else if (memMax >= 8192) {
|
|
406
501
|
gcArgs = [
|
|
407
502
|
'-XX:+ParallelRefProcEnabled',
|
|
408
|
-
'-XX:MaxGCPauseMillis=150',
|
|
409
503
|
'-XX:+UnlockExperimentalVMOptions',
|
|
410
504
|
'-XX:+DisableExplicitGC',
|
|
411
505
|
'-XX:+AlwaysPreTouch',
|
|
@@ -416,30 +510,30 @@ world-settings:
|
|
|
416
510
|
'-XX:G1HeapWastePercent=5',
|
|
417
511
|
'-XX:G1MixedGCCountTarget=4',
|
|
418
512
|
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
419
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
420
513
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
421
514
|
'-XX:SurvivorRatio=32',
|
|
422
515
|
'-XX:+PerfDisableSharedMem',
|
|
423
516
|
'-XX:MaxTenuringThreshold=1'
|
|
424
517
|
];
|
|
425
518
|
} else {
|
|
426
|
-
gcArgs =
|
|
519
|
+
gcArgs = [
|
|
427
520
|
'-XX:+ParallelRefProcEnabled',
|
|
428
|
-
'-XX:MaxGCPauseMillis=200',
|
|
429
521
|
'-XX:+UnlockExperimentalVMOptions',
|
|
430
522
|
'-XX:+DisableExplicitGC',
|
|
431
523
|
'-XX:+AlwaysPreTouch',
|
|
432
524
|
'-XX:G1HeapWastePercent=5',
|
|
433
525
|
'-XX:G1MixedGCCountTarget=4',
|
|
434
526
|
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
435
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
436
527
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
437
528
|
'-XX:SurvivorRatio=32',
|
|
438
529
|
'-XX:+PerfDisableSharedMem',
|
|
439
|
-
'-XX:MaxTenuringThreshold=1'
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
}
|
|
443
537
|
}
|
|
444
538
|
|
|
445
539
|
if (javaVersion === '21') {
|
|
@@ -472,167 +566,41 @@ world-settings:
|
|
|
472
566
|
return env;
|
|
473
567
|
}
|
|
474
568
|
|
|
475
|
-
|
|
476
|
-
this.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
this.generateOptimizedConfigs();
|
|
480
|
-
|
|
481
|
-
if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
|
|
482
|
-
this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
|
|
483
|
-
this.logger.info('Log cleanup scheduled every 3 hours');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (this.owners.size > 0) {
|
|
487
|
-
this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
this.javaInfo = await JavaChecker.ensureJava(
|
|
491
|
-
this.config.version,
|
|
492
|
-
this.options.usePortableJava || false
|
|
493
|
-
);
|
|
494
|
-
this.javaCommand = this.javaInfo.path;
|
|
495
|
-
this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
|
|
496
|
-
|
|
497
|
-
const systemInfo = SystemDetector.getSystemInfo();
|
|
498
|
-
this.logger.debug('System info:', systemInfo);
|
|
499
|
-
|
|
500
|
-
const serverDir = process.cwd();
|
|
501
|
-
await FileUtils.ensureServerStructure(this.config);
|
|
502
|
-
|
|
503
|
-
this.worldSize = await this.calculateWorldSize();
|
|
504
|
-
if (this.worldSize > 10 * 1024 * 1024 * 1024) {
|
|
505
|
-
this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const jarPath = await this.engine.download(this.config, serverDir);
|
|
509
|
-
|
|
510
|
-
if (this.config.type === 'forge') {
|
|
511
|
-
await this.engine.prepare(this.config, serverDir, jarPath);
|
|
512
|
-
} else {
|
|
513
|
-
await this.engine.prepare(this.config, serverDir);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (this.config.platform === 'all') {
|
|
517
|
-
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
518
|
-
await this.geyser.setup(this.config);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (this.options.enableViaVersion !== false) {
|
|
522
|
-
this.logger.info('Enabling ViaVersion for client version compatibility...');
|
|
523
|
-
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
524
|
-
await this.viaVersion.setup(this.config);
|
|
525
|
-
await this.viaVersion.configureViaVersion(this.config);
|
|
526
|
-
|
|
527
|
-
if (this.options.enableViaBackwards !== false) {
|
|
528
|
-
this.logger.info('ViaBackwards will be installed');
|
|
529
|
-
}
|
|
530
|
-
if (this.options.enableViaRewind !== false) {
|
|
531
|
-
this.logger.info('ViaRewind will be installed');
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (this.options.enableSkinRestorer !== false) {
|
|
536
|
-
this.logger.info('Enabling SkinRestorer for player skins...');
|
|
537
|
-
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
538
|
-
await this.skinRestorer.setup(this.config);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const javaArgs = this.buildJavaArgs();
|
|
542
|
-
const serverJar = this.engine.getServerJar(jarPath);
|
|
543
|
-
const serverArgs = this.engine.getServerArgs();
|
|
544
|
-
|
|
545
|
-
const fullArgs = [
|
|
546
|
-
...javaArgs,
|
|
547
|
-
'-jar',
|
|
548
|
-
serverJar,
|
|
549
|
-
...serverArgs
|
|
550
|
-
];
|
|
551
|
-
|
|
552
|
-
if (this.options.networkOptimization?.tcpFastOpen) {
|
|
553
|
-
fullArgs.unshift('-Djava.net.preferIPv4Stack=true');
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (this.options.networkOptimization?.bungeeMode) {
|
|
557
|
-
fullArgs.unshift('-Dnet.kyori.adventure.text.serializer.legacy.AMPMSupport=true');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const env = this.buildEnvironment();
|
|
561
|
-
|
|
562
|
-
this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
|
|
563
|
-
|
|
564
|
-
this.process = spawn(this.javaCommand, fullArgs, {
|
|
565
|
-
cwd: serverDir,
|
|
566
|
-
env: env,
|
|
567
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
this.serverInfo.pid = this.process.pid!;
|
|
571
|
-
this.serverInfo.status = 'starting';
|
|
572
|
-
this.startTime = new Date();
|
|
573
|
-
|
|
574
|
-
if (this.options.silentMode) {
|
|
575
|
-
this.process.stdout.pipe(process.stdout);
|
|
576
|
-
this.process.stderr.pipe(process.stderr);
|
|
577
|
-
} else {
|
|
578
|
-
this.setupLogHandlers();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
this.process.on('exit', (code: number) => {
|
|
582
|
-
this.serverInfo.status = 'stopped';
|
|
583
|
-
this.logger.warning(`Server stopped with code ${code}`);
|
|
584
|
-
|
|
585
|
-
if (this.config.autoRestart && code !== 0) {
|
|
586
|
-
this.logger.info('Auto-restarting...');
|
|
587
|
-
this.start();
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
this.emit('stop', { code });
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
if (this.options.statsInterval && this.options.statsInterval > 0) {
|
|
594
|
-
this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
this.monitorResources();
|
|
569
|
+
private setupLogHandlers(): void {
|
|
570
|
+
this.process.stdout.on('data', (data: Buffer) => {
|
|
571
|
+
const output = data.toString();
|
|
572
|
+
process.stdout.write(output);
|
|
598
573
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
}
|
|
602
591
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
this.logger.success('Server started successfully!');
|
|
607
|
-
|
|
608
|
-
if (this.options.enableViaVersion !== false) {
|
|
609
|
-
this.logger.info('ViaVersion is active - players from older versions can connect');
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (this.options.enableSkinRestorer !== false) {
|
|
613
|
-
this.logger.info('SkinRestorer is active - player skins will be restored');
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (this.worldSize > 0) {
|
|
617
|
-
this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
618
|
-
}
|
|
592
|
+
if (this.owners.size > 0) {
|
|
593
|
+
this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
|
|
594
|
+
}
|
|
619
595
|
|
|
620
|
-
|
|
621
|
-
|
|
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();
|
|
622
602
|
}
|
|
623
|
-
|
|
624
|
-
this.emit('ready', this.serverInfo);
|
|
625
|
-
this.startMemoryMonitor();
|
|
626
603
|
}
|
|
627
|
-
}, 10000);
|
|
628
|
-
|
|
629
|
-
return this.serverInfo;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
private setupLogHandlers(): void {
|
|
633
|
-
this.process.stdout.on('data', (data: Buffer) => {
|
|
634
|
-
const output = data.toString();
|
|
635
|
-
process.stdout.write(output);
|
|
636
604
|
|
|
637
605
|
if (output.includes('joined the game')) {
|
|
638
606
|
const match = output.match(/(\w+) joined the game/);
|
|
@@ -969,7 +937,8 @@ world-settings:
|
|
|
969
937
|
this.emit('resource', this.serverInfo);
|
|
970
938
|
|
|
971
939
|
} catch (error) {
|
|
972
|
-
|
|
940
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
941
|
+
this.logger.error(`Stats update error: ${errorMessage}`);
|
|
973
942
|
}
|
|
974
943
|
}
|
|
975
944
|
|
|
@@ -1003,24 +972,23 @@ world-settings:
|
|
|
1003
972
|
if (isIncreasing) {
|
|
1004
973
|
this.logger.warning('Memory leak detected!');
|
|
1005
974
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
default:
|
|
1017
|
-
this.logger.warning('Please restart server to free memory');
|
|
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');
|
|
1018
985
|
}
|
|
1019
986
|
}
|
|
1020
987
|
}
|
|
1021
988
|
|
|
1022
989
|
} catch (error) {
|
|
1023
|
-
|
|
990
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
991
|
+
this.logger.error(`Memory monitor error: ${errorMessage}`);
|
|
1024
992
|
}
|
|
1025
993
|
}, interval);
|
|
1026
994
|
}
|
|
@@ -1050,6 +1018,145 @@ world-settings:
|
|
|
1050
1018
|
await this.start();
|
|
1051
1019
|
}
|
|
1052
1020
|
|
|
1021
|
+
public async start(): Promise<ServerInfo> {
|
|
1022
|
+
this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
|
|
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
|
+
|
|
1032
|
+
if (this.owners.size > 0) {
|
|
1033
|
+
this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
this.javaInfo = await JavaChecker.ensureJava(
|
|
1037
|
+
this.config.version,
|
|
1038
|
+
this.options.usePortableJava || false
|
|
1039
|
+
);
|
|
1040
|
+
this.javaCommand = this.javaInfo.path;
|
|
1041
|
+
this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
|
|
1042
|
+
|
|
1043
|
+
const systemInfo = SystemDetector.getSystemInfo();
|
|
1044
|
+
this.logger.debug('System info:', systemInfo);
|
|
1045
|
+
|
|
1046
|
+
const serverDir = process.cwd();
|
|
1047
|
+
await FileUtils.ensureServerStructure(this.config);
|
|
1048
|
+
|
|
1049
|
+
this.worldSize = await this.calculateWorldSize();
|
|
1050
|
+
if (this.worldSize > 10 * 1024 * 1024 * 1024) {
|
|
1051
|
+
this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const jarPath = await this.engine.download(this.config, serverDir);
|
|
1055
|
+
|
|
1056
|
+
if (this.config.type === 'forge') {
|
|
1057
|
+
await this.engine.prepare(this.config, serverDir, jarPath);
|
|
1058
|
+
} else {
|
|
1059
|
+
await this.engine.prepare(this.config, serverDir);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (this.config.platform === 'all') {
|
|
1063
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
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
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (this.options.enableViaVersion !== false) {
|
|
1084
|
+
this.logger.info('Enabling ViaVersion for client version compatibility...');
|
|
1085
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
1086
|
+
await this.viaVersion.setup(this.config);
|
|
1087
|
+
await this.viaVersion.configureViaVersion(this.config);
|
|
1088
|
+
|
|
1089
|
+
if (this.options.enableViaBackwards !== false) {
|
|
1090
|
+
this.logger.info('ViaBackwards will be installed');
|
|
1091
|
+
}
|
|
1092
|
+
if (this.options.enableViaRewind !== false) {
|
|
1093
|
+
this.logger.info('ViaRewind will be installed');
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
1098
|
+
this.logger.info('Enabling SkinRestorer for player skins...');
|
|
1099
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
1100
|
+
await this.skinRestorer.setup(this.config);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const javaArgs = this.buildJavaArgs();
|
|
1104
|
+
const serverJar = this.engine.getServerJar(jarPath);
|
|
1105
|
+
const serverArgs = this.engine.getServerArgs();
|
|
1106
|
+
|
|
1107
|
+
const fullArgs = [
|
|
1108
|
+
...javaArgs,
|
|
1109
|
+
'-jar',
|
|
1110
|
+
serverJar,
|
|
1111
|
+
...serverArgs
|
|
1112
|
+
];
|
|
1113
|
+
|
|
1114
|
+
const env = this.buildEnvironment();
|
|
1115
|
+
|
|
1116
|
+
this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
|
|
1117
|
+
|
|
1118
|
+
this.process = spawn(this.javaCommand, fullArgs, {
|
|
1119
|
+
cwd: serverDir,
|
|
1120
|
+
env: env,
|
|
1121
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
this.serverInfo.pid = this.process.pid!;
|
|
1125
|
+
this.serverInfo.status = 'starting';
|
|
1126
|
+
this.startTime = new Date();
|
|
1127
|
+
|
|
1128
|
+
if (this.options.silentMode) {
|
|
1129
|
+
this.process.stdout.pipe(process.stdout);
|
|
1130
|
+
this.process.stderr.pipe(process.stderr);
|
|
1131
|
+
} else {
|
|
1132
|
+
this.setupLogHandlers();
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
this.process.on('exit', (code: number) => {
|
|
1136
|
+
this.serverInfo.status = 'stopped';
|
|
1137
|
+
this.logger.warning(`Server stopped with code ${code}`);
|
|
1138
|
+
|
|
1139
|
+
if (this.config.autoRestart && code !== 0) {
|
|
1140
|
+
this.logger.info('Auto-restarting...');
|
|
1141
|
+
this.start();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
this.emit('stop', { code });
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
if (this.options.statsInterval && this.options.statsInterval > 0) {
|
|
1148
|
+
this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
this.monitorResources();
|
|
1152
|
+
|
|
1153
|
+
if (this.config.backup.enabled) {
|
|
1154
|
+
this.setupBackups();
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return this.serverInfo;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1053
1160
|
public async stop(): Promise<void> {
|
|
1054
1161
|
if (!this.process) {
|
|
1055
1162
|
this.logger.warning('Server not running');
|
|
@@ -1077,6 +1184,10 @@ world-settings:
|
|
|
1077
1184
|
this.cleanLogsCron.stop();
|
|
1078
1185
|
}
|
|
1079
1186
|
|
|
1187
|
+
if (this.ramdiskBackupCron) {
|
|
1188
|
+
this.ramdiskBackupCron.stop();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1080
1191
|
if (this.memoryMonitorInterval) {
|
|
1081
1192
|
clearInterval(this.memoryMonitorInterval);
|
|
1082
1193
|
}
|
|
@@ -1089,7 +1200,10 @@ world-settings:
|
|
|
1089
1200
|
this.geyser.stop();
|
|
1090
1201
|
}
|
|
1091
1202
|
|
|
1092
|
-
this.
|
|
1203
|
+
if (this.options.ramdisk?.enabled) {
|
|
1204
|
+
await this.backupRamdiskToMaster();
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1093
1207
|
this.logger.success('Server stopped');
|
|
1094
1208
|
}
|
|
1095
1209
|
|
|
@@ -1200,33 +1314,4 @@ world-settings:
|
|
|
1200
1314
|
}
|
|
1201
1315
|
return value;
|
|
1202
1316
|
}
|
|
1203
|
-
|
|
1204
|
-
private async calculateWorldSize(): Promise<number> {
|
|
1205
|
-
try {
|
|
1206
|
-
const worldPath = path.join(process.cwd(), this.config.folders.world);
|
|
1207
|
-
if (!await fs.pathExists(worldPath)) return 0;
|
|
1208
|
-
|
|
1209
|
-
const getSize = async (dir: string): Promise<number> => {
|
|
1210
|
-
let total = 0;
|
|
1211
|
-
const files = await fs.readdir(dir);
|
|
1212
|
-
|
|
1213
|
-
for (const file of files) {
|
|
1214
|
-
const filePath = path.join(dir, file);
|
|
1215
|
-
const stat = await fs.stat(filePath);
|
|
1216
|
-
|
|
1217
|
-
if (stat.isDirectory()) {
|
|
1218
|
-
total += await getSize(filePath);
|
|
1219
|
-
} else {
|
|
1220
|
-
total += stat.size;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return total;
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
return await getSize(worldPath);
|
|
1228
|
-
} catch {
|
|
1229
|
-
return 0;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
1317
|
}
|