import { execSync, exec } from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as https from 'https'; import * as http from 'http'; import * as os from 'os'; import * as tar from 'tar'; export interface JavaInfo { path: string; version: string; type: 'system' | 'portable'; } export class JavaChecker { private static readonly JAVA_DIR = JavaChecker.getOptimalJavaDirectory(); private static readonly JAVA_URLS = { '21': { 'x64': [ 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jre_x64_linux_hotspot_21.0.2_13.tar.gz', 'https://corretto.aws/downloads/latest/amazon-corretto-21-x64-linux-jre.tar.gz', 'https://download.bell-sw.com/java/21.0.2+13/bellsoft-jre21.0.2+13-linux-amd64.tar.gz' ], 'arm64': [ 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jre_aarch64_linux_hotspot_21.0.2_13.tar.gz', 'https://corretto.aws/downloads/latest/amazon-corretto-21-aarch64-linux-jre.tar.gz', 'https://download.bell-sw.com/java/21.0.2+13/bellsoft-jre21.0.2+13-linux-aarch64.tar.gz' ], 'arm': [ 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jre_arm_linux_hotspot_21.0.2_13.tar.gz' ] }, '17': { 'x64': [ 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jre_x64_linux_hotspot_17.0.10_7.tar.gz', 'https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jre.tar.gz', 'https://download.bell-sw.com/java/17.0.10+13/bellsoft-jre17.0.10+13-linux-amd64.tar.gz' ], 'arm64': [ 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jre_aarch64_linux_hotspot_17.0.10_7.tar.gz', 'https://corretto.aws/downloads/latest/amazon-corretto-17-aarch64-linux-jre.tar.gz', 'https://download.bell-sw.com/java/17.0.10+13/bellsoft-jre17.0.10+13-linux-aarch64.tar.gz' ], 'arm': [ 'https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jre_arm_linux_hotspot_17.0.10_7.tar.gz' ] } }; private static getOptimalJavaDirectory(): string { const pterodactylDir = '/home/container/.java'; const tmpDir = '/tmp/.mc-headless'; try { if (fs.existsSync('/home/container')) { return pterodactylDir; } } catch { } const totalRamGB = os.totalmem() / 1024 / 1024 / 1024; if (totalRamGB < 3) { console.warn(`Warning: Low RAM (${totalRamGB.toFixed(1)} GB). Java in /tmp may cause issues.`); } return tmpDir; } private static getArch(): 'x64' | 'arm64' | 'arm' { const arch = process.arch; if (arch === 'arm64') return 'arm64'; if (arch === 'arm') return 'arm'; return 'x64'; } public static getRequiredJavaVersion(serverVersion: string): '17' | '21' { if (serverVersion.startsWith('1.21') || serverVersion === '1.21.11') { return '21'; } if (serverVersion.startsWith('1.20') || serverVersion.startsWith('1.19') || serverVersion.startsWith('1.18')) { return '17'; } return '21'; } public static async checkJava(): Promise { return new Promise((resolve) => { exec('which java', (error, stdout) => { if (error || !stdout.trim()) { resolve(false); } else { resolve(true); } }); }); } public static getSystemJavaPath(): string | null { try { const stdout = execSync('which java').toString().trim(); return stdout || null; } catch { return null; } } public static getSystemJavaVersion(): string | null { try { const output = execSync('java -version 2>&1').toString(); const versionRegex = /(?:version|openjdk|java)[\s"]+(\d+)(?:\.(\d+))?/i; const match = output.match(versionRegex); if (match) { const majorVersion = parseInt(match[1]); if (majorVersion === 21) return '21'; if (majorVersion === 17) return '17'; if (majorVersion === 11) return '11'; if (majorVersion === 8) return '8'; if (majorVersion === 1 && match[2] === '8') return '8'; } if (output.includes('version "21') || output.includes('openjdk 21')) return '21'; if (output.includes('version "17') || output.includes('openjdk 17')) return '17'; if (output.includes('version "11') || output.includes('openjdk 11')) return '11'; if (output.includes('version "1.8') || output.includes('openjdk 1.8')) return '8'; return null; } catch { return null; } } private static shouldUpdateJava(javaHome: string): boolean { try { const stats = fs.statSync(javaHome); const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24); return ageInDays > 30; } catch { return true; } } private static async downloadAndExtractWithStream(url: string, javaHome: string, retryCount = 0): Promise { const maxRetries = 3; const userAgents = [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36' ]; return new Promise((resolve, reject) => { console.log(`Downloading Java...`); const protocol = url.startsWith('https') ? https : http; let isDone = false; let cleanup = () => { if (!isDone) { isDone = true; fs.remove(javaHome).catch(() => {}); } }; const request = protocol.get(url, { headers: { 'User-Agent': userAgents[retryCount % userAgents.length], 'Accept': 'application/octet-stream', 'Accept-Encoding': 'gzip, deflate' } }, (response) => { if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) { const location = response.headers.location; if (location && retryCount < maxRetries) { this.downloadAndExtractWithStream(location, javaHome, retryCount + 1).then(resolve).catch(reject); } else { cleanup(); reject(new Error('Redirect failed')); } return; } if (response.statusCode !== 200) { if (response.statusCode === 429 && retryCount < maxRetries) { setTimeout(() => { this.downloadAndExtractWithStream(url, javaHome, retryCount + 1).then(resolve).catch(reject); }, 10000); } else if (response.statusCode && response.statusCode >= 500 && retryCount < maxRetries) { setTimeout(() => { this.downloadAndExtractWithStream(url, javaHome, retryCount + 1).then(resolve).catch(reject); }, 10000); } else { cleanup(); reject(new Error(`Download failed: ${response.statusCode}`)); } return; } const contentLength = response.headers['content-length']; let downloadedBytes = 0; fs.ensureDirSync(javaHome); const extractor = tar.extract({ cwd: javaHome, strip: 1 }); let extractError: Error | null = null; extractor.on('finish', () => { if (!isDone) { isDone = true; resolve(); } }); extractor.on('error', (err) => { extractError = err; cleanup(); reject(err); }); response.on('data', (chunk) => { downloadedBytes += chunk.length; }); response.on('error', (err) => { extractor.end(); cleanup(); reject(err); }); response.on('end', () => { if (contentLength) { const expected = parseInt(contentLength); if (downloadedBytes < expected * 0.95) { cleanup(); reject(new Error(`Download incomplete: ${downloadedBytes} of ${expected} bytes`)); return; } } extractor.on('finish', () => { if (!isDone && !extractError) { isDone = true; resolve(); } }); }); response.pipe(extractor); }); request.on('error', (err) => { if (retryCount < maxRetries) { setTimeout(() => { this.downloadAndExtractWithStream(url, javaHome, retryCount + 1).then(resolve).catch(reject); }, 5000); } else { cleanup(); reject(err); } }); request.setTimeout(300000, () => { request.destroy(); cleanup(); reject(new Error('Download timeout')); }); request.end(); }); } public static async getOrDownloadPortableJava(version: '17' | '21'): Promise { await fs.ensureDir(this.JAVA_DIR); const arch = this.getArch(); const javaHome = path.join(this.JAVA_DIR, `jre-${version}-${arch}`); const javaBin = path.join(javaHome, 'bin', 'java'); if (await fs.pathExists(javaBin)) { if (!this.shouldUpdateJava(javaHome)) { return { path: javaBin, version: version, type: 'portable' }; } console.log('Java update available, downloading latest...'); await fs.remove(javaHome); } const archUrls = this.JAVA_URLS[version]?.[arch]; if (!archUrls || archUrls.length === 0) { throw new Error(`No Java download for ${version} on ${arch}`); } let lastError: Error | null = null; for (let i = 0; i < archUrls.length; i++) { const url = archUrls[i]; try { await fs.remove(javaHome); await fs.ensureDir(javaHome); await this.downloadAndExtractWithStream(url, javaHome); if (!await fs.pathExists(javaBin)) { throw new Error('Java binary not found after extraction'); } await fs.chmod(javaBin, 0o755); try { execSync(`"${javaBin}" -version 2>&1`); } catch { await fs.remove(javaHome); throw new Error('Java verification failed'); } return { path: javaBin, version: version, type: 'portable' }; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); console.log(`Download attempt ${i + 1} failed, trying next source...`); await fs.remove(javaHome).catch(() => {}); } } throw new Error(`Failed to download Java: ${lastError?.message}`); } public static async ensureJava(serverVersion: string, usePortable: boolean = true): Promise { const requiredVersion = this.getRequiredJavaVersion(serverVersion); if (!usePortable) { const systemJava = this.getSystemJavaPath(); if (systemJava) { const systemVersion = this.getSystemJavaVersion(); if (systemVersion === requiredVersion) { return { path: systemJava, version: systemVersion, type: 'system' }; } } } return this.getOrDownloadPortableJava(requiredVersion); } public static cleanupOldJava(): void { try { if (!fs.existsSync(this.JAVA_DIR)) return; const files = fs.readdirSync(this.JAVA_DIR); const now = Date.now(); const oneDay = 24 * 60 * 60 * 1000; files.forEach(file => { const filePath = path.join(this.JAVA_DIR, file); try { const stats = fs.statSync(filePath); if (now - stats.mtimeMs > oneDay) { if (stats.isDirectory()) { fs.removeSync(filePath); } } } catch { } }); } catch { } } public static getJavaInfo(): { dir: string; arch: string } { return { dir: this.JAVA_DIR, arch: this.getArch() }; } }