@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.
Files changed (35) hide show
  1. package/README.md +658 -765
  2. package/dist/core/JavaChecker.d.ts +8 -2
  3. package/dist/core/JavaChecker.d.ts.map +1 -1
  4. package/dist/core/JavaChecker.js +219 -104
  5. package/dist/core/JavaChecker.js.map +1 -1
  6. package/dist/core/MinecraftServer.d.ts +17 -32
  7. package/dist/core/MinecraftServer.d.ts.map +1 -1
  8. package/dist/core/MinecraftServer.js +449 -186
  9. package/dist/core/MinecraftServer.js.map +1 -1
  10. package/dist/index.d.ts +19 -18
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +40 -18
  13. package/dist/index.js.map +1 -1
  14. package/dist/platforms/GeyserBridge.d.ts +21 -3
  15. package/dist/platforms/GeyserBridge.d.ts.map +1 -1
  16. package/dist/platforms/GeyserBridge.js +160 -60
  17. package/dist/platforms/GeyserBridge.js.map +1 -1
  18. package/dist/platforms/PluginManager.d.ts +16 -0
  19. package/dist/platforms/PluginManager.d.ts.map +1 -0
  20. package/dist/platforms/PluginManager.js +208 -0
  21. package/dist/platforms/PluginManager.js.map +1 -0
  22. package/dist/platforms/ViaVersion.d.ts +1 -7
  23. package/dist/platforms/ViaVersion.d.ts.map +1 -1
  24. package/dist/platforms/ViaVersion.js +23 -87
  25. package/dist/platforms/ViaVersion.js.map +1 -1
  26. package/dist/types/index.d.ts +189 -0
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/package.json +4 -2
  29. package/src/core/JavaChecker.ts +224 -108
  30. package/src/core/MinecraftServer.ts +540 -254
  31. package/src/index.ts +19 -19
  32. package/src/platforms/GeyserBridge.ts +196 -80
  33. package/src/platforms/PluginManager.ts +206 -0
  34. package/src/platforms/ViaVersion.ts +26 -107
  35. package/src/types/index.ts +206 -0
@@ -3,8 +3,8 @@ import * as fs from 'fs-extra';
3
3
  import * as path from 'path';
4
4
  import * as https from 'https';
5
5
  import * as http from 'http';
6
+ import * as os from 'os';
6
7
  import * as tar from 'tar';
7
- import { Logger } from '../utils/Logger';
8
8
 
