@dimzxzzx07/mc-headless 2.2.2 → 2.2.4
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 +424 -336
- 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 +503 -414
- 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
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private generateOptimizedConfigs(): void {
|
|
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');
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
399
|
+
this.logger.warning(`Failed to clean logs: ${errorMessage}`);
|
|
328
400
|
}
|
|
329
401
|
}
|
|
330
402
|
|
|
331
|
-
private
|
|
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,22 +434,55 @@ 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
|
+
'-Dio.netty.recycler.maxCapacity.default=0',
|
|
453
|
+
'-Dio.netty.leakDetectionLevel=disabled',
|
|
454
|
+
'-Dterminal.jline=false',
|
|
455
|
+
'-Dterminal.ansi=true'
|
|
381
456
|
];
|
|
382
457
|
|
|
458
|
+
if (this.options.networkOptimization?.preferIPv4) {
|
|
459
|
+
baseArgs.unshift('-Djava.net.preferIPv4Stack=true');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (this.options.networkOptimization?.tcpFastOpen) {
|
|
463
|
+
baseArgs.unshift('-Djava.net.tcpFastOpen=true');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (this.options.networkOptimization?.tcpNoDelay) {
|
|
467
|
+
baseArgs.unshift('-Djava.net.tcp.nodelay=true');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (this.options.networkOptimization?.compressionThreshold) {
|
|
471
|
+
baseArgs.push(`-Dnetwork-compression-threshold=${this.options.networkOptimization.compressionThreshold}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (this.options.networkOptimization?.velocity?.enabled) {
|
|
475
|
+
baseArgs.push('-Dvelocity.secret=' + this.options.networkOptimization.velocity.secret);
|
|
476
|
+
}
|
|
477
|
+
|
|
383
478
|
let gcArgs: string[] = [];
|
|
384
479
|
|
|
385
480
|
if (memMax >= 16384) {
|
|
386
481
|
gcArgs = [
|
|
387
|
-
'-XX:+ParallelRefProcEnabled',
|
|
388
|
-
'-XX:MaxGCPauseMillis=100',
|
|
389
482
|
'-XX:+UnlockExperimentalVMOptions',
|
|
483
|
+
'-XX:+UseStringDeduplication',
|
|
484
|
+
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
485
|
+
'-XX:+ParallelRefProcEnabled',
|
|
390
486
|
'-XX:+DisableExplicitGC',
|
|
391
487
|
'-XX:+AlwaysPreTouch',
|
|
392
488
|
'-XX:G1NewSizePercent=40',
|
|
@@ -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',
|
|
@@ -404,9 +499,10 @@ world-settings:
|
|
|
404
499
|
];
|
|
405
500
|
} else if (memMax >= 8192) {
|
|
406
501
|
gcArgs = [
|
|
407
|
-
'-XX:+ParallelRefProcEnabled',
|
|
408
|
-
'-XX:MaxGCPauseMillis=150',
|
|
409
502
|
'-XX:+UnlockExperimentalVMOptions',
|
|
503
|
+
'-XX:+UseStringDeduplication',
|
|
504
|
+
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
505
|
+
'-XX:+ParallelRefProcEnabled',
|
|
410
506
|
'-XX:+DisableExplicitGC',
|
|
411
507
|
'-XX:+AlwaysPreTouch',
|
|
412
508
|
'-XX:G1NewSizePercent=30',
|
|
@@ -416,30 +512,32 @@ world-settings:
|
|
|
416
512
|
'-XX:G1HeapWastePercent=5',
|
|
417
513
|
'-XX:G1MixedGCCountTarget=4',
|
|
418
514
|
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
419
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
420
515
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
421
516
|
'-XX:SurvivorRatio=32',
|
|
422
517
|
'-XX:+PerfDisableSharedMem',
|
|
423
518
|
'-XX:MaxTenuringThreshold=1'
|
|
424
519
|
];
|
|
425
520
|
} else {
|
|
426
|
-
gcArgs =
|
|
427
|
-
'-XX:+ParallelRefProcEnabled',
|
|
428
|
-
'-XX:MaxGCPauseMillis=200',
|
|
521
|
+
gcArgs = [
|
|
429
522
|
'-XX:+UnlockExperimentalVMOptions',
|
|
523
|
+
'-XX:+UseStringDeduplication',
|
|
524
|
+
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
525
|
+
'-XX:+ParallelRefProcEnabled',
|
|
430
526
|
'-XX:+DisableExplicitGC',
|
|
431
527
|
'-XX:+AlwaysPreTouch',
|
|
432
528
|
'-XX:G1HeapWastePercent=5',
|
|
433
529
|
'-XX:G1MixedGCCountTarget=4',
|
|
434
530
|
'-XX:InitiatingHeapOccupancyPercent=15',
|
|
435
|
-
'-XX:G1MixedGCLiveThresholdPercent=90',
|
|
436
531
|
'-XX:G1RSetUpdatingPauseTimePercent=5',
|
|
437
532
|
'-XX:SurvivorRatio=32',
|
|
438
533
|
'-XX:+PerfDisableSharedMem',
|
|
439
|
-
'-XX:MaxTenuringThreshold=1'
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
534
|
+
'-XX:MaxTenuringThreshold=1'
|
|
535
|
+
];
|
|
536
|
+
|
|
537
|
+
if (this.config.memory.useAikarsFlags) {
|
|
538
|
+
gcArgs.push('-Dusing.aikars.flags=https://mcflags.emc.gs');
|
|
539
|
+
gcArgs.push('-Daikars.new.flags=true');
|
|
540
|
+
}
|
|
443
541
|
}
|
|
444
542
|
|
|
445
543
|
if (javaVersion === '21') {
|
|
@@ -472,167 +570,41 @@ world-settings:
|
|
|
472
570
|
return env;
|
|
473
571
|
}
|
|
474
572
|
|
|
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();
|
|
573
|
+
private setupLogHandlers(): void {
|
|
574
|
+
this.process.stdout.on('data', (data: Buffer) => {
|
|
575
|
+
const output = data.toString();
|
|
576
|
+
process.stdout.write(output);
|
|
598
577
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
578
|
+
if (!this.serverReady) {
|
|
579
|
+
if (output.includes('Done') || output.includes('For help, type "help"')) {
|
|
580
|
+
this.serverReady = true;
|
|
581
|
+
this.serverInfo.status = 'running';
|
|
582
|
+
this.logger.success('Server started successfully!');
|
|
583
|
+
|
|
584
|
+
if (this.options.enableViaVersion !== false) {
|
|
585
|
+
this.logger.info('ViaVersion is active - players from older versions can connect');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
589
|
+
this.logger.info('SkinRestorer is active - player skins will be restored');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (this.worldSize > 0) {
|
|
593
|
+
this.logger.info(`World size: ${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
594
|
+
}
|
|
602
595
|
|
|
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
|
-
}
|
|
596
|
+
if (this.owners.size > 0) {
|
|
597
|
+
this.logger.info(`Owner commands enabled with prefix: ${this.ownerCommandPrefix}`);
|
|
598
|
+
}
|
|
619
599
|
|
|
620
|
-
|
|
621
|
-
|
|
600
|
+
if (this.options.ramdisk?.enabled) {
|
|
601
|
+
this.logger.info('RAM disk active - worlds/plugins running in memory');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.emit('ready', this.serverInfo);
|
|
605
|
+
this.startMemoryMonitor();
|
|
622
606
|
}
|
|
623
|
-
|
|
624
|
-
this.emit('ready', this.serverInfo);
|
|
625
|
-
this.startMemoryMonitor();
|
|
626
607
|
}
|
|
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
608
|
|
|
637
609
|
if (output.includes('joined the game')) {
|
|
638
610
|
const match = output.match(/(\w+) joined the game/);
|
|
@@ -969,7 +941,8 @@ world-settings:
|
|
|
969
941
|
this.emit('resource', this.serverInfo);
|
|
970
942
|
|
|
971
943
|
} catch (error) {
|
|
972
|
-
|
|
944
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
945
|
+
this.logger.error(`Stats update error: ${errorMessage}`);
|
|
973
946
|
}
|
|
974
947
|
}
|
|
975
948
|
|
|
@@ -1003,24 +976,23 @@ world-settings:
|
|
|
1003
976
|
if (isIncreasing) {
|
|
1004
977
|
this.logger.warning('Memory leak detected!');
|
|
1005
978
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
default:
|
|
1017
|
-
this.logger.warning('Please restart server to free memory');
|
|
979
|
+
if (action === 'restart' && this.players.size === 0) {
|
|
980
|
+
this.logger.info('No players online, restarting server due to memory leak...');
|
|
981
|
+
await this.gracefulRestart();
|
|
982
|
+
} else if (action === 'stop' && this.players.size === 0) {
|
|
983
|
+
this.logger.info('No players online, stopping server due to memory leak...');
|
|
984
|
+
await this.stop();
|
|
985
|
+
} else if (action === 'warn') {
|
|
986
|
+
this.logger.warning('Please restart server to free memory');
|
|
987
|
+
} else {
|
|
988
|
+
this.logger.warning('Cannot restart: players online');
|
|
1018
989
|
}
|
|
1019
990
|
}
|
|
1020
991
|
}
|
|
1021
992
|
|
|
1022
993
|
} catch (error) {
|
|
1023
|
-
|
|
994
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
995
|
+
this.logger.error(`Memory monitor error: ${errorMessage}`);
|
|
1024
996
|
}
|
|
1025
997
|
}, interval);
|
|
1026
998
|
}
|
|
@@ -1050,6 +1022,145 @@ world-settings:
|
|
|
1050
1022
|
await this.start();
|
|
1051
1023
|
}
|
|
1052
1024
|
|
|
1025
|
+
public async start(): Promise<ServerInfo> {
|
|
1026
|
+
this.logger.info(`Starting ${this.config.type} server v${this.config.version}...`);
|
|
1027
|
+
|
|
1028
|
+
await this.setupSymlinkMaster();
|
|
1029
|
+
await this.setupRamdisk();
|
|
1030
|
+
|
|
1031
|
+
if (this.options.cleanLogsInterval && this.options.cleanLogsInterval > 0) {
|
|
1032
|
+
this.cleanLogsCron = cron.schedule('0 */3 * * *', () => this.cleanOldLogs());
|
|
1033
|
+
this.logger.info('Log cleanup scheduled every 3 hours');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (this.owners.size > 0) {
|
|
1037
|
+
this.logger.info(`Owners configured: ${Array.from(this.owners).join(', ')}`);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
this.javaInfo = await JavaChecker.ensureJava(
|
|
1041
|
+
this.config.version,
|
|
1042
|
+
this.options.usePortableJava || false
|
|
1043
|
+
);
|
|
1044
|
+
this.javaCommand = this.javaInfo.path;
|
|
1045
|
+
this.logger.success(`Using Java ${this.javaInfo.version} (${this.javaInfo.type}) at ${this.javaInfo.path}`);
|
|
1046
|
+
|
|
1047
|
+
const systemInfo = SystemDetector.getSystemInfo();
|
|
1048
|
+
this.logger.debug('System info:', systemInfo);
|
|
1049
|
+
|
|
1050
|
+
const serverDir = process.cwd();
|
|
1051
|
+
await FileUtils.ensureServerStructure(this.config);
|
|
1052
|
+
|
|
1053
|
+
this.worldSize = await this.calculateWorldSize();
|
|
1054
|
+
if (this.worldSize > 10 * 1024 * 1024 * 1024) {
|
|
1055
|
+
this.logger.warning(`Large world detected (${(this.worldSize / 1024 / 1024 / 1024).toFixed(2)} GB). Consider increasing memory allocation.`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const jarPath = await this.engine.download(this.config, serverDir);
|
|
1059
|
+
|
|
1060
|
+
if (this.config.type === 'forge') {
|
|
1061
|
+
await this.engine.prepare(this.config, serverDir, jarPath);
|
|
1062
|
+
} else {
|
|
1063
|
+
await this.engine.prepare(this.config, serverDir);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (this.config.platform === 'all') {
|
|
1067
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
1068
|
+
await this.geyser.setup(this.config, this.isPterodactyl);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const pluginsToInstall = [
|
|
1072
|
+
{ name: 'ProtocolLib', enabled: this.options.enableProtocolLib, url: 'https://github.com/dmulloy2/ProtocolLib/releases/latest/download/ProtocolLib.jar' },
|
|
1073
|
+
{ name: 'TCPShield', enabled: this.options.enableTCPShield, url: 'https://tcpshield.com/api/download/plugin' },
|
|
1074
|
+
{ name: 'SpigotOptimizers', enabled: this.options.enableSpigotOptimizers, url: 'https://github.com/SpigotOptimizers/SpigotOptimizers/releases/latest/download/SpigotOptimizers.jar' },
|
|
1075
|
+
{ name: 'Spark', enabled: this.options.enableSpark, url: 'https://spark.lucko.me/download' },
|
|
1076
|
+
{ name: 'ViewDistanceTweaks', enabled: this.options.enableViewDistanceTweaks, url: 'https://github.com/ViewDistanceTweaks/ViewDistanceTweaks/releases/latest/download/ViewDistanceTweaks.jar' },
|
|
1077
|
+
{ name: 'FarmControl', enabled: this.options.enableFarmControl, url: 'https://github.com/FarmControl/FarmControl/releases/latest/download/FarmControl.jar' },
|
|
1078
|
+
{ name: 'EntityDetection', enabled: this.options.enableEntityDetection, url: 'https://github.com/EntityDetection/EntityDetection/releases/latest/download/EntityDetection.jar' }
|
|
1079
|
+
];
|
|
1080
|
+
|
|
1081
|
+
for (const plugin of pluginsToInstall) {
|
|
1082
|
+
if (plugin.enabled) {
|
|
1083
|
+
await this.pluginManager.installPlugin(plugin.name, plugin.url, this.config.folders.plugins);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (this.options.enableViaVersion !== false) {
|
|
1088
|
+
this.logger.info('Enabling ViaVersion for client version compatibility...');
|
|
1089
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
1090
|
+
await this.viaVersion.setup(this.config);
|
|
1091
|
+
await this.viaVersion.configureViaVersion(this.config);
|
|
1092
|
+
|
|
1093
|
+
if (this.options.enableViaBackwards !== false) {
|
|
1094
|
+
this.logger.info('ViaBackwards will be installed');
|
|
1095
|
+
}
|
|
1096
|
+
if (this.options.enableViaRewind !== false) {
|
|
1097
|
+
this.logger.info('ViaRewind will be installed');
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (this.options.enableSkinRestorer !== false) {
|
|
1102
|
+
this.logger.info('Enabling SkinRestorer for player skins...');
|
|
1103
|
+
await FileUtils.ensureDir(this.config.folders.plugins);
|
|
1104
|
+
await this.skinRestorer.setup(this.config);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const javaArgs = this.buildJavaArgs();
|
|
1108
|
+
const serverJar = this.engine.getServerJar(jarPath);
|
|
1109
|
+
const serverArgs = this.engine.getServerArgs();
|
|
1110
|
+
|
|
1111
|
+
const fullArgs = [
|
|
1112
|
+
...javaArgs,
|
|
1113
|
+
'-jar',
|
|
1114
|
+
serverJar,
|
|
1115
|
+
...serverArgs
|
|
1116
|
+
];
|
|
1117
|
+
|
|
1118
|
+
const env = this.buildEnvironment();
|
|
1119
|
+
|
|
1120
|
+
this.logger.info(`Launching: ${this.javaCommand} ${fullArgs.join(' ')}`);
|
|
1121
|
+
|
|
1122
|
+
this.process = spawn(this.javaCommand, fullArgs, {
|
|
1123
|
+
cwd: serverDir,
|
|
1124
|
+
env: env,
|
|
1125
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
this.serverInfo.pid = this.process.pid!;
|
|
1129
|
+
this.serverInfo.status = 'starting';
|
|
1130
|
+
this.startTime = new Date();
|
|
1131
|
+
|
|
1132
|
+
if (this.options.silentMode) {
|
|
1133
|
+
this.process.stdout.pipe(process.stdout);
|
|
1134
|
+
this.process.stderr.pipe(process.stderr);
|
|
1135
|
+
} else {
|
|
1136
|
+
this.setupLogHandlers();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
this.process.on('exit', (code: number) => {
|
|
1140
|
+
this.serverInfo.status = 'stopped';
|
|
1141
|
+
this.logger.warning(`Server stopped with code ${code}`);
|
|
1142
|
+
|
|
1143
|
+
if (this.config.autoRestart && code !== 0) {
|
|
1144
|
+
this.logger.info('Auto-restarting...');
|
|
1145
|
+
this.start();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
this.emit('stop', { code });
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
if (this.options.statsInterval && this.options.statsInterval > 0) {
|
|
1152
|
+
this.statsInterval = setInterval(() => this.updateStats(), this.options.statsInterval);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
this.monitorResources();
|
|
1156
|
+
|
|
1157
|
+
if (this.config.backup.enabled) {
|
|
1158
|
+
this.setupBackups();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return this.serverInfo;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1053
1164
|
public async stop(): Promise<void> {
|
|
1054
1165
|
if (!this.process) {
|
|
1055
1166
|
this.logger.warning('Server not running');
|
|
@@ -1077,6 +1188,10 @@ world-settings:
|
|
|
1077
1188
|
this.cleanLogsCron.stop();
|
|
1078
1189
|
}
|
|
1079
1190
|
|
|
1191
|
+
if (this.ramdiskBackupCron) {
|
|
1192
|
+
this.ramdiskBackupCron.stop();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1080
1195
|
if (this.memoryMonitorInterval) {
|
|
1081
1196
|
clearInterval(this.memoryMonitorInterval);
|
|
1082
1197
|
}
|
|
@@ -1089,7 +1204,10 @@ world-settings:
|
|
|
1089
1204
|
this.geyser.stop();
|
|
1090
1205
|
}
|
|
1091
1206
|
|
|
1092
|
-
this.
|
|
1207
|
+
if (this.options.ramdisk?.enabled) {
|
|
1208
|
+
await this.backupRamdiskToMaster();
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1093
1211
|
this.logger.success('Server stopped');
|
|
1094
1212
|
}
|
|
1095
1213
|
|
|
@@ -1200,33 +1318,4 @@ world-settings:
|
|
|
1200
1318
|
}
|
|
1201
1319
|
return value;
|
|
1202
1320
|
}
|
|
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
1321
|
}
|