9
9
  export interface JavaInfo {
10
10
  path: string;
@@ -13,21 +13,67 @@ export interface JavaInfo {
13
13
  }
14
14
 
15
15
  export class JavaChecker {
16
- private static logger = Logger.getInstance();
17
- private static readonly JAVA_DIR = path.join(process.cwd(), '.java');
16
+ private static readonly JAVA_DIR = JavaChecker.getOptimalJavaDirectory();
18
17
 
19
- // Hanya pakai URL yang reliable dan direct download
20
18
  private static readonly JAVA_URLS = {
21
- '21': [
22
- 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_x64_linux_hotspot_21.0.2_13.tar.gz',
23
- 'https://corretto.aws/downloads/latest/amazon-corretto-21-x64-linux-jre.tar.gz'
24
- ],
25
- '17': [
26
- 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.10_7.tar.gz',
27
- 'https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jre.tar.gz'
28
- ]
19
+ '21': {
20
+ 'x64': [
21
+ 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jre_x64_linux_hotspot_21.0.2_13.tar.gz',
22
+ 'https://corretto.aws/downloads/latest/amazon-corretto-21-x64-linux-jre.tar.gz',
23
+ 'https://download.bell-sw.com/java/21.0.2+13/bellsoft-jre21.0.2+13-linux-amd64.tar.gz'
24
+ ],
25
+ 'arm64': [
26
+ 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jre_aarch64_linux_hotspot_21.0.2_13.tar.gz',
27
+ 'https://corretto.aws/downloads/latest/amazon-corretto-21-aarch64-linux-jre.tar.gz',
28
+ 'https://download.bell-sw.com/java/21.0.2+13/bellsoft-jre21.0.2+13-linux-aarch64.tar.gz'
29
+ ],
30
+ 'arm': [
31
+ 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jre_arm_linux_hotspot_21.0.2_13.tar.gz'
32
+ ]
33
+ },
34
+ '17': {
35
+ 'x64': [
36
+ 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jre_x64_linux_hotspot_17.0.10_7.tar.gz',
37
+ 'https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jre.tar.gz',
38
+ 'https://download.bell-sw.com/java/17.0.10+13/bellsoft-jre17.0.10+13-linux-amd64.tar.gz'
39
+ ],
40
+ 'arm64': [
41
+ 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jre_aarch64_linux_hotspot_17.0.10_7.tar.gz',
42
+ 'https://corretto.aws/downloads/latest/amazon-corretto-17-aarch64-linux-jre.tar.gz',
43
+ 'https://download.bell-sw.com/java/17.0.10+13/bellsoft-jre17.0.10+13-linux-aarch64.tar.gz'
44
+ ],
45
+ 'arm': [
46
+ 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jre_arm_linux_hotspot_17.0.10_7.tar.gz'
47
+ ]
48
+ }
29
49
  };
30
50
 
51
+ private static getOptimalJavaDirectory(): string {
52
+ const pterodactylDir = '/home/container/.java';
53
+ const tmpDir = '/tmp/.mc-headless';
54
+
55
+ try {
56
+ if (fs.existsSync('/home/container')) {
57
+ return pterodactylDir;
58
+ }
59
+ } catch {
60
+ }
61
+
62
+ const totalRamGB = os.totalmem() / 1024 / 1024 / 1024;
63
+ if (totalRamGB < 3) {
64
+ console.warn(`Warning: Low RAM (${totalRamGB.toFixed(1)} GB). Java in /tmp may cause issues.`);
65
+ }
66
+
67
+ return tmpDir;
68
+ }
69
+
70
+ private static getArch(): 'x64' | 'arm64' | 'arm' {
71
+ const arch = process.arch;
72
+ if (arch === 'arm64') return 'arm64';
73
+ if (arch === 'arm') return 'arm';
74
+ return 'x64';
75
+ }
76
+
31
77
  public static getRequiredJavaVersion(serverVersion: string): '17' | '21' {
32
78
  if (serverVersion.startsWith('1.21') || serverVersion === '1.21.11') {
33
79
  return '21';
@@ -42,11 +88,8 @@ export class JavaChecker {
42
88
  return new Promise((resolve) => {
43
89
  exec('which java', (error, stdout) => {
44
90
  if (error || !stdout.trim()) {
45
- this.logger.warning('Java is not installed in system');
46
91
  resolve(false);
47
92
  } else {
48
- const javaPath = stdout.trim();
49
- this.logger.success(`System Java found at: ${javaPath}`);
50
93
  resolve(true);
51
94
  }
52
95
  });
@@ -65,95 +108,166 @@ export class JavaChecker {
65
108
  public static getSystemJavaVersion(): string | null {
66
109
  try {
67
110
  const output = execSync('java -version 2>&1').toString();
68
- if (output.includes('version "21')) return '21';
69
- if (output.includes('version "17')) return '17';
70
- if (output.includes('version "11')) return '11';
71
- if (output.includes('version "1.8')) return '8';
72
- const match = output.match(/version "([^"]+)"/);
73
- return match ? match[1] : null;
111
+
112
+ const versionRegex = /(?:version|openjdk|java)[\s"]+(\d+)(?:\.(\d+))?/i;
113
+ const match = output.match(versionRegex);
114
+
115
+ if (match) {
116
+ const majorVersion = parseInt(match[1]);
117
+ if (majorVersion === 21) return '21';
118
+ if (majorVersion === 17) return '17';
119
+ if (majorVersion === 11) return '11';
120
+ if (majorVersion === 8) return '8';
121
+ if (majorVersion === 1 && match[2] === '8') return '8';
122
+ }
123
+
124
+ if (output.includes('version "21') || output.includes('openjdk 21')) return '21';
125
+ if (output.includes('version "17') || output.includes('openjdk 17')) return '17';
126
+ if (output.includes('version "11') || output.includes('openjdk 11')) return '11';
127
+ if (output.includes('version "1.8') || output.includes('openjdk 1.8')) return '8';
128
+
129
+ return null;
74
130
  } catch {
75
131
  return null;
76
132
  }
77
133
  }
78
134
 
79
- private static async downloadWithNode(url: string, destPath: string): Promise<void> {
135
+ private static shouldUpdateJava(javaHome: string): boolean {
136
+ try {
137
+ const stats = fs.statSync(javaHome);
138
+ const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24);
139
+ return ageInDays > 30;
140
+ } catch {
141
+ return true;
142
+ }
143
+ }
144
+
145
+ private static async downloadAndExtractWithStream(url: string, javaHome: string, retryCount = 0): Promise<void> {
146
+ const maxRetries = 3;
147
+ const userAgents = [
148
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
149
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
150
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36'
151
+ ];
152
+
80
153
  return new Promise((resolve, reject) => {
81
- this.logger.info(`Downloading Java from ${url}`);
154
+ console.log(`Downloading Java...`);
82
155
 
83
156
  const protocol = url.startsWith('https') ? https : http;
84
157
 
158
+ let isDone = false;
159
+ let cleanup = () => {
160
+ if (!isDone) {
161
+ isDone = true;
162
+ fs.remove(javaHome).catch(() => {});
163
+ }
164
+ };
165
+
85
166
  const request = protocol.get(url, {
86
167
  headers: {
87
- 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
168
+ 'User-Agent': userAgents[retryCount % userAgents.length],
169
+ 'Accept': 'application/octet-stream',
170
+ 'Accept-Encoding': 'gzip, deflate'
88
171
  }
89
172
  }, (response) => {
90
173
  if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
91
174
  const location = response.headers.location;
92
- if (location) {
93
- this.logger.info(`Redirecting to ${location}`);
94
- this.downloadWithNode(location, destPath).then(resolve).catch(reject);
95
- return;
175
+ if (location && retryCount < maxRetries) {
176
+ this.downloadAndExtractWithStream(location, javaHome, retryCount + 1).then(resolve).catch(reject);
177
+ } else {
178
+ cleanup();
179
+ reject(new Error('Redirect failed'));
96
180
  }
181
+ return;
97
182
  }
98
183
 
99
184
  if (response.statusCode !== 200) {
100
- reject(new Error(`Download failed: ${response.statusCode}`));
185
+ if (response.statusCode === 429 && retryCount < maxRetries) {
186
+ setTimeout(() => {
187
+ this.downloadAndExtractWithStream(url, javaHome, retryCount + 1).then(resolve).catch(reject);
188
+ }, 10000);
189
+ } else if (response.statusCode && response.statusCode >= 500 && retryCount < maxRetries) {
190
+ setTimeout(() => {
191
+ this.downloadAndExtractWithStream(url, javaHome, retryCount + 1).then(resolve).catch(reject);
192
+ }, 10000);
193
+ } else {
194
+ cleanup();
195
+ reject(new Error(`Download failed: ${response.statusCode}`));
196
+ }
101
197
  return;
102
198
  }
103
199
 
104
- const contentType = response.headers['content-type'];
105
- if (contentType && contentType.includes('text/html')) {
106
- reject(new Error('Received HTML instead of binary - likely license redirect'));
107
- return;
108
- }
200
+ const contentLength = response.headers['content-length'];
201
+ let downloadedBytes = 0;
109
202
 
110
- const file = fs.createWriteStream(destPath);
111
- const totalSize = parseInt(response.headers['content-length'] || '0');
112
- let downloadedSize = 0;
113
- let lastPercent = 0;
203
+ fs.ensureDirSync(javaHome);
114
204
 
115
- response.on('data', (chunk) => {
116
- downloadedSize += chunk.length;
117
- if (totalSize > 0) {
118
- const percent = ((downloadedSize / totalSize) * 100).toFixed(1);
119
- if (parseFloat(percent) >= lastPercent + 5 || parseFloat(percent) >= 100) {
120
- lastPercent = parseFloat(percent);
121
- process.stdout.write(`\rDownloading Java: ${percent}% (${(downloadedSize / 1024 / 1024).toFixed(1)} MB)`);
122
- }
123
- }
205
+ const extractor = tar.extract({
206
+ cwd: javaHome,
207
+ strip: 1
124
208
  });
125
209
 
126
- response.pipe(file);
210
+ let extractError: Error | null = null;
127
211
 
128
- file.on('finish', () => {
129
- process.stdout.write('\n');
130
- file.close();
131
-
132
- const stats = fs.statSync(destPath);
133
- if (stats.size < 1000000) {
134
- fs.unlinkSync(destPath);
135
- reject(new Error(`Downloaded file too small: ${stats.size} bytes - possibly HTML page`));
136
- return;
212
+ extractor.on('finish', () => {
213
+ if (!isDone) {
214
+ isDone = true;
215
+ resolve();
137
216
  }
138
-
139
- resolve();
140
217
  });
141
218
 
142
- file.on('error', (err) => {
143
- fs.unlink(destPath).catch(() => {});
219
+ extractor.on('error', (err) => {
220
+ extractError = err;
221
+ cleanup();
144
222
  reject(err);
145
223
  });
224
+
225
+ response.on('data', (chunk) => {
226
+ downloadedBytes += chunk.length;
227
+ });
228
+
229
+ response.on('error', (err) => {
230
+ extractor.end();
231
+ cleanup();
232
+ reject(err);
233
+ });
234
+
235
+ response.on('end', () => {
236
+ if (contentLength) {
237
+ const expected = parseInt(contentLength);
238
+ if (downloadedBytes < expected * 0.95) {
239
+ cleanup();
240
+ reject(new Error(`Download incomplete: ${downloadedBytes} of ${expected} bytes`));
241
+ return;
242
+ }
243
+ }
244
+
245
+ extractor.on('finish', () => {
246
+ if (!isDone && !extractError) {
247
+ isDone = true;
248
+ resolve();
249
+ }
250
+ });
251
+ });
252
+
253
+ response.pipe(extractor);
146
254
  });
147
255
 
148
256
  request.on('error', (err) => {
149
- fs.unlink(destPath).catch(() => {});
150
- reject(err);
257
+ if (retryCount < maxRetries) {
258
+ setTimeout(() => {
259
+ this.downloadAndExtractWithStream(url, javaHome, retryCount + 1).then(resolve).catch(reject);
260
+ }, 5000);
261
+ } else {
262
+ cleanup();
263
+ reject(err);
264
+ }
151
265
  });
152
266
 
153
267
  request.setTimeout(300000, () => {
154
268
  request.destroy();
155
- fs.unlink(destPath).catch(() => {});
156
- reject(new Error('Download timeout after 5 minutes'));
269
+ cleanup();
270
+ reject(new Error('Download timeout'));
157
271
  });
158
272
 
159
273
  request.end();
@@ -163,55 +277,50 @@ export class JavaChecker {
163
277
  public static async getOrDownloadPortableJava(version: '17' | '21'): Promise<JavaInfo> {
164
278
  await fs.ensureDir(this.JAVA_DIR);
165
279
 
166
- const javaHome = path.join(this.JAVA_DIR, `jre-${version}`);
280
+ const arch = this.getArch();
281
+ const javaHome = path.join(this.JAVA_DIR, `jre-${version}-${arch}`);
167
282
  const javaBin = path.join(javaHome, 'bin', 'java');
168
283
 
169
284
  if (await fs.pathExists(javaBin)) {
170
- this.logger.info(`Portable Java ${version} already exists at ${javaBin}`);
171
-
172
- try {
173
- const versionOutput = execSync(`"${javaBin}" -version 2>&1`).toString();
174
- this.logger.debug(`Portable Java version: ${versionOutput.split('\n')[0]}`);
285
+ if (!this.shouldUpdateJava(javaHome)) {
175
286
  return {
176
287
  path: javaBin,
177
288
  version: version,
178
289
  type: 'portable'
179
290
  };
180
- } catch (error) {
181
- this.logger.warning(`Existing Java seems corrupted, re-downloading...`);
182
- await fs.remove(javaHome);
183
291
  }
292
+ console.log('Java update available, downloading latest...');
293
+ await fs.remove(javaHome);
294
+ }
295
+
296
+ const archUrls = this.JAVA_URLS[version]?.[arch];
297
+ if (!archUrls || archUrls.length === 0) {
298
+ throw new Error(`No Java download for ${version} on ${arch}`);
184
299
  }
185
300
 
186
- const urls = this.JAVA_URLS[version] || this.JAVA_URLS['21'];
187
301
  let lastError: Error | null = null;
188
302
 
189
- for (let i = 0; i < urls.length; i++) {
190
- const url = urls[i];
191
- const tarPath = path.join(this.JAVA_DIR, `jre-${version}-${Date.now()}.tar.gz`);
303
+ for (let i = 0; i < archUrls.length; i++) {
304
+ const url = archUrls[i];
192
305
 
193
306
  try {
194
- this.logger.info(`Downloading portable Java ${version} (attempt ${i + 1}/${urls.length})...`);
195
- this.logger.info(`URL: ${url}`);
196
-
197
- await this.downloadWithNode(url, tarPath);
198
-
199
- this.logger.info(`Extracting Java ${version}...`);
307
+ await fs.remove(javaHome);
200
308
  await fs.ensureDir(javaHome);
201
- await tar.extract({
202
- file: tarPath,
203
- cwd: javaHome,
204
- strip: 1
205
- });
206
309
 
207
- await fs.remove(tarPath);
310
+ await this.downloadAndExtractWithStream(url, javaHome);
208
311
 
209
- await fs.chmod(javaBin, 0o755);
312
+ if (!await fs.pathExists(javaBin)) {
313
+ throw new Error('Java binary not found after extraction');
314
+ }
210
315
 
211
- this.logger.success(`Portable Java ${version} installed at ${javaBin}`);
316
+ await fs.chmod(javaBin, 0o755);
212
317
 
213
- const versionOutput = execSync(`"${javaBin}" -version 2>&1`).toString();
214
- this.logger.debug(`Portable Java version: ${versionOutput.split('\n')[0]}`);
318
+ try {
319
+ execSync(`"${javaBin}" -version 2>&1`);
320
+ } catch {
321
+ await fs.remove(javaHome);
322
+ throw new Error('Java verification failed');
323
+ }
215
324
 
216
325
  return {
217
326
  path: javaBin,
@@ -221,55 +330,62 @@ export class JavaChecker {
221
330
 
222
331
  } catch (error) {
223
332
  lastError = error instanceof Error ? error : new Error(String(error));
224
- this.logger.warning(`Attempt ${i + 1} failed: ${lastError.message}`);
225
- await fs.remove(tarPath).catch(() => {});
333
+ console.log(`Download attempt ${i + 1} failed, trying next source...`);
334
+ await fs.remove(javaHome).catch(() => {});
226
335
  }
227
336
  }
228
337
 
229
- throw new Error(`Failed to download Java ${version} after ${urls.length} attempts. Last error: ${lastError?.message}`);
338
+ throw new Error(`Failed to download Java: ${lastError?.message}`);
230
339
  }
231
340
 
232
341
  public static async ensureJava(serverVersion: string, usePortable: boolean = true): Promise<JavaInfo> {
233
342
  const requiredVersion = this.getRequiredJavaVersion(serverVersion);
234
- this.logger.info(`Server requires Java ${requiredVersion}`);
235
343
 
236
344
  if (!usePortable) {
237
345
  const systemJava = this.getSystemJavaPath();
238
346
  if (systemJava) {
239
347
  const systemVersion = this.getSystemJavaVersion();
240
348
  if (systemVersion === requiredVersion) {
241
- this.logger.success(`Using system Java ${systemVersion} at ${systemJava}`);
242
349
  return {
243
350
  path: systemJava,
244
351
  version: systemVersion,
245
352
  type: 'system'
246
353
  };
247
- } else {
248
- this.logger.warning(`System Java is ${systemVersion}, but server needs Java ${requiredVersion}`);
249
354
  }
250
355
  }
251
356
  }
252
357
 
253
- this.logger.info(`Downloading portable Java ${requiredVersion}...`);
254
358
  return this.getOrDownloadPortableJava(requiredVersion);
255
359
  }
256
360
 
257
361
  public static cleanupOldJava(): void {
258
362
  try {
363
+ if (!fs.existsSync(this.JAVA_DIR)) return;
364
+
259
365
  const files = fs.readdirSync(this.JAVA_DIR);
260
366
  const now = Date.now();
261
367
  const oneDay = 24 * 60 * 60 * 1000;
262
368
 
263
369
  files.forEach(file => {
264
370
  const filePath = path.join(this.JAVA_DIR, file);
265
- const stats = fs.statSync(filePath);
266
- if (now - stats.mtimeMs > oneDay) {
267
- fs.removeSync(filePath);
268
- this.logger.debug(`Cleaned up old Java: ${file}`);
371
+ try {
372
+ const stats = fs.statSync(filePath);
373
+ if (now - stats.mtimeMs > oneDay) {
374
+ if (stats.isDirectory()) {
375
+ fs.removeSync(filePath);
376
+ }
377
+ }
378
+ } catch {
269
379
  }
270
380
  });
271
- } catch (error) {
272
- this.logger.debug('No old Java to cleanup');
381
+ } catch {
273
382
  }
274
383
  }
384
+
385
+ public static getJavaInfo(): { dir: string; arch: string } {
386
+ return {
387
+ dir: this.JAVA_DIR,
388
+ arch: this.getArch()
389
+ };
390
+ }
275
391
  }