diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee394f1..408f0d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,15 +12,15 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v1 + uses: actions/checkout@v3 - name: Set up Node - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: 18 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.x diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css index e95bcb2..8b23931 100644 --- a/app/assets/css/launcher.css +++ b/app/assets/css/launcher.css @@ -3772,6 +3772,7 @@ input:checked + .toggleSwitchSlider:before { font-size: 10px; line-height: 10px; font-weight: bold; + text-align: left; } /* Content container for the server listing's information. */ diff --git a/app/assets/js/assetexec.js b/app/assets/js/assetexec.js deleted file mode 100644 index f1af291..0000000 --- a/app/assets/js/assetexec.js +++ /dev/null @@ -1,74 +0,0 @@ -let target = require('./assetguard')[process.argv[2]] -if(target == null){ - process.send({context: 'error', data: null, error: 'Invalid class name'}) - console.error('Invalid class name passed to argv[2], cannot continue.') - process.exit(1) -} -let tracker = new target(...(process.argv.splice(3))) - -const { LoggerUtil } = require('helios-core') -const logger = LoggerUtil.getLogger('AssetExec') - -//const tracker = new AssetGuard(process.argv[2], process.argv[3]) -logger.info('AssetExec Started') - -// Temporary for debug purposes. -process.on('unhandledRejection', r => console.log(r)) - -let percent = 0 -function assignListeners(){ - tracker.on('validate', (data) => { - process.send({context: 'validate', data}) - }) - tracker.on('progress', (data, acc, total) => { - const currPercent = parseInt((acc/total) * 100) - if (currPercent !== percent) { - percent = currPercent - process.send({context: 'progress', data, value: acc, total, percent}) - } - }) - tracker.on('complete', (data, ...args) => { - process.send({context: 'complete', data, args}) - }) - tracker.on('error', (data, error) => { - process.send({context: 'error', data, error}) - }) -} - -assignListeners() - -process.on('message', (msg) => { - if(msg.task === 'execute'){ - const func = msg.function - let nS = tracker[func] // Nonstatic context - let iS = target[func] // Static context - if(typeof nS === 'function' || typeof iS === 'function'){ - const f = typeof nS === 'function' ? nS : iS - const res = f.apply(f === nS ? tracker : null, msg.argsArr) - if(res instanceof Promise){ - res.then((v) => { - process.send({result: v, context: func}) - }).catch((err) => { - process.send({result: err.message || err, context: func}) - }) - } else { - process.send({result: res, context: func}) - } - } else { - process.send({context: 'error', data: null, error: `Function ${func} not found on ${process.argv[2]}`}) - } - } else if(msg.task === 'changeContext'){ - target = require('./assetguard')[msg.class] - if(target == null){ - process.send({context: 'error', data: null, error: `Invalid class ${msg.class}`}) - } else { - tracker = new target(...(msg.args)) - assignListeners() - } - } -}) - -process.on('disconnect', () => { - logger.info('AssetExec Disconnected') - process.exit(0) -}) \ No newline at end of file diff --git a/app/assets/js/assetguard.js b/app/assets/js/assetguard.js deleted file mode 100644 index d447854..0000000 --- a/app/assets/js/assetguard.js +++ /dev/null @@ -1,1911 +0,0 @@ -// Requirements -const AdmZip = require('adm-zip') -const async = require('async') -const child_process = require('child_process') -const crypto = require('crypto') -const EventEmitter = require('events') -const fs = require('fs-extra') -const { LoggerUtil } = require('helios-core') -const nodeDiskInfo = require('node-disk-info') -const StreamZip = require('node-stream-zip') -const path = require('path') -const Registry = require('winreg') -const request = require('request') -const tar = require('tar-fs') -const zlib = require('zlib') - -const ConfigManager = require('./configmanager') -const DistroManager = require('./distromanager') -const isDev = require('./isdev') - -const isARM64 = process.arch === 'arm64' - -// Classes - -/** Class representing a base asset. */ -class Asset { - /** - * Create an asset. - * - * @param {any} id The id of the asset. - * @param {string} hash The hash value of the asset. - * @param {number} size The size in bytes of the asset. - * @param {string} from The url where the asset can be found. - * @param {string} to The absolute local file path of the asset. - */ - constructor(id, hash, size, from, to){ - this.id = id - this.hash = hash - this.size = size - this.from = from - this.to = to - } -} - -/** Class representing a mojang library. */ -class Library extends Asset { - - /** - * Converts the process.platform OS names to match mojang's OS names. - */ - static mojangFriendlyOS(){ - const opSys = process.platform - if (opSys === 'darwin') { - return 'osx' - } else if (opSys === 'win32'){ - return 'windows' - } else if (opSys === 'linux'){ - return 'linux' - } else { - return 'unknown_os' - } - } - - /** - * Checks whether or not a library is valid for download on a particular OS, following - * the rule format specified in the mojang version data index. If the allow property has - * an OS specified, then the library can ONLY be downloaded on that OS. If the disallow - * property has instead specified an OS, the library can be downloaded on any OS EXCLUDING - * the one specified. - * - * If the rules are undefined, the natives property will be checked for a matching entry - * for the current OS. - * - * @param {Array.} rules The Library's download rules. - * @param {Object} natives The Library's natives object. - * @returns {boolean} True if the Library follows the specified rules, otherwise false. - */ - static validateRules(rules, natives){ - if(rules == null) { - if(natives == null) { - return true - } else { - return natives[Library.mojangFriendlyOS()] != null - } - } - - for(let rule of rules){ - const action = rule.action - const osProp = rule.os - if(action != null && osProp != null){ - const osName = osProp.name - const osMoj = Library.mojangFriendlyOS() - if(action === 'allow'){ - return osName === osMoj - } else if(action === 'disallow'){ - return osName !== osMoj - } - } - } - return true - } -} - -class DistroModule extends Asset { - - /** - * Create a DistroModule. This is for processing, - * not equivalent to the module objects in the - * distro index. - * - * @param {any} id The id of the asset. - * @param {string} hash The hash value of the asset. - * @param {number} size The size in bytes of the asset. - * @param {string} from The url where the asset can be found. - * @param {string} to The absolute local file path of the asset. - * @param {string} type The the module type. - */ - constructor(id, hash, size, from, to, type){ - super(id, hash, size, from, to) - this.type = type - } - -} - -/** - * Class representing a download tracker. This is used to store meta data - * about a download queue, including the queue itself. - */ -class DLTracker { - - /** - * Create a DLTracker - * - * @param {Array.} dlqueue An array containing assets queued for download. - * @param {number} dlsize The combined size of each asset in the download queue array. - * @param {function(Asset)} callback Optional callback which is called when an asset finishes downloading. - */ - constructor(dlqueue, dlsize, callback = null){ - this.dlqueue = dlqueue - this.dlsize = dlsize - this.callback = callback - } - -} - -class Util { - - /** - * Returns true if the actual version is greater than - * or equal to the desired version. - * - * @param {string} desired The desired version. - * @param {string} actual The actual version. - */ - static mcVersionAtLeast(desired, actual){ - const des = desired.split('.') - const act = actual.split('.') - - for(let i=0; i= parseInt(des[i]))){ - return false - } - } - return true - } - - static isForgeGradle3(mcVersion, forgeVersion) { - - if(Util.mcVersionAtLeast('1.13', mcVersion)) { - return true - } - - try { - - const forgeVer = forgeVersion.split('-')[1] - - const maxFG2 = [14, 23, 5, 2847] - const verSplit = forgeVer.split('.').map(v => Number(v)) - - for(let i=0; i maxFG2[i]) { - return true - } else if(verSplit[i] < maxFG2[i]) { - return false - } - } - - return false - - } catch(err) { - throw new Error('Forge version is complex (changed).. launcher requires a patch.') - } - } - - static isAutoconnectBroken(forgeVersion) { - - const minWorking = [31, 2, 15] - const verSplit = forgeVersion.split('.').map(v => Number(v)) - - if(verSplit[0] === 31) { - for(let i=0; i minWorking[i]) { - return false - } else if(verSplit[i] < minWorking[i]) { - return true - } - } - } - - return false - } - -} - - -class JavaGuard extends EventEmitter { - - constructor(mcVersion){ - super() - this.mcVersion = mcVersion - this.logger = LoggerUtil.getLogger('JavaGuard') - } - - /** - * @typedef OpenJDKData - * @property {string} uri The base uri of the JRE. - * @property {number} size The size of the download. - * @property {string} name The name of the artifact. - */ - - /** - * Fetch the last open JDK binary. - * - * HOTFIX: Uses Corretto 8 for macOS. - * See: https://github.com/dscalzi/HeliosLauncher/issues/70 - * See: https://github.com/AdoptOpenJDK/openjdk-support/issues/101 - * - * @param {string} major The major version of Java to fetch. - * - * @returns {Promise.} Promise which resolved to an object containing the JRE download data. - */ - static _latestOpenJDK(major = '8'){ - - if(process.platform === 'darwin') { - return this._latestCorretto(major) - } else { - return this._latestAdoptium(major) - } - } - - static _latestAdoptium(major) { - - const majorNum = Number(major) - const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform) - const url = `https://api.adoptium.net/v3/assets/latest/${major}/hotspot?vendor=eclipse` - - return new Promise((resolve, reject) => { - request({url, json: true}, (err, resp, body) => { - if(!err && body.length > 0){ - - const targetBinary = body.find(entry => { - return entry.version.major === majorNum - && entry.binary.os === sanitizedOS - && entry.binary.image_type === 'jdk' - && entry.binary.architecture === 'x64' - }) - - if(targetBinary != null) { - resolve({ - uri: targetBinary.binary.package.link, - size: targetBinary.binary.package.size, - name: targetBinary.binary.package.name - }) - } else { - resolve(null) - } - } else { - resolve(null) - } - }) - }) - } - - static _latestCorretto(major) { - - let sanitizedOS, ext - - switch(process.platform) { - case 'win32': - sanitizedOS = 'windows' - ext = 'zip' - break - case 'darwin': - sanitizedOS = 'macos' - ext = 'tar.gz' - break - case 'linux': - sanitizedOS = 'linux' - ext = 'tar.gz' - break - default: - sanitizedOS = process.platform - ext = 'tar.gz' - break - } - - const arch = isARM64 ? 'aarch64' : 'x64' - const url = `https://corretto.aws/downloads/latest/amazon-corretto-${major}-${arch}-${sanitizedOS}-jdk.${ext}` - - return new Promise((resolve, reject) => { - request.head({url, json: true}, (err, resp) => { - if(!err && resp.statusCode === 200){ - resolve({ - uri: url, - size: parseInt(resp.headers['content-length']), - name: url.substr(url.lastIndexOf('/')+1) - }) - } else { - resolve(null) - } - }) - }) - - } - - /** - * Returns the path of the OS-specific executable for the given Java - * installation. Supported OS's are win32, darwin, linux. - * - * @param {string} rootDir The root directory of the Java installation. - * @returns {string} The path to the Java executable. - */ - static javaExecFromRoot(rootDir){ - if(process.platform === 'win32'){ - return path.join(rootDir, 'bin', 'javaw.exe') - } else if(process.platform === 'darwin'){ - return path.join(rootDir, 'Contents', 'Home', 'bin', 'java') - } else if(process.platform === 'linux'){ - return path.join(rootDir, 'bin', 'java') - } - return rootDir - } - - /** - * Check to see if the given path points to a Java executable. - * - * @param {string} pth The path to check against. - * @returns {boolean} True if the path points to a Java executable, otherwise false. - */ - static isJavaExecPath(pth){ - if(pth == null) { - return false - } - if(process.platform === 'win32'){ - return pth.endsWith(path.join('bin', 'javaw.exe')) - } else if(process.platform === 'darwin'){ - return pth.endsWith(path.join('bin', 'java')) - } else if(process.platform === 'linux'){ - return pth.endsWith(path.join('bin', 'java')) - } - return false - } - - /** - * Load Mojang's launcher.json file. - * - * @returns {Promise.} Promise which resolves to Mojang's launcher.json object. - */ - static loadMojangLauncherData(){ - return new Promise((resolve, reject) => { - request.get('https://launchermeta.mojang.com/mc/launcher.json', (err, resp, body) => { - if(err){ - resolve(null) - } else { - resolve(JSON.parse(body)) - } - }) - }) - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Dynamically detects the formatting - * to use. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static parseJavaRuntimeVersion(verString){ - const major = verString.split('.')[0] - if(major == 1){ - return JavaGuard._parseJavaRuntimeVersion_8(verString) - } else { - return JavaGuard._parseJavaRuntimeVersion_9(verString) - } - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Uses Java 8 formatting. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static _parseJavaRuntimeVersion_8(verString){ - // 1.{major}.0_{update}-b{build} - // ex. 1.8.0_152-b16 - const ret = {} - let pts = verString.split('-') - ret.build = parseInt(pts[1].substring(1)) - pts = pts[0].split('_') - ret.update = parseInt(pts[1]) - ret.major = parseInt(pts[0].split('.')[1]) - return ret - } - - /** - * Parses a **full** Java Runtime version string and resolves - * the version information. Uses Java 9+ formatting. - * - * @param {string} verString Full version string to parse. - * @returns Object containing the version information. - */ - static _parseJavaRuntimeVersion_9(verString){ - // {major}.{minor}.{revision}+{build} - // ex. 10.0.2+13 - const ret = {} - let pts = verString.split('+') - ret.build = parseInt(pts[1]) - pts = pts[0].split('.') - ret.major = parseInt(pts[0]) - ret.minor = parseInt(pts[1]) - ret.revision = parseInt(pts[2]) - return ret - } - - /** - * Validates the output of a JVM's properties. Currently validates that a JRE is x64 - * and that the major = 8, update > 52. - * - * @param {string} stderr The output to validate. - * - * @returns {Promise.} A promise which resolves to a meta object about the JVM. - * The validity is stored inside the `valid` property. - */ - _validateJVMProperties(stderr){ - const res = stderr - const props = res.split('\n') - - const goal = 2 - let checksum = 0 - - const meta = {} - - for(let i=0; i -1){ - let arch = props[i].split('=')[1].trim() - arch = parseInt(arch) - this.logger.debug(props[i].trim()) - if(arch === 64){ - meta.arch = arch - ++checksum - if(checksum === goal){ - break - } - } - } else if(props[i].indexOf('java.runtime.version') > -1){ - let verString = props[i].split('=')[1].trim() - this.logger.debug(props[i].trim()) - const verOb = JavaGuard.parseJavaRuntimeVersion(verString) - // TODO implement a support matrix eventually. Right now this is good enough - // 1.7-1.16 = Java 8 - // 1.17+ = Java 17 - // Actual support may vary, but we're going with this rule for simplicity. - if(verOb.major < 9){ - // Java 8 - if(!Util.mcVersionAtLeast('1.17', this.mcVersion)){ - if(verOb.major === 8 && verOb.update > 52){ - meta.version = verOb - ++checksum - if(checksum === goal){ - break - } - } - } - } else if(verOb.major >= 17) { - // Java 9+ - if(Util.mcVersionAtLeast('1.17', this.mcVersion)){ - meta.version = verOb - ++checksum - if(checksum === goal){ - break - } - } - } - // Space included so we get only the vendor. - } else if(props[i].lastIndexOf('java.vendor ') > -1) { - let vendorName = props[i].split('=')[1].trim() - this.logger.debug(props[i].trim()) - meta.vendor = vendorName - } else if (props[i].indexOf('os.arch') > -1) { - meta.isARM = props[i].split('=')[1].trim() === 'aarch64' - } - } - - meta.valid = checksum === goal - - return meta - } - - /** - * Validates that a Java binary is at least 64 bit. This makes use of the non-standard - * command line option -XshowSettings:properties. The output of this contains a property, - * sun.arch.data.model = ARCH, in which ARCH is either 32 or 64. This option is supported - * in Java 8 and 9. Since this is a non-standard option. This will resolve to true if - * the function's code throws errors. That would indicate that the option is changed or - * removed. - * - * @param {string} binaryExecPath Path to the java executable we wish to validate. - * - * @returns {Promise.} A promise which resolves to a meta object about the JVM. - * The validity is stored inside the `valid` property. - */ - _validateJavaBinary(binaryExecPath){ - - return new Promise((resolve, reject) => { - if(!JavaGuard.isJavaExecPath(binaryExecPath)){ - resolve({valid: false}) - } else if(fs.existsSync(binaryExecPath)){ - // Workaround (javaw.exe no longer outputs this information.) - this.logger.debug(typeof binaryExecPath) - if(binaryExecPath.indexOf('javaw.exe') > -1) { - binaryExecPath.replace('javaw.exe', 'java.exe') - } - child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => { - try { - // Output is stored in stderr? - resolve(this._validateJVMProperties(stderr)) - } catch (err){ - // Output format might have changed, validation cannot be completed. - resolve({valid: false}) - } - }) - } else { - resolve({valid: false}) - } - }) - - } - - /** - * Checks for the presence of the environment variable JAVA_HOME. If it exits, we will check - * to see if the value points to a path which exists. If the path exits, the path is returned. - * - * @returns {string} The path defined by JAVA_HOME, if it exists. Otherwise null. - */ - static _scanJavaHome(){ - const jHome = process.env.JAVA_HOME - try { - let res = fs.existsSync(jHome) - return res ? jHome : null - } catch (err) { - // Malformed JAVA_HOME property. - return null - } - } - - /** - * Scans the registry for 64-bit Java entries. The paths of each entry are added to - * a set and returned. Currently, only Java 8 (1.8) is supported. - * - * @returns {Promise.>} A promise which resolves to a set of 64-bit Java root - * paths found in the registry. - */ - static _scanRegistry(){ - - return new Promise((resolve, reject) => { - // Keys for Java v9.0.0 and later: - // 'SOFTWARE\\JavaSoft\\JRE' - // 'SOFTWARE\\JavaSoft\\JDK' - // Forge does not yet support Java 9, therefore we do not. - - // Keys for Java 1.8 and prior: - const regKeys = [ - '\\SOFTWARE\\JavaSoft\\Java Runtime Environment', - '\\SOFTWARE\\JavaSoft\\Java Development Kit' - ] - - let keysDone = 0 - - const candidates = new Set() - - for(let i=0; i { - if(exists) { - key.keys((err, javaVers) => { - if(err){ - keysDone++ - console.error(err) - - // REG KEY DONE - // DUE TO ERROR - if(keysDone === regKeys.length){ - resolve(candidates) - } - } else { - if(javaVers.length === 0){ - // REG KEY DONE - // NO SUBKEYS - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } else { - - let numDone = 0 - - for(let j=0; j { - const jHome = res.value - if(jHome.indexOf('(x86)') === -1){ - candidates.add(jHome) - } - - // SUBKEY DONE - - numDone++ - if(numDone === javaVers.length){ - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - }) - } else { - - // SUBKEY DONE - // NOT JAVA 8 - - numDone++ - if(numDone === javaVers.length){ - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - } - } - } - } - }) - } else { - - // REG KEY DONE - // DUE TO NON-EXISTANCE - - keysDone++ - if(keysDone === regKeys.length){ - resolve(candidates) - } - } - }) - } - - }) - - } - - /** - * See if JRE exists in the Internet Plug-Ins folder. - * - * @returns {string} The path of the JRE if found, otherwise null. - */ - static _scanInternetPlugins(){ - // /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java - const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin' - const res = fs.existsSync(JavaGuard.javaExecFromRoot(pth)) - return res ? pth : null - } - - /** - * Scan a directory for root JVM folders. - * - * @param {string} scanDir The directory to scan. - * @returns {Promise.>} A promise which resolves to a set of the discovered - * root JVM folders. - */ - static async _scanFileSystem(scanDir){ - - let res = new Set() - - if(await fs.pathExists(scanDir)) { - - const files = await fs.readdir(scanDir) - for(let i=0; i} rootSet A set of JVM root strings to validate. - * @returns {Promise.} A promise which resolves to an array of meta objects - * for each valid JVM root directory. - */ - async _validateJavaRootSet(rootSet){ - - const rootArr = Array.from(rootSet) - const validArr = [] - - for(let i=0; i { - - if(a.version.major === b.version.major){ - - if(a.version.major < 9){ - // Java 8 - if(a.version.update === b.version.update){ - if(a.version.build === b.version.build){ - - // Same version, give priority to JRE. - if(a.execPath.toLowerCase().indexOf('jdk') > -1){ - return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 - } else { - return -1 - } - - } else { - return a.version.build > b.version.build ? -1 : 1 - } - } else { - return a.version.update > b.version.update ? -1 : 1 - } - } else { - // Java 9+ - if(a.version.minor === b.version.minor){ - if(a.version.revision === b.version.revision){ - - // Same version, give priority to JRE. - if(a.execPath.toLowerCase().indexOf('jdk') > -1){ - return b.execPath.toLowerCase().indexOf('jdk') > -1 ? 0 : 1 - } else { - return -1 - } - - } else { - return a.version.revision > b.version.revision ? -1 : 1 - } - } else { - return a.version.minor > b.version.minor ? -1 : 1 - } - } - - } else { - return a.version.major > b.version.major ? -1 : 1 - } - }) - - return retArr - } - - /** - * Attempts to find a valid x64 installation of Java on Windows machines. - * Possible paths will be pulled from the registry and the JAVA_HOME environment - * variable. The paths will be sorted with higher versions preceeding lower, and - * JREs preceeding JDKs. The binaries at the sorted paths will then be validated. - * The first validated is returned. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _win32JavaValidate(dataDir){ - - // Get possible paths from the registry. - let pathSet1 = await JavaGuard._scanRegistry() - if(pathSet1.size === 0){ - - // Do a manual file system scan of program files. - // Check all drives - const driveMounts = nodeDiskInfo.getDiskInfoSync().map(({ mounted }) => mounted) - for(const mount of driveMounts) { - pathSet1 = new Set([ - ...pathSet1, - ...(await JavaGuard._scanFileSystem(`${mount}\\Program Files\\Java`)), - ...(await JavaGuard._scanFileSystem(`${mount}\\Program Files\\Eclipse Adoptium`)), - ...(await JavaGuard._scanFileSystem(`${mount}\\Program Files\\Eclipse Foundation`)), - ...(await JavaGuard._scanFileSystem(`${mount}\\Program Files\\AdoptOpenJDK`)) - ]) - } - - } - - // Get possible paths from the data directory. - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - // Merge the results. - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Validate JAVA_HOME. - const jHome = JavaGuard._scanJavaHome() - if(jHome != null && jHome.indexOf('(x86)') === -1){ - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - - } - - /** - * Attempts to find a valid x64 installation of Java on MacOS. - * The system JVM directory is scanned for possible installations. - * The JAVA_HOME enviroment variable and internet plugins directory - * are also scanned and validated. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - * - * Added: On the system with ARM architecture attempts to find aarch64 Java. - * - */ - async _darwinJavaValidate(dataDir){ - - const pathSet1 = await JavaGuard._scanFileSystem('/Library/Java/JavaVirtualMachines') - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Check Internet Plugins folder. - const iPPath = JavaGuard._scanInternetPlugins() - if(iPPath != null){ - uberSet.add(iPPath) - } - - // Check the JAVA_HOME environment variable. - let jHome = JavaGuard._scanJavaHome() - if(jHome != null){ - // Ensure we are at the absolute root. - if(jHome.contains('/Contents/Home')){ - jHome = jHome.substring(0, jHome.indexOf('/Contents/Home')) - } - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - - // TODO Revise this a bit, seems to work for now. Discovery logic should - // probably just filter out the invalid architectures before it even - // gets to this point. - if (isARM64) { - return pathArr.find(({ isARM }) => isARM)?.execPath ?? null - } else { - return pathArr.find(({ isARM }) => !isARM)?.execPath ?? null - } - - } else { - return null - } - } - - /** - * Attempts to find a valid x64 installation of Java on Linux. - * The system JVM directory is scanned for possible installations. - * The JAVA_HOME enviroment variable is also scanned and validated. - * - * Higher versions > Lower versions - * If versions are equal, JRE > JDK. - * - * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the executable path of a valid - * x64 Java installation. If none are found, null is returned. - */ - async _linuxJavaValidate(dataDir){ - - const pathSet1 = await JavaGuard._scanFileSystem('/usr/lib/jvm') - const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64')) - - const uberSet = new Set([...pathSet1, ...pathSet2]) - - // Validate JAVA_HOME - const jHome = JavaGuard._scanJavaHome() - if(jHome != null){ - uberSet.add(jHome) - } - - let pathArr = await this._validateJavaRootSet(uberSet) - pathArr = JavaGuard._sortValidJavaArray(pathArr) - - if(pathArr.length > 0){ - return pathArr[0].execPath - } else { - return null - } - } - - /** - * Retrieve the path of a valid x64 Java installation. - * - * @param {string} dataDir The base launcher directory. - * @returns {string} A path to a valid x64 Java installation, null if none found. - */ - async validateJava(dataDir){ - return await this['_' + process.platform + 'JavaValidate'](dataDir) - } - -} - - - - -/** - * Central object class used for control flow. This object stores data about - * categories of downloads. Each category is assigned an identifier with a - * DLTracker object as its value. Combined information is also stored, such as - * the total size of all the queued files in each category. This event is used - * to emit events so that external modules can listen into processing done in - * this module. - */ -class AssetGuard extends EventEmitter { - - static logger = LoggerUtil.getLogger('AssetGuard') - - /** - * Create an instance of AssetGuard. - * On creation the object's properties are never-null default - * values. Each identifier is resolved to an empty DLTracker. - * - * @param {string} commonPath The common path for shared game files. - * @param {string} javaexec The path to a java executable which will be used - * to finalize installation. - */ - constructor(commonPath, javaexec){ - super() - this.totaldlsize = 0 - this.progress = 0 - this.assets = new DLTracker([], 0) - this.libraries = new DLTracker([], 0) - this.files = new DLTracker([], 0) - this.forge = new DLTracker([], 0) - this.java = new DLTracker([], 0) - this.extractQueue = [] - this.commonPath = commonPath - this.javaexec = javaexec - } - - // Static Utility Functions - // #region - - // Static Hash Validation Functions - // #region - - /** - * Calculates the hash for a file using the specified algorithm. - * - * @param {Buffer} buf The buffer containing file data. - * @param {string} algo The hash algorithm. - * @returns {string} The calculated hash in hex. - */ - static _calculateHash(buf, algo){ - return crypto.createHash(algo).update(buf).digest('hex') - } - - /** - * Used to parse a checksums file. This is specifically designed for - * the checksums.sha1 files found inside the forge scala dependencies. - * - * @param {string} content The string content of the checksums file. - * @returns {Object} An object with keys being the file names, and values being the hashes. - */ - static _parseChecksumsFile(content){ - let finalContent = {} - let lines = content.split('\n') - for(let i=0; i} checksums The checksums listed in the forge version index. - * @returns {boolean} True if the file exists and the hashes match, otherwise false. - */ - static _validateForgeChecksum(filePath, checksums){ - if(fs.existsSync(filePath)){ - if(checksums == null || checksums.length === 0){ - return true - } - let buf = fs.readFileSync(filePath) - let calcdhash = AssetGuard._calculateHash(buf, 'sha1') - let valid = checksums.includes(calcdhash) - if(!valid && filePath.endsWith('.jar')){ - valid = AssetGuard._validateForgeJar(filePath, checksums) - } - return valid - } - return false - } - - /** - * Validates a forge jar file dependency who declares a checksums.sha1 file. - * This can be an expensive task as it usually requires that we calculate thousands - * of hashes. - * - * @param {Buffer} buf The buffer of the jar file. - * @param {Array.} checksums The checksums listed in the forge version index. - * @returns {boolean} True if all hashes declared in the checksums.sha1 file match the actual hashes. - */ - static _validateForgeJar(buf, checksums){ - // Double pass method was the quickest I found. I tried a version where we store data - // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time. - - const hashes = {} - let expected = {} - - const zip = new AdmZip(buf) - const zipEntries = zip.getEntries() - - //First pass - for(let i=0; i} filePaths The paths of the files to be extracted and unpacked. - * @returns {Promise.} An empty promise to indicate the extraction has completed. - */ - static _extractPackXZ(filePaths, javaExecutable){ - const extractLogger = LoggerUtil.getLogger('PackXZExtract') - extractLogger.info('Starting') - return new Promise((resolve, reject) => { - - let libPath - if(isDev){ - libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') - } else { - if(process.platform === 'darwin'){ - libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar') - } else { - libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar') - } - } - - const filePath = filePaths.join(',') - const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath]) - child.stdout.on('data', (data) => { - extractLogger.info(data.toString('utf8')) - }) - child.stderr.on('data', (data) => { - extractLogger.info(data.toString('utf8')) - }) - child.on('close', (code, signal) => { - extractLogger.info('Exited with code', code) - resolve() - }) - }) - } - - /** - * Function which finalizes the forge installation process. This creates a 'version' - * instance for forge and saves its version.json file into that instance. If that - * instance already exists, the contents of the version.json file are read and returned - * in a promise. - * - * @param {Asset} asset The Asset object representing Forge. - * @param {string} commonPath The common path for shared game files. - * @returns {Promise.} A promise which resolves to the contents of forge's version.json. - */ - static _finalizeForgeAsset(asset, commonPath){ - return new Promise((resolve, reject) => { - fs.readFile(asset.to, (err, data) => { - const zip = new AdmZip(data) - const zipEntries = zip.getEntries() - - for(let i=0; i} Promise which resolves to the version data object. - */ - loadVersionData(version, force = false){ - const self = this - return new Promise(async (resolve, reject) => { - const versionPath = path.join(self.commonPath, 'versions', version) - const versionFile = path.join(versionPath, version + '.json') - if(!fs.existsSync(versionFile) || force){ - const url = await self._getVersionDataUrl(version) - //This download will never be tracked as it's essential and trivial. - AssetGuard.logger.info('Preparing download of ' + version + ' assets.') - fs.ensureDirSync(versionPath) - const stream = request(url).pipe(fs.createWriteStream(versionFile)) - stream.on('finish', () => { - resolve(JSON.parse(fs.readFileSync(versionFile))) - }) - } else { - resolve(JSON.parse(fs.readFileSync(versionFile))) - } - }) - } - - /** - * Parses Mojang's version manifest and retrieves the url of the version - * data index. - * - * @param {string} version The version to lookup. - * @returns {Promise.} Promise which resolves to the url of the version data index. - * If the version could not be found, resolves to null. - */ - _getVersionDataUrl(version){ - return new Promise((resolve, reject) => { - request('https://launchermeta.mojang.com/mc/game/version_manifest.json', (error, resp, body) => { - if(error){ - reject(error) - } else { - const manifest = JSON.parse(body) - - for(let v of manifest.versions){ - if(v.id === version){ - resolve(v.url) - } - } - - resolve(null) - } - }) - }) - } - - - // Asset (Category=''') Validation Functions - // #region - - /** - * Public asset validation function. This function will handle the validation of assets. - * It will parse the asset index specified in the version data, analyzing each - * asset entry. In this analysis it will check to see if the local file exists and is valid. - * If not, it will be added to the download queue for the 'assets' identifier. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateAssets(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - self._assetChainIndexData(versionData, force).then(() => { - resolve() - }) - }) - } - - //Chain the asset tasks to provide full async. The below functions are private. - /** - * Private function used to chain the asset validation process. This function retrieves - * the index data. - * @param {Object} versionData - * @param {boolean} force - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - _assetChainIndexData(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - //Asset index constants. - const assetIndex = versionData.assetIndex - const name = assetIndex.id + '.json' - const indexPath = path.join(self.commonPath, 'assets', 'indexes') - const assetIndexLoc = path.join(indexPath, name) - - let data = null - if(!fs.existsSync(assetIndexLoc) || force){ - AssetGuard.logger.info('Downloading ' + versionData.id + ' asset index.') - fs.ensureDirSync(indexPath) - const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc)) - stream.on('finish', () => { - data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) - self._assetChainValidateAssets(versionData, data).then(() => { - resolve() - }) - }) - } else { - data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) - self._assetChainValidateAssets(versionData, data).then(() => { - resolve() - }) - } - }) - } - - /** - * Private function used to chain the asset validation process. This function processes - * the assets and enqueues missing or invalid files. - * @param {Object} versionData - * @param {boolean} force - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - _assetChainValidateAssets(versionData, indexData){ - const self = this - return new Promise((resolve, reject) => { - - //Asset constants - const resourceURL = 'https://resources.download.minecraft.net/' - const localPath = path.join(self.commonPath, 'assets') - const objectPath = path.join(localPath, 'objects') - - const assetDlQueue = [] - let dlSize = 0 - let acc = 0 - const total = Object.keys(indexData.objects).length - //const objKeys = Object.keys(data.objects) - async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => { - acc++ - self.emit('progress', 'assets', acc, total) - const hash = value.hash - const assetName = path.join(hash.substring(0, 2), hash) - const urlName = hash.substring(0, 2) + '/' + hash - const ast = new Asset(key, hash, value.size, resourceURL + urlName, path.join(objectPath, assetName)) - if(!AssetGuard._validateLocal(ast.to, 'sha1', ast.hash)){ - dlSize += (ast.size*1) - assetDlQueue.push(ast) - } - cb() - }, (err) => { - self.assets = new DLTracker(assetDlQueue, dlSize) - resolve() - }) - }) - } - - // #endregion - - // Library (Category=''') Validation Functions - // #region - - /** - * Public library validation function. This function will handle the validation of libraries. - * It will parse the version data, analyzing each library entry. In this analysis, it will - * check to see if the local file exists and is valid. If not, it will be added to the download - * queue for the 'libraries' identifier. - * - * @param {Object} versionData The version data for the assets. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateLibraries(versionData){ - const self = this - return new Promise((resolve, reject) => { - - const libArr = versionData.libraries - const libPath = path.join(self.commonPath, 'libraries') - - const libDlQueue = [] - let dlSize = 0 - - //Check validity of each library. If the hashs don't match, download the library. - async.eachLimit(libArr, 5, (lib, cb) => { - if(Library.validateRules(lib.rules, lib.natives)){ - let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))] - const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path)) - if(!AssetGuard._validateLocal(libItm.to, 'sha1', libItm.hash)){ - dlSize += (libItm.size*1) - libDlQueue.push(libItm) - } - } - cb() - }, (err) => { - self.libraries = new DLTracker(libDlQueue, dlSize) - resolve() - }) - }) - } - - // #endregion - - // Miscellaneous (Category=files) Validation Functions - // #region - - /** - * Public miscellaneous mojang file validation function. These files will be enqueued under - * the 'files' identifier. - * - * @param {Object} versionData The version data for the assets. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateMiscellaneous(versionData){ - const self = this - return new Promise(async (resolve, reject) => { - await self.validateClient(versionData) - await self.validateLogConfig(versionData) - resolve() - }) - } - - /** - * Validate client file - artifact renamed from client.jar to '{version}'.jar. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateClient(versionData, force = false){ - const self = this - return new Promise((resolve, reject) => { - const clientData = versionData.downloads.client - const version = versionData.id - const targetPath = path.join(self.commonPath, 'versions', version) - const targetFile = version + '.jar' - - let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, path.join(targetPath, targetFile)) - - if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){ - self.files.dlqueue.push(client) - self.files.dlsize += client.size*1 - resolve() - } else { - resolve() - } - }) - } - - /** - * Validate log config. - * - * @param {Object} versionData The version data for the assets. - * @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false. - * @returns {Promise.} An empty promise to indicate the async processing has completed. - */ - validateLogConfig(versionData){ - const self = this - return new Promise((resolve, reject) => { - const client = versionData.logging.client - const file = client.file - const targetPath = path.join(self.commonPath, 'assets', 'log_configs') - - let logConfig = new Asset(file.id, file.sha1, file.size, file.url, path.join(targetPath, file.id)) - - if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){ - self.files.dlqueue.push(logConfig) - self.files.dlsize += logConfig.size*1 - resolve() - } else { - resolve() - } - }) - } - - // #endregion - - // Distribution (Category=forge) Validation Functions - // #region - - /** - * Validate the distribution. - * - * @param {Server} server The Server to validate. - * @returns {Promise.} A promise which resolves to the server distribution object. - */ - validateDistribution(server){ - const self = this - return new Promise((resolve, reject) => { - self.forge = self._parseDistroModules(server.getModules(), server.getMinecraftVersion(), server.getID()) - resolve(server) - }) - } - - _parseDistroModules(modules, version, servid){ - let alist = [] - let asize = 0 - for(let ob of modules){ - let obArtifact = ob.getArtifact() - let obPath = obArtifact.getPath() - let artifact = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, ob.getType()) - const validationPath = obPath.toLowerCase().endsWith('.pack.xz') ? obPath.substring(0, obPath.toLowerCase().lastIndexOf('.pack.xz')) : obPath - if(!AssetGuard._validateLocal(validationPath, 'MD5', artifact.hash)){ - asize += artifact.size*1 - alist.push(artifact) - if(validationPath !== obPath) this.extractQueue.push(obPath) - } - //Recursively process the submodules then combine the results. - if(ob.getSubModules() != null){ - let dltrack = this._parseDistroModules(ob.getSubModules(), version, servid) - asize += dltrack.dlsize*1 - alist = alist.concat(dltrack.dlqueue) - } - } - - return new DLTracker(alist, asize) - } - - /** - * Loads Forge's version.json data into memory for the specified server id. - * - * @param {string} server The Server to load Forge data for. - * @returns {Promise.} A promise which resolves to Forge's version.json data. - */ - loadForgeData(server){ - const self = this - return new Promise(async (resolve, reject) => { - const modules = server.getModules() - for(let ob of modules){ - const type = ob.getType() - if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){ - if(Util.isForgeGradle3(server.getMinecraftVersion(), ob.getVersion())){ - // Read Manifest - for(let sub of ob.getSubModules()){ - if(sub.getType() === DistroManager.Types.VersionManifest){ - resolve(JSON.parse(fs.readFileSync(sub.getArtifact().getPath(), 'utf-8'))) - return - } - } - reject('No forge version manifest found!') - return - } else { - let obArtifact = ob.getArtifact() - let obPath = obArtifact.getPath() - let asset = new DistroModule(ob.getIdentifier(), obArtifact.getHash(), obArtifact.getSize(), obArtifact.getURL(), obPath, type) - try { - let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.commonPath) - resolve(forgeData) - } catch (err){ - reject(err) - } - return - } - } - } - reject('No forge module found!') - }) - } - - _parseForgeLibraries(){ - /* TODO - * Forge asset validations are already implemented. When there's nothing much - * to work on, implement forge downloads using forge's version.json. This is to - * have the code on standby if we ever need it (since it's half implemented already). - */ - } - - // #endregion - - // Java (Category=''') Validation (download) Functions - // #region - - _enqueueOpenJDK(dataDir, mcVersion){ - return new Promise((resolve, reject) => { - const major = Util.mcVersionAtLeast('1.17', mcVersion) ? '17' : '8' - JavaGuard._latestOpenJDK(major).then(verData => { - if(verData != null){ - - dataDir = path.join(dataDir, 'runtime', 'x64') - const fDir = path.join(dataDir, verData.name) - const jre = new Asset(verData.name, null, verData.size, verData.uri, fDir) - this.java = new DLTracker([jre], jre.size, (a, self) => { - if(verData.name.endsWith('zip')){ - - this._extractJdkZip(a.to, dataDir, self) - - } else { - // Tar.gz - let h = null - fs.createReadStream(a.to) - .on('error', err => AssetGuard.logger.error(err)) - .pipe(zlib.createGunzip()) - .on('error', err => AssetGuard.logger.error(err)) - .pipe(tar.extract(dataDir, { - map: (header) => { - if(h == null){ - h = header.name - } - } - })) - .on('error', err => AssetGuard.logger.error(err)) - .on('finish', () => { - fs.unlink(a.to, err => { - if(err){ - AssetGuard.logger.error(err) - } - if(h.indexOf('/') > -1){ - h = h.substring(0, h.indexOf('/')) - } - const pos = path.join(dataDir, h) - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - }) - }) - } - }) - resolve(true) - - } else { - resolve(false) - } - }) - }) - - } - - async _extractJdkZip(zipPath, runtimeDir, self) { - - const zip = new StreamZip.async({ - file: zipPath, - storeEntries: true - }) - - let pos = '' - try { - const entries = await zip.entries() - pos = path.join(runtimeDir, Object.keys(entries)[0]) - - AssetGuard.logger.info('Extracting jdk..') - await zip.extract(null, runtimeDir) - AssetGuard.logger.info('Cleaning up..') - await fs.remove(zipPath) - AssetGuard.logger.info('Jdk extraction complete.') - - } catch(err) { - AssetGuard.logger.error(err) - } finally { - zip.close() - self.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos)) - } - } - - // _enqueueMojangJRE(dir){ - // return new Promise((resolve, reject) => { - // // Mojang does not host the JRE for linux. - // if(process.platform === 'linux'){ - // resolve(false) - // } - // AssetGuard.loadMojangLauncherData().then(data => { - // if(data != null) { - - // try { - // const mJRE = data[Library.mojangFriendlyOS()]['64'].jre - // const url = mJRE.url - - // request.head(url, (err, resp, body) => { - // if(err){ - // resolve(false) - // } else { - // const name = url.substring(url.lastIndexOf('/')+1) - // const fDir = path.join(dir, name) - // const jre = new Asset('jre' + mJRE.version, mJRE.sha1, resp.headers['content-length'], url, fDir) - // this.java = new DLTracker([jre], jre.size, a => { - // fs.readFile(a.to, (err, data) => { - // // Data buffer needs to be decompressed from lzma, - // // not really possible using node.js - // }) - // }) - // } - // }) - // } catch (err){ - // resolve(false) - // } - - // } - // }) - // }) - // } - - - // #endregion - - // #endregion - - // Control Flow Functions - // #region - - /** - * Initiate an async download process for an AssetGuard DLTracker. - * - * @param {string} identifier The identifier of the AssetGuard DLTracker. - * @param {number} limit Optional. The number of async processes to run in parallel. - * @returns {boolean} True if the process began, otherwise false. - */ - startAsyncProcess(identifier, limit = 5){ - - const self = this - const dlTracker = this[identifier] - const dlQueue = dlTracker.dlqueue - - if(dlQueue.length > 0){ - AssetGuard.logger.info('DLQueue', dlQueue) - - async.eachLimit(dlQueue, limit, (asset, cb) => { - - fs.ensureDirSync(path.join(asset.to, '..')) - - let req = request(asset.from) - req.pause() - - req.on('response', (resp) => { - - if(resp.statusCode === 200){ - - let doHashCheck = false - const contentLength = parseInt(resp.headers['content-length']) - - if(contentLength !== asset.size){ - AssetGuard.logger.warn(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`) - doHashCheck = true - - // Adjust download - this.totaldlsize -= asset.size - this.totaldlsize += contentLength - } - - let writeStream = fs.createWriteStream(asset.to) - writeStream.on('close', () => { - if(dlTracker.callback != null){ - dlTracker.callback.apply(dlTracker, [asset, self]) - } - - if(doHashCheck){ - const v = AssetGuard._validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash) - if(v){ - AssetGuard.logger.warn(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`) - } else { - AssetGuard.logger.error(`Hashes do not match, ${asset.id} may be corrupted.`) - } - } - - cb() - }) - req.pipe(writeStream) - req.resume() - - } else { - - req.abort() - AssetGuard.logger.error(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`) - self.progress += asset.size*1 - self.emit('progress', 'download', self.progress, self.totaldlsize) - cb() - - } - - }) - - req.on('error', (err) => { - self.emit('error', 'download', err) - }) - - req.on('data', (chunk) => { - self.progress += chunk.length - self.emit('progress', 'download', self.progress, self.totaldlsize) - }) - - }, (err) => { - - if(err){ - AssetGuard.logger.warn('An item in ' + identifier + ' failed to process') - } else { - AssetGuard.logger.info('All ' + identifier + ' have been processed successfully') - } - - //self.totaldlsize -= dlTracker.dlsize - //self.progress -= dlTracker.dlsize - self[identifier] = new DLTracker([], 0) - - if(self.progress >= self.totaldlsize) { - if(self.extractQueue.length > 0){ - self.emit('progress', 'extract', 1, 1) - //self.emit('extracting') - AssetGuard._extractPackXZ(self.extractQueue, self.javaexec).then(() => { - self.extractQueue = [] - self.emit('complete', 'download') - }) - } else { - self.emit('complete', 'download') - } - } - - }) - - return true - - } else { - return false - } - } - - /** - * This function will initiate the download processed for the specified identifiers. If no argument is - * given, all identifiers will be initiated. Note that in order for files to be processed you need to run - * the processing function corresponding to that identifier. If you run this function without processing - * the files, it is likely nothing will be enqueued in the object and processing will complete - * immediately. Once all downloads are complete, this function will fire the 'complete' event on the - * global object instance. - * - * @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit. - */ - processDlQueues(identifiers = [{id:'assets', limit:20}, {id:'libraries', limit:5}, {id:'files', limit:5}, {id:'forge', limit:5}]){ - return new Promise((resolve, reject) => { - let shouldFire = true - - // Assign dltracking variables. - this.totaldlsize = 0 - this.progress = 0 - - for(let iden of identifiers){ - this.totaldlsize += this[iden.id].dlsize - } - - this.once('complete', (data) => { - resolve() - }) - - for(let iden of identifiers){ - let r = this.startAsyncProcess(iden.id, iden.limit) - if(r) shouldFire = false - } - - if(shouldFire){ - this.emit('complete', 'download') - } - }) - } - - async validateEverything(serverid, dev = false){ - - try { - if(!ConfigManager.isLoaded()){ - ConfigManager.load() - } - DistroManager.setDevMode(dev) - const dI = await DistroManager.pullLocal() - - const server = dI.getServer(serverid) - - // Validate Everything - - await this.validateDistribution(server) - this.emit('validate', 'distribution') - const versionData = await this.loadVersionData(server.getMinecraftVersion()) - this.emit('validate', 'version') - await this.validateAssets(versionData) - this.emit('validate', 'assets') - await this.validateLibraries(versionData) - this.emit('validate', 'libraries') - await this.validateMiscellaneous(versionData) - this.emit('validate', 'files') - await this.processDlQueues() - //this.emit('complete', 'download') - const forgeData = await this.loadForgeData(server) - - return { - versionData, - forgeData - } - - } catch (err){ - return { - versionData: null, - forgeData: null, - error: err - } - } - - - } - - // #endregion - -} - -module.exports = { - Util, - AssetGuard, - JavaGuard, - Asset, - Library -} diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js index c3f7489..38f864f 100644 --- a/app/assets/js/configmanager.js +++ b/app/assets/js/configmanager.js @@ -6,11 +6,10 @@ const path = require('path') const logger = LoggerUtil.getLogger('ConfigManager') const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) -// TODO change + const dataPath = path.join(sysRoot, '.helioslauncher') -// Forked processes do not have access to electron, so we have this workaround. -const launcherDir = process.env.CONFIG_DIRECT_PATH || require('@electron/remote').app.getPath('userData') +const launcherDir = require('@electron/remote').app.getPath('userData') /** * Retrieve the absolute path of the launcher directory. @@ -44,45 +43,30 @@ const configPath = path.join(exports.getLauncherDirectory(), 'config.json') const configPathLEGACY = path.join(dataPath, 'config.json') const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) -exports.getAbsoluteMinRAM = function(){ - const mem = os.totalmem() - return mem >= 6000000000 ? 3 : 2 -} - -exports.getAbsoluteMaxRAM = function(){ - const mem = os.totalmem() - const gT16 = mem-16000000000 - return Math.floor((mem-1000000000-(gT16 > 0 ? (Number.parseInt(gT16/8) + 16000000000/4) : mem/4))/1000000000) -} - -function resolveMaxRAM(){ - const mem = os.totalmem() - return mem >= 8000000000 ? '4G' : (mem >= 6000000000 ? '3G' : '2G') -} - -function resolveMinRAM(){ - return resolveMaxRAM() -} - -/** - * TODO Copy pasted, should be in a utility file. - * - * Returns true if the actual version is greater than - * or equal to the desired version. - * - * @param {string} desired The desired version. - * @param {string} actual The actual version. - */ -function mcVersionAtLeast(desired, actual){ - const des = desired.split('.') - const act = actual.split('.') - - for(let i=0; i= parseInt(des[i]))){ - return false - } +exports.getAbsoluteMinRAM = function(ram){ + if(ram?.minimum != null) { + return ram.minimum/1024 + } else { + // Legacy behavior + const mem = os.totalmem() + return mem >= (6*1073741824) ? 3 : 2 + } +} + +exports.getAbsoluteMaxRAM = function(ram){ + const mem = os.totalmem() + const gT16 = mem-(16*1073741824) + return Math.floor((mem-(gT16 > 0 ? (Number.parseInt(gT16/8) + (16*1073741824)/4) : mem/4))/1073741824) +} + +function resolveSelectedRAM(ram) { + if(ram?.recommended != null) { + return `${ram.recommended}M` + } else { + // Legacy behavior + const mem = os.totalmem() + return mem >= (8*1073741824) ? '4G' : (mem >= (6*1073741824) ? '3G' : '2G') } - return true } /** @@ -523,18 +507,18 @@ exports.setModConfiguration = function(serverid, configuration){ // Java Settings -function defaultJavaConfig(mcVersion) { - if(mcVersionAtLeast('1.17', mcVersion)) { - return defaultJavaConfig117() +function defaultJavaConfig(effectiveJavaOptions, ram) { + if(effectiveJavaOptions.suggestedMajor > 8) { + return defaultJavaConfig17(ram) } else { - return defaultJavaConfigBelow117() + return defaultJavaConfig8(ram) } } -function defaultJavaConfigBelow117() { +function defaultJavaConfig8(ram) { return { - minRAM: resolveMinRAM(), - maxRAM: resolveMaxRAM(), // Dynamic + minRAM: resolveSelectedRAM(ram), + maxRAM: resolveSelectedRAM(ram), executable: null, jvmOptions: [ '-XX:+UseConcMarkSweepGC', @@ -545,10 +529,10 @@ function defaultJavaConfigBelow117() { } } -function defaultJavaConfig117() { +function defaultJavaConfig17(ram) { return { - minRAM: resolveMinRAM(), - maxRAM: resolveMaxRAM(), // Dynamic + minRAM: resolveSelectedRAM(ram), + maxRAM: resolveSelectedRAM(ram), executable: null, jvmOptions: [ '-XX:+UnlockExperimentalVMOptions', @@ -567,9 +551,9 @@ function defaultJavaConfig117() { * @param {string} serverid The server id. * @param {*} mcVersion The minecraft version of the server. */ -exports.ensureJavaConfig = function(serverid, mcVersion) { +exports.ensureJavaConfig = function(serverid, effectiveJavaOptions, ram) { if(!Object.prototype.hasOwnProperty.call(config.javaConfig, serverid)) { - config.javaConfig[serverid] = defaultJavaConfig(mcVersion) + config.javaConfig[serverid] = defaultJavaConfig(effectiveJavaOptions, ram) } } diff --git a/app/assets/js/distromanager.js b/app/assets/js/distromanager.js index 4fcc685..8ae8ca0 100644 --- a/app/assets/js/distromanager.js +++ b/app/assets/js/distromanager.js @@ -1,621 +1,17 @@ -const fs = require('fs') -const path = require('path') -const request = require('request') -const { LoggerUtil } = require('helios-core') +const { DistributionAPI } = require('helios-core/common') const ConfigManager = require('./configmanager') -const logger = LoggerUtil.getLogger('DistroManager') - -/** - * Represents the download information - * for a specific module. - */ -class Artifact { - - /** - * Parse a JSON object into an Artifact. - * - * @param {Object} json A JSON object representing an Artifact. - * - * @returns {Artifact} The parsed Artifact. - */ - static fromJSON(json){ - return Object.assign(new Artifact(), json) - } - - /** - * Get the MD5 hash of the artifact. This value may - * be undefined for artifacts which are not to be - * validated and updated. - * - * @returns {string} The MD5 hash of the Artifact or undefined. - */ - getHash(){ - return this.MD5 - } - - /** - * @returns {number} The download size of the artifact. - */ - getSize(){ - return this.size - } - - /** - * @returns {string} The download url of the artifact. - */ - getURL(){ - return this.url - } - - /** - * @returns {string} The artifact's destination path. - */ - getPath(){ - return this.path - } - -} -exports.Artifact - -/** - * Represents a the requirement status - * of a module. - */ -class Required { - - /** - * Parse a JSON object into a Required object. - * - * @param {Object} json A JSON object representing a Required object. - * - * @returns {Required} The parsed Required object. - */ - static fromJSON(json){ - if(json == null){ - return new Required(true, true) - } else { - return new Required(json.value == null ? true : json.value, json.def == null ? true : json.def) - } - } - - constructor(value, def){ - this.value = value - this.default = def - } - - /** - * Get the default value for a required object. If a module - * is not required, this value determines whether or not - * it is enabled by default. - * - * @returns {boolean} The default enabled value. - */ - isDefault(){ - return this.default - } - - /** - * @returns {boolean} Whether or not the module is required. - */ - isRequired(){ - return this.value - } - -} -exports.Required - -/** - * Represents a module. - */ -class Module { - - /** - * Parse a JSON object into a Module. - * - * @param {Object} json A JSON object representing a Module. - * @param {string} serverid The ID of the server to which this module belongs. - * - * @returns {Module} The parsed Module. - */ - static fromJSON(json, serverid){ - return new Module(json.id, json.name, json.type, json.classpath, json.required, json.artifact, json.subModules, serverid) - } - - /** - * Resolve the default extension for a specific module type. - * - * @param {string} type The type of the module. - * - * @return {string} The default extension for the given type. - */ - static _resolveDefaultExtension(type){ - switch (type) { - case exports.Types.Library: - case exports.Types.ForgeHosted: - case exports.Types.LiteLoader: - case exports.Types.ForgeMod: - return 'jar' - case exports.Types.LiteMod: - return 'litemod' - case exports.Types.File: - default: - return 'jar' // There is no default extension really. - } - } - - constructor(id, name, type, classpath, required, artifact, subModules, serverid) { - this.identifier = id - this.type = type - this.classpath = classpath - this._resolveMetaData() - this.name = name - this.required = Required.fromJSON(required) - this.artifact = Artifact.fromJSON(artifact) - this._resolveArtifactPath(artifact.path, serverid) - this._resolveSubModules(subModules, serverid) - } - - _resolveMetaData(){ - try { - - const m0 = this.identifier.split('@') - - this.artifactExt = m0[1] || Module._resolveDefaultExtension(this.type) - - const m1 = m0[0].split(':') - - this.artifactClassifier = m1[3] || undefined - this.artifactVersion = m1[2] || '???' - this.artifactID = m1[1] || '???' - this.artifactGroup = m1[0] || '???' - - } catch (err) { - // Improper identifier - logger.error('Improper ID for module', this.identifier, err) - } - } - - _resolveArtifactPath(artifactPath, serverid){ - const pth = artifactPath == null ? path.join(...this.getGroup().split('.'), this.getID(), this.getVersion(), `${this.getID()}-${this.getVersion()}${this.artifactClassifier != undefined ? `-${this.artifactClassifier}` : ''}.${this.getExtension()}`) : artifactPath - - switch (this.type){ - case exports.Types.Library: - case exports.Types.ForgeHosted: - case exports.Types.LiteLoader: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'libraries', pth) - break - case exports.Types.ForgeMod: - case exports.Types.LiteMod: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'modstore', pth) - break - case exports.Types.VersionManifest: - this.artifact.path = path.join(ConfigManager.getCommonDirectory(), 'versions', this.getIdentifier(), `${this.getIdentifier()}.json`) - break - case exports.Types.File: - default: - this.artifact.path = path.join(ConfigManager.getInstanceDirectory(), serverid, pth) - break - } - - } - - _resolveSubModules(json, serverid){ - const arr = [] - if(json != null){ - for(let sm of json){ - arr.push(Module.fromJSON(sm, serverid)) - } - } - this.subModules = arr.length > 0 ? arr : null - } - - /** - * @returns {string} The full, unparsed module identifier. - */ - getIdentifier(){ - return this.identifier - } - - /** - * @returns {string} The name of the module. - */ - getName(){ - return this.name - } - - /** - * @returns {Required} The required object declared by this module. - */ - getRequired(){ - return this.required - } - - /** - * @returns {Artifact} The artifact declared by this module. - */ - getArtifact(){ - return this.artifact - } - - /** - * @returns {string} The maven identifier of this module's artifact. - */ - getID(){ - return this.artifactID - } - - /** - * @returns {string} The maven group of this module's artifact. - */ - getGroup(){ - return this.artifactGroup - } - - /** - * @returns {string} The identifier without he version or extension. - */ - getVersionlessID(){ - return this.getGroup() + ':' + this.getID() - } - - /** - * @returns {string} The identifier without the extension. - */ - getExtensionlessID(){ - return this.getIdentifier().split('@')[0] - } - - /** - * @returns {string} The version of this module's artifact. - */ - getVersion(){ - return this.artifactVersion - } - - /** - * @returns {string} The classifier of this module's artifact - */ - getClassifier(){ - return this.artifactClassifier - } - - /** - * @returns {string} The extension of this module's artifact. - */ - getExtension(){ - return this.artifactExt - } - - /** - * @returns {boolean} Whether or not this module has sub modules. - */ - hasSubModules(){ - return this.subModules != null - } - - /** - * @returns {Array.} An array of sub modules. - */ - getSubModules(){ - return this.subModules - } - - /** - * @returns {string} The type of the module. - */ - getType(){ - return this.type - } - - /** - * @returns {boolean} Whether or not this library should be on the classpath. - */ - getClasspath(){ - return this.classpath ?? true - } - -} -exports.Module - -/** - * Represents a server configuration. - */ -class Server { - - /** - * Parse a JSON object into a Server. - * - * @param {Object} json A JSON object representing a Server. - * - * @returns {Server} The parsed Server object. - */ - static fromJSON(json){ - - const mdls = json.modules - json.modules = [] - - const serv = Object.assign(new Server(), json) - serv._resolveModules(mdls) - - return serv - } - - _resolveModules(json){ - const arr = [] - for(let m of json){ - arr.push(Module.fromJSON(m, this.getID())) - } - this.modules = arr - } - - /** - * @returns {string} The ID of the server. - */ - getID(){ - return this.id - } - - /** - * @returns {string} The name of the server. - */ - getName(){ - return this.name - } - - /** - * @returns {string} The description of the server. - */ - getDescription(){ - return this.description - } - - /** - * @returns {string} The URL of the server's icon. - */ - getIcon(){ - return this.icon - } - - /** - * @returns {string} The version of the server configuration. - */ - getVersion(){ - return this.version - } - - /** - * @returns {string} The IP address of the server. - */ - getAddress(){ - return this.address - } - - /** - * @returns {string} The minecraft version of the server. - */ - getMinecraftVersion(){ - return this.minecraftVersion - } - - /** - * @returns {boolean} Whether or not this server is the main - * server. The main server is selected by the launcher when - * no valid server is selected. - */ - isMainServer(){ - return this.mainServer - } - - /** - * @returns {boolean} Whether or not the server is autoconnect. - * by default. - */ - isAutoConnect(){ - return this.autoconnect - } - - /** - * @returns {Array.} An array of modules for this server. - */ - getModules(){ - return this.modules - } - -} -exports.Server - -/** - * Represents the Distribution Index. - */ -class DistroIndex { - - /** - * Parse a JSON object into a DistroIndex. - * - * @param {Object} json A JSON object representing a DistroIndex. - * - * @returns {DistroIndex} The parsed Server object. - */ - static fromJSON(json){ - - const servers = json.servers - json.servers = [] - - const distro = Object.assign(new DistroIndex(), json) - distro._resolveServers(servers) - distro._resolveMainServer() - - return distro - } - - _resolveServers(json){ - const arr = [] - for(let s of json){ - arr.push(Server.fromJSON(s)) - } - this.servers = arr - } - - _resolveMainServer(){ - - for(let serv of this.servers){ - if(serv.mainServer){ - this.mainServer = serv.id - return - } - } - - // If no server declares default_selected, default to the first one declared. - this.mainServer = (this.servers.length > 0) ? this.servers[0].getID() : null - } - - /** - * @returns {string} The version of the distribution index. - */ - getVersion(){ - return this.version - } - - /** - * @returns {string} The URL to the news RSS feed. - */ - getRSS(){ - return this.rss - } - - /** - * @returns {Array.} An array of declared server configurations. - */ - getServers(){ - return this.servers - } - - /** - * Get a server configuration by its ID. If it does not - * exist, null will be returned. - * - * @param {string} id The ID of the server. - * - * @returns {Server} The server configuration with the given ID or null. - */ - getServer(id){ - for(let serv of this.servers){ - if(serv.id === id){ - return serv - } - } - return null - } - - /** - * Get the main server. - * - * @returns {Server} The main server. - */ - getMainServer(){ - return this.mainServer != null ? this.getServer(this.mainServer) : null - } - -} -exports.DistroIndex - -exports.Types = { - Library: 'Library', - ForgeHosted: 'ForgeHosted', - Forge: 'Forge', // Unimplemented - LiteLoader: 'LiteLoader', - ForgeMod: 'ForgeMod', - LiteMod: 'LiteMod', - File: 'File', - VersionManifest: 'VersionManifest' -} - -let DEV_MODE = false - -const DISTRO_PATH = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') -const DEV_PATH = path.join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json') - -let data = null - -/** - * @returns {Promise.} - */ -exports.pullRemote = function(){ - if(DEV_MODE){ - return exports.pullLocal() - } - return new Promise((resolve, reject) => { - const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json' - //const distroURL = 'https://gist.githubusercontent.com/dscalzi/53b1ba7a11d26a5c353f9d5ae484b71b/raw/' - const opts = { - url: distroURL, - timeout: 2500 - } - const distroDest = path.join(ConfigManager.getLauncherDirectory(), 'distribution.json') - request(opts, (error, resp, body) => { - if(!error){ - - try { - data = DistroIndex.fromJSON(JSON.parse(body)) - } catch (e) { - reject(e) - return - } - - fs.writeFile(distroDest, body, 'utf-8', (err) => { - if(!err){ - resolve(data) - return - } else { - reject(err) - return - } - }) - } else { - reject(error) - return - } - }) - }) -} - -/** - * @returns {Promise.} - */ -exports.pullLocal = function(){ - return new Promise((resolve, reject) => { - fs.readFile(DEV_MODE ? DEV_PATH : DISTRO_PATH, 'utf-8', (err, d) => { - if(!err){ - data = DistroIndex.fromJSON(JSON.parse(d)) - resolve(data) - return - } else { - reject(err) - return - } - }) - }) -} - -exports.setDevMode = function(value){ - if(value){ - logger.info('Developer mode enabled.') - logger.info('If you don\'t know what that means, revert immediately.') - } else { - logger.info('Developer mode disabled.') - } - DEV_MODE = value -} - -exports.isDevMode = function(){ - return DEV_MODE -} - -/** - * @returns {DistroIndex} - */ -exports.getDistribution = function(){ - return data -} \ No newline at end of file +// Old WesterosCraft url. +// exports.REMOTE_DISTRO_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json' +exports.REMOTE_DISTRO_URL = 'https://helios-files.geekcorner.eu.org/distribution.json' + +const api = new DistributionAPI( + ConfigManager.getLauncherDirectory(), + null, // Injected forcefully by the preloader. + null, // Injected forcefully by the preloader. + exports.REMOTE_DISTRO_URL, + false +) + +exports.DistroAPI = api \ No newline at end of file diff --git a/app/assets/js/preloader.js b/app/assets/js/preloader.js index f43eda8..ae01bb8 100644 --- a/app/assets/js/preloader.js +++ b/app/assets/js/preloader.js @@ -4,9 +4,11 @@ const os = require('os') const path = require('path') const ConfigManager = require('./configmanager') -const DistroManager = require('./distromanager') +const { DistroAPI } = require('./distromanager') const LangLoader = require('./langloader') const { LoggerUtil } = require('helios-core') +// eslint-disable-next-line no-unused-vars +const { HeliosDistribution } = require('helios-core/common') const logger = LoggerUtil.getLogger('Preloader') @@ -15,16 +17,25 @@ logger.info('Loading..') // Load ConfigManager ConfigManager.load() +// Yuck! +// TODO Fix this +DistroAPI['commonDir'] = ConfigManager.getCommonDirectory() +DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory() + // Load Strings LangLoader.loadLanguage('en_US') +/** + * + * @param {HeliosDistribution} data + */ function onDistroLoad(data){ if(data != null){ // Resolve the selected server if its value has yet to be set. - if(ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null){ + if(ConfigManager.getSelectedServer() == null || data.getServerById(ConfigManager.getSelectedServer()) == null){ logger.info('Determining default selected server..') - ConfigManager.setSelectedServer(data.getMainServer().getID()) + ConfigManager.setSelectedServer(data.getMainServer().rawServer.id) ConfigManager.save() } } @@ -32,35 +43,20 @@ function onDistroLoad(data){ } // Ensure Distribution is downloaded and cached. -DistroManager.pullRemote().then((data) => { - logger.info('Loaded distribution index.') - - onDistroLoad(data) - -}).catch((err) => { - logger.info('Failed to load distribution index.') - logger.error(err) - - logger.info('Attempting to load an older version of the distribution index.') - // Try getting a local copy, better than nothing. - DistroManager.pullLocal().then((data) => { - logger.info('Successfully loaded an older version of the distribution index.') - - onDistroLoad(data) - - - }).catch((err) => { +DistroAPI.getDistribution() + .then(heliosDistro => { + logger.info('Loaded distribution index.') + onDistroLoad(heliosDistro) + }) + .catch(err => { logger.info('Failed to load an older version of the distribution index.') logger.info('Application cannot run.') logger.error(err) onDistroLoad(null) - }) -}) - // Clean up temp dir incase previous launches ended unexpectedly. fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { if(err){ diff --git a/app/assets/js/processbuilder.js b/app/assets/js/processbuilder.js index 5efc1e6..b86698c 100644 --- a/app/assets/js/processbuilder.js +++ b/app/assets/js/processbuilder.js @@ -3,20 +3,19 @@ const child_process = require('child_process') const crypto = require('crypto') const fs = require('fs-extra') const { LoggerUtil } = require('helios-core') +const { getMojangOS, isLibraryCompatible, mcVersionAtLeast } = require('helios-core/common') +const { Type } = require('helios-distribution-types') const os = require('os') const path = require('path') -const { URL } = require('url') -const { Util, Library } = require('./assetguard') const ConfigManager = require('./configmanager') -const DistroManager = require('./distromanager') const logger = LoggerUtil.getLogger('ProcessBuilder') class ProcessBuilder { constructor(distroServer, versionData, forgeData, authUser, launcherVersion){ - this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.getID()) + this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.rawServer.id) this.commonDir = ConfigManager.getCommonDirectory() this.server = distroServer this.versionData = versionData @@ -41,10 +40,10 @@ class ProcessBuilder { process.throwDeprecation = true this.setupLiteLoader() logger.info('Using liteloader:', this.usingLiteLoader) - const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.getID()).mods, this.server.getModules()) + const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.rawServer.id).mods, this.server.modules) // Mod list below 1.13 - if(!Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){ + if(!mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){ this.constructJSONModList('forge', modObj.fMods, true) if(this.usingLiteLoader){ this.constructJSONModList('liteloader', modObj.lMods, true) @@ -54,14 +53,14 @@ class ProcessBuilder { const uberModArr = modObj.fMods.concat(modObj.lMods) let args = this.constructJVMArguments(uberModArr, tempNativePath) - if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){ + if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){ //args = args.concat(this.constructModArguments(modObj.fMods)) args = args.concat(this.constructModList(modObj.fMods)) } logger.info('Launch Arguments:', args) - const child = child_process.spawn(ConfigManager.getJavaExecutable(this.server.getID()), args, { + const child = child_process.spawn(ConfigManager.getJavaExecutable(this.server.rawServer.id), args, { cwd: this.gameDir, detached: ConfigManager.getLaunchDetached() }) @@ -122,7 +121,7 @@ class ProcessBuilder { * @returns {boolean} True if the mod is enabled, false otherwise. */ static isModEnabled(modCfg, required = null){ - return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.isDefault() : true + return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.def : true } /** @@ -132,20 +131,20 @@ class ProcessBuilder { * mod. It must not be declared as a submodule. */ setupLiteLoader(){ - for(let ll of this.server.getModules()){ - if(ll.getType() === DistroManager.Types.LiteLoader){ - if(!ll.getRequired().isRequired()){ - const modCfg = ConfigManager.getModConfiguration(this.server.getID()).mods - if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessID()], ll.getRequired())){ - if(fs.existsSync(ll.getArtifact().getPath())){ + for(let ll of this.server.modules){ + if(ll.rawModule.type === Type.LiteLoader){ + if(!ll.getRequired().value){ + const modCfg = ConfigManager.getModConfiguration(this.server.rawServer.id).mods + if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessMavenIdentifier()], ll.getRequired())){ + if(fs.existsSync(ll.getPath())){ this.usingLiteLoader = true - this.llPath = ll.getArtifact().getPath() + this.llPath = ll.getPath() } } } else { - if(fs.existsSync(ll.getArtifact().getPath())){ + if(fs.existsSync(ll.getPath())){ this.usingLiteLoader = true - this.llPath = ll.getArtifact().getPath() + this.llPath = ll.getPath() } } } @@ -166,20 +165,20 @@ class ProcessBuilder { let lMods = [] for(let mdl of mdls){ - const type = mdl.getType() - if(type === DistroManager.Types.ForgeMod || type === DistroManager.Types.LiteMod || type === DistroManager.Types.LiteLoader){ - const o = !mdl.getRequired().isRequired() - const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessID()], mdl.getRequired()) + const type = mdl.rawModule.type + if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader){ + const o = !mdl.getRequired().value + const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessMavenIdentifier()], mdl.getRequired()) if(!o || (o && e)){ - if(mdl.hasSubModules()){ - const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessID()].mods, mdl.getSubModules()) + if(mdl.subModules.length > 0){ + const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessMavenIdentifier()].mods, mdl.subModules) fMods = fMods.concat(v.fMods) lMods = lMods.concat(v.lMods) - if(mdl.type === DistroManager.Types.LiteLoader){ + if(type === Type.LiteLoader){ continue } } - if(mdl.type === DistroManager.Types.ForgeMod){ + if(type === Type.ForgeMod){ fMods.push(mdl) } else { lMods.push(mdl) @@ -242,11 +241,11 @@ class ProcessBuilder { const ids = [] if(type === 'forge'){ for(let mod of mods){ - ids.push(mod.getExtensionlessID()) + ids.push(mod.getExtensionlessMavenIdentifier()) } } else { for(let mod of mods){ - ids.push(mod.getExtensionlessID() + '@' + mod.getExtension()) + ids.push(mod.getMavenIdentifier()) } } modList.modRef = ids @@ -266,7 +265,7 @@ class ProcessBuilder { // */ // constructModArguments(mods){ // const argStr = mods.map(mod => { - // return mod.getExtensionlessID() + // return mod.getExtensionlessMavenIdentifier() // }).join(',') // if(argStr){ @@ -289,7 +288,7 @@ class ProcessBuilder { */ constructModList(mods) { const writeBuffer = mods.map(mod => { - return mod.getExtensionlessID() + return mod.getExtensionlessMavenIdentifier() }).join('\n') if(writeBuffer) { @@ -307,14 +306,11 @@ class ProcessBuilder { } _processAutoConnectArg(args){ - if(ConfigManager.getAutoConnect() && this.server.isAutoConnect()){ - const serverURL = new URL('my://' + this.server.getAddress()) + if(ConfigManager.getAutoConnect() && this.server.rawServer.autoconnect){ args.push('--server') - args.push(serverURL.hostname) - if(serverURL.port){ - args.push('--port') - args.push(serverURL.port) - } + args.push(this.server.hostname) + args.push('--port') + args.push(this.server.port) } } @@ -326,7 +322,7 @@ class ProcessBuilder { * @returns {Array.} An array containing the full JVM arguments for this process. */ constructJVMArguments(mods, tempNativePath){ - if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){ + if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){ return this._constructJVMArguments113(mods, tempNativePath) } else { return this._constructJVMArguments112(mods, tempNativePath) @@ -354,9 +350,9 @@ class ProcessBuilder { args.push('-Xdock:name=HeliosLauncher') args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns')) } - args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.getID())) - args.push('-Xms' + ConfigManager.getMinRAM(this.server.getID())) - args = args.concat(ConfigManager.getJVMOptions(this.server.getID())) + args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id)) + args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id)) + args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id)) args.push('-Djava.library.path=' + tempNativePath) // Main Java Class @@ -405,9 +401,9 @@ class ProcessBuilder { args.push('-Xdock:name=HeliosLauncher') args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns')) } - args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.getID())) - args.push('-Xms' + ConfigManager.getMinRAM(this.server.getID())) - args = args.concat(ConfigManager.getJVMOptions(this.server.getID())) + args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id)) + args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id)) + args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id)) // Main Java Class args.push(this.forgeData.mainClass) @@ -421,7 +417,7 @@ class ProcessBuilder { let checksum = 0 for(let rule of args[i].rules){ if(rule.os != null){ - if(rule.os.name === Library.mojangFriendlyOS() + if(rule.os.name === getMojangOS() && (rule.os.version == null || new RegExp(rule.os.version).test(os.release))){ if(rule.action === 'allow'){ checksum++ @@ -471,7 +467,7 @@ class ProcessBuilder { break case 'version_name': //val = versionData.id - val = this.server.getID() + val = this.server.rawServer.id break case 'game_directory': val = this.gameDir @@ -523,7 +519,7 @@ class ProcessBuilder { // Autoconnect let isAutoconnectBroken try { - isAutoconnectBroken = Util.isAutoconnectBroken(this.forgeData.id.split('-')[2]) + isAutoconnectBroken = ProcessBuilder.isAutoconnectBroken(this.forgeData.id.split('-')[2]) } catch(err) { logger.error(err) logger.error('Forge version format changed.. assuming autoconnect works.') @@ -569,7 +565,7 @@ class ProcessBuilder { break case 'version_name': //val = versionData.id - val = this.server.getID() + val = this.server.rawServer.id break case 'game_directory': val = this.gameDir @@ -668,7 +664,7 @@ class ProcessBuilder { classpathArg(mods, tempNativePath){ let cpArgs = [] - if(!Util.mcVersionAtLeast('1.17', this.server.getMinecraftVersion())) { + if(!mcVersionAtLeast('1.17', this.server.rawServer.minecraftVersion)) { // Add the version.jar to the classpath. // Must not be added to the classpath for Forge 1.17+. const version = this.versionData.id @@ -714,13 +710,13 @@ class ProcessBuilder { fs.ensureDirSync(tempNativePath) for(let i=0; i 0){ const res = this._resolveModuleLibraries(mdl) if(res.length > 0){ libs = {...libs, ...res} @@ -863,20 +859,20 @@ class ProcessBuilder { * @returns {Array.} An array containing the paths of each library this module requires. */ _resolveModuleLibraries(mdl){ - if(!mdl.hasSubModules()){ + if(!mdl.subModules.length > 0){ return [] } let libs = [] - for(let sm of mdl.getSubModules()){ - if(sm.getType() === DistroManager.Types.Library){ + for(let sm of mdl.subModules){ + if(sm.rawModule.type === Type.Library){ - if(sm.getClasspath()) { - libs.push(sm.getArtifact().getPath()) + if(sm.rawModule.classpath ?? true) { + libs.push(sm.getPath()) } } // If this module has submodules, we need to resolve the libraries for those. // To avoid unnecessary recursive calls, base case is checked here. - if(mdl.hasSubModules()){ + if(mdl.subModules.length > 0){ const res = this._resolveModuleLibraries(sm) if(res.length > 0){ libs = libs.concat(res) @@ -886,6 +882,24 @@ class ProcessBuilder { return libs } + static isAutoconnectBroken(forgeVersion) { + + const minWorking = [31, 2, 15] + const verSplit = forgeVersion.split('.').map(v => Number(v)) + + if(verSplit[0] === 31) { + for(let i=0; i minWorking[i]) { + return false + } else if(verSplit[i] < minWorking[i]) { + return true + } + } + } + + return false + } + } module.exports = ProcessBuilder \ No newline at end of file diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js index a7a2d0c..94d311a 100644 --- a/app/assets/js/scripts/landing.js +++ b/app/assets/js/scripts/landing.js @@ -5,14 +5,33 @@ const cp = require('child_process') const crypto = require('crypto') const { URL } = require('url') -const { MojangRestAPI, getServerStatus } = require('helios-core/mojang') +const { + MojangRestAPI, + getServerStatus +} = require('helios-core/mojang') +const { + RestResponseStatus, + isDisplayableError, + validateLocalFile +} = require('helios-core/common') +const { + FullRepair, + DistributionIndexProcessor, + MojangIndexProcessor, + downloadFile +} = require('helios-core/dl') +const { + validateSelectedJvm, + ensureJavaDirIsRoot, + javaExecFromRoot, + discoverBestJvmInstallation, + latestOpenJDK, + extractJdk +} = require('helios-core/java') // Internal Requirements const DiscordWrapper = require('./assets/js/discordwrapper') const ProcessBuilder = require('./assets/js/processbuilder') -const { Util } = require('./assets/js/assetguard') -const { RestResponseStatus, isDisplayableError } = require('helios-core/common') -const { stdout } = require('process') // Launch Elements const launch_content = document.getElementById('launch_content') @@ -54,26 +73,22 @@ function setLaunchDetails(details){ /** * Set the value of the loading progress bar and display that value. * - * @param {number} value The progress value. - * @param {number} max The total size. - * @param {number|string} percent Optional. The percentage to display on the progress label. + * @param {number} percent Percentage (0-100) */ -function setLaunchPercentage(value, max, percent = ((value/max)*100)){ - launch_progress.setAttribute('max', max) - launch_progress.setAttribute('value', value) +function setLaunchPercentage(percent){ + launch_progress.setAttribute('max', 100) + launch_progress.setAttribute('value', percent) launch_progress_label.innerHTML = percent + '%' } /** * Set the value of the OS progress bar and display that on the UI. * - * @param {number} value The progress value. - * @param {number} max The total download size. - * @param {number|string} percent Optional. The percentage to display on the progress label. + * @param {number} percent Percentage (0-100) */ -function setDownloadPercentage(value, max, percent = ((value/max)*100)){ - remote.getCurrentWindow().setProgressBar(value/max) - setLaunchPercentage(value, max, percent) +function setDownloadPercentage(percent){ + remote.getCurrentWindow().setProgressBar(percent/100) + setLaunchPercentage(percent) } /** @@ -86,39 +101,43 @@ function setLaunchEnabled(val){ } // Bind launch button -document.getElementById('launch_button').addEventListener('click', function(e){ +document.getElementById('launch_button').addEventListener('click', async e => { loggerLanding.info('Launching game..') - const mcVersion = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion() - const jExe = ConfigManager.getJavaExecutable(ConfigManager.getSelectedServer()) - if(jExe == null){ - asyncSystemScan(mcVersion) - } else { + try { + const server = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) + const jExe = ConfigManager.getJavaExecutable(ConfigManager.getSelectedServer()) + if(jExe == null){ + await asyncSystemScan(server.effectiveJavaOptions) + } else { - setLaunchDetails(Lang.queryJS('landing.launch.pleaseWait')) - toggleLaunchArea(true) - setLaunchPercentage(0, 100) + setLaunchDetails(Lang.queryJS('landing.launch.pleaseWait')) + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + + const details = await validateSelectedJvm(ensureJavaDirIsRoot(jExe), server.effectiveJavaOptions.supported) + if(details != null){ + loggerLanding.info('Jvm Details', details) + await dlAsync() - const jg = new JavaGuard(mcVersion) - jg._validateJavaBinary(jExe).then((v) => { - loggerLanding.info('Java version meta', v) - if(v.valid){ - dlAsync() } else { - asyncSystemScan(mcVersion) + await asyncSystemScan(server.effectiveJavaOptions) } - }) + } + } catch(err) { + loggerLanding.error('Unhandled error in during launch process.', err) + showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.') } }) // Bind settings button -document.getElementById('settingsMediaButton').onclick = (e) => { - prepareSettings() +document.getElementById('settingsMediaButton').onclick = async e => { + await prepareSettings() switchView(getCurrentView(), VIEWS.settings) } // Bind avatar overlay button. -document.getElementById('avatarOverlay').onclick = (e) => { - prepareSettings() +document.getElementById('avatarOverlay').onclick = async e => { + await prepareSettings() switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { settingsNavItemListener(document.getElementById('settingsNavAccount'), false) }) @@ -144,9 +163,9 @@ function updateSelectedServer(serv){ if(getCurrentView() === VIEWS.settings){ fullSettingsSave() } - ConfigManager.setSelectedServer(serv != null ? serv.getID() : null) + ConfigManager.setSelectedServer(serv != null ? serv.rawServer.id : null) ConfigManager.save() - server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.getName() : 'No Server Selected') + server_selection_button.innerHTML = '\u2022 ' + (serv != null ? serv.rawServer.name : 'No Server Selected') if(getCurrentView() === VIEWS.settings){ animateSettingsTabRefresh() } @@ -154,9 +173,9 @@ function updateSelectedServer(serv){ } // Real text is set in uibinder.js on distributionIndexDone. server_selection_button.innerHTML = '\u2022 Loading..' -server_selection_button.onclick = (e) => { +server_selection_button.onclick = async e => { e.target.blur() - toggleServerSelection(true) + await toggleServerSelection(true) } // Update Mojang Status Color @@ -220,17 +239,16 @@ const refreshMojangStatuses = async function(){ document.getElementById('mojang_status_icon').style.color = MojangRestAPI.statusToHex(status) } -const refreshServerStatus = async function(fade = false){ +const refreshServerStatus = async (fade = false) => { loggerLanding.info('Refreshing Server Status') - const serv = DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()) + const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) let pLabel = 'SERVER' let pVal = 'OFFLINE' try { - const serverURL = new URL('my://' + serv.getAddress()) - const servStat = await getServerStatus(47, serverURL.hostname, Number(serverURL.port)) + const servStat = await getServerStatus(47, serv.hostname, serv.port) console.log(servStat) pLabel = 'PLAYERS' pVal = servStat.players.online + '/' + servStat.players.max @@ -279,189 +297,145 @@ function showLaunchFailure(title, desc){ /* System (Java) Scan */ -let sysAEx -let scanAt - -let extractListener - /** * Asynchronously scan the system for valid Java installations. * - * @param {string} mcVersion The Minecraft version we are scanning for. * @param {boolean} launchAfter Whether we should begin to launch after scanning. */ -function asyncSystemScan(mcVersion, launchAfter = true){ +async function asyncSystemScan(effectiveJavaOptions, launchAfter = true){ - setLaunchDetails('Please wait..') + setLaunchDetails('Checking system info..') toggleLaunchArea(true) setLaunchPercentage(0, 100) - const forkEnv = JSON.parse(JSON.stringify(process.env)) - forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() + const jvmDetails = await discoverBestJvmInstallation( + ConfigManager.getDataDirectory(), + effectiveJavaOptions.supported + ) - // Fork a process to run validations. - sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ - 'JavaGuard', - mcVersion - ], { - env: forkEnv, - stdio: 'pipe' - }) - // Stdout - sysAEx.stdio[1].setEncoding('utf8') - sysAEx.stdio[1].on('data', (data) => { - console.log(`\x1b[32m[SysAEx]\x1b[0m ${data}`) - }) - // Stderr - sysAEx.stdio[2].setEncoding('utf8') - sysAEx.stdio[2].on('data', (data) => { - console.log(`\x1b[31m[SysAEx]\x1b[0m ${data}`) - }) - - const javaVer = Util.mcVersionAtLeast('1.17', mcVersion) ? '17' : '8' - - sysAEx.on('message', (m) => { - - if(m.context === 'validateJava'){ - if(m.result == null){ - // If the result is null, no valid Java installation was found. - // Show this information to the user. + if(jvmDetails == null) { + // If the result is null, no valid Java installation was found. + // Show this information to the user. + setOverlayContent( + 'No Compatible
Java Installation Found', + `In order to join WesterosCraft, you need a 64-bit installation of Java ${effectiveJavaOptions.suggestedMajor}. Would you like us to install a copy?`, + 'Install Java', + 'Install Manually' + ) + setOverlayHandler(() => { + setLaunchDetails('Preparing Java Download..') + toggleOverlay(false) + + try { + downloadJava(effectiveJavaOptions, launchAfter) + } catch(err) { + loggerLanding.error('Unhandled error in Java Download', err) + showLaunchFailure('Error During Java Download', 'See console (CTRL + Shift + i) for more details.') + } + }) + setDismissHandler(() => { + $('#overlayContent').fadeOut(250, () => { + //$('#overlayDismiss').toggle(false) setOverlayContent( - 'No Compatible
Java Installation Found', - `In order to join WesterosCraft, you need a 64-bit installation of Java ${javaVer}. Would you like us to install a copy?`, - 'Install Java', - 'Install Manually' + 'Java is Required
to Launch', + `A valid x64 installation of Java ${effectiveJavaOptions.suggestedMajor} is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.`, + 'I Understand', + 'Go Back' ) setOverlayHandler(() => { - setLaunchDetails('Preparing Java Download..') - sysAEx.send({task: 'changeContext', class: 'AssetGuard', args: [ConfigManager.getCommonDirectory(),ConfigManager.getJavaExecutable(ConfigManager.getSelectedServer())]}) - sysAEx.send({task: 'execute', function: '_enqueueOpenJDK', argsArr: [ConfigManager.getDataDirectory(), mcVersion]}) + toggleLaunchArea(false) toggleOverlay(false) }) setDismissHandler(() => { - $('#overlayContent').fadeOut(250, () => { - //$('#overlayDismiss').toggle(false) - setOverlayContent( - 'Java is Required
to Launch', - `A valid x64 installation of Java ${javaVer} is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.`, - 'I Understand', - 'Go Back' - ) - setOverlayHandler(() => { - toggleLaunchArea(false) - toggleOverlay(false) - }) - setDismissHandler(() => { - toggleOverlay(false, true) - asyncSystemScan() - }) - $('#overlayContent').fadeIn(250) - }) + toggleOverlay(false, true) + + asyncSystemScan(effectiveJavaOptions, launchAfter) }) - toggleOverlay(true, true) + $('#overlayContent').fadeIn(250) + }) + }) + toggleOverlay(true, true) + } else { + // Java installation found, use this to launch the game. + const javaExec = javaExecFromRoot(jvmDetails.path) + ConfigManager.setJavaExecutable(ConfigManager.getSelectedServer(), javaExec) + ConfigManager.save() - } else { - // Java installation found, use this to launch the game. - ConfigManager.setJavaExecutable(ConfigManager.getSelectedServer(), m.result) - ConfigManager.save() + // We need to make sure that the updated value is on the settings UI. + // Just incase the settings UI is already open. + settingsJavaExecVal.value = javaExec + await populateJavaExecDetails(settingsJavaExecVal.value) - // We need to make sure that the updated value is on the settings UI. - // Just incase the settings UI is already open. - settingsJavaExecVal.value = m.result - populateJavaExecDetails(settingsJavaExecVal.value) - - if(launchAfter){ - dlAsync() - } - sysAEx.disconnect() - } - } else if(m.context === '_enqueueOpenJDK'){ - - if(m.result === true){ - - // Oracle JRE enqueued successfully, begin download. - setLaunchDetails('Downloading Java..') - sysAEx.send({task: 'execute', function: 'processDlQueues', argsArr: [[{id:'java', limit:1}]]}) - - } else { - - // Oracle JRE enqueue failed. Probably due to a change in their website format. - // User will have to follow the guide to install Java. - setOverlayContent( - 'Unexpected Issue:
Java Download Failed', - 'Unfortunately we\'ve encountered an issue while attempting to install Java. You will need to manually install a copy. Please check out our Troubleshooting Guide for more details and instructions.', - 'I Understand' - ) - setOverlayHandler(() => { - toggleOverlay(false) - toggleLaunchArea(false) - }) - toggleOverlay(true) - sysAEx.disconnect() - - } - - } else if(m.context === 'progress'){ - - switch(m.data){ - case 'download': - // Downloading.. - setDownloadPercentage(m.value, m.total, m.percent) - break - } - - } else if(m.context === 'complete'){ - - switch(m.data){ - case 'download': { - // Show installing progress bar. - remote.getCurrentWindow().setProgressBar(2) - - // Wait for extration to complete. - const eLStr = 'Extracting' - let dotStr = '' - setLaunchDetails(eLStr) - extractListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - setLaunchDetails(eLStr + dotStr) - }, 750) - break - } - case 'java': - // Download & extraction complete, remove the loading from the OS progress bar. - remote.getCurrentWindow().setProgressBar(-1) - - // Extraction completed successfully. - ConfigManager.setJavaExecutable(ConfigManager.getSelectedServer(), m.args[0]) - ConfigManager.save() - - if(extractListener != null){ - clearInterval(extractListener) - extractListener = null - } - - setLaunchDetails('Java Installed!') - - if(launchAfter){ - dlAsync() - } - - sysAEx.disconnect() - break - } - - } else if(m.context === 'error'){ - console.log(m.error) + // TODO Callback hell, refactor + // TODO Move this out, separate concerns. + if(launchAfter){ + await dlAsync() } - }) + } - // Begin system Java scan. - setLaunchDetails('Checking system info..') - sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory()]}) +} + +async function downloadJava(effectiveJavaOptions, launchAfter = true) { + + // TODO Error handling. + // asset can be null. + const asset = await latestOpenJDK( + effectiveJavaOptions.suggestedMajor, + ConfigManager.getDataDirectory(), + effectiveJavaOptions.distribution) + + if(asset == null) { + throw new Error('Failed to find OpenJDK distribution.') + } + + let received = 0 + await downloadFile(asset.url, asset.path, ({ transferred }) => { + received = transferred + setDownloadPercentage(Math.trunc((transferred/asset.size)*100)) + }) + setDownloadPercentage(100) + + if(received != asset.size) { + loggerLanding.warn(`Java Download: Expected ${asset.size} bytes but received ${received}`) + if(!await validateLocalFile(asset.path, asset.algo, asset.hash)) { + log.error(`Hashes do not match, ${asset.id} may be corrupted.`) + // Don't know how this could happen, but report it. + throw new Error('Downloaded JDK has bad hash, file may be corrupted.') + } + } + + // Extract + // Show installing progress bar. + remote.getCurrentWindow().setProgressBar(2) + + // Wait for extration to complete. + const eLStr = 'Extracting Java' + let dotStr = '' + setLaunchDetails(eLStr) + const extractListener = setInterval(() => { + if(dotStr.length >= 3){ + dotStr = '' + } else { + dotStr += '.' + } + setLaunchDetails(eLStr + dotStr) + }, 750) + + const newJavaExec = await extractJdk(asset.path) + + // Extraction complete, remove the loading from the OS progress bar. + remote.getCurrentWindow().setProgressBar(-1) + + // Extraction completed successfully. + ConfigManager.setJavaExecutable(ConfigManager.getSelectedServer(), newJavaExec) + ConfigManager.save() + + clearInterval(extractListener) + setLaunchDetails('Java Installed!') + + // TODO Callback hell + // Refactor the launch functions + asyncSystemScan(effectiveJavaOptions, launchAfter) } @@ -475,18 +449,28 @@ const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/ const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/ const MIN_LINGER = 5000 -let aEx -let serv -let versionData -let forgeData - -let progressListener - -function dlAsync(login = true){ +async function dlAsync(login = true) { // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without // launching the game. + const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite') + + setLaunchDetails('Loading server information..') + + let distro + + try { + distro = await DistroAPI.refreshDistributionOrFallback() + onDistroRefresh(distro) + } catch(err) { + loggerLaunchSuite.error('Unable to refresh distribution index.', err) + showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.') + return + } + + const serv = distro.getServerById(ConfigManager.getSelectedServer()) + if(login) { if(ConfigManager.getSelectedAccount() == null){ loggerLanding.error('You must be logged into an account.') @@ -498,272 +482,162 @@ function dlAsync(login = true){ toggleLaunchArea(true) setLaunchPercentage(0, 100) - const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite') - - const forkEnv = JSON.parse(JSON.stringify(process.env)) - forkEnv.CONFIG_DIRECT_PATH = ConfigManager.getLauncherDirectory() - - // Start AssetExec to run validations and downloads in a forked process. - aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ - 'AssetGuard', + const fullRepairModule = new FullRepair( ConfigManager.getCommonDirectory(), - ConfigManager.getJavaExecutable(ConfigManager.getSelectedServer()) - ], { - env: forkEnv, - stdio: 'pipe' - }) - // Stdout - aEx.stdio[1].setEncoding('utf8') - aEx.stdio[1].on('data', (data) => { - console.log(`\x1b[32m[AEx]\x1b[0m ${data}`) - }) - // Stderr - aEx.stdio[2].setEncoding('utf8') - aEx.stdio[2].on('data', (data) => { - console.log(`\x1b[31m[AEx]\x1b[0m ${data}`) - }) - aEx.on('error', (err) => { + ConfigManager.getInstanceDirectory(), + ConfigManager.getLauncherDirectory(), + ConfigManager.getSelectedServer(), + DistroAPI.isDevMode() + ) + + fullRepairModule.spawnReceiver() + + fullRepairModule.childProcess.on('error', (err) => { loggerLaunchSuite.error('Error during launch', err) showLaunchFailure('Error During Launch', err.message || 'See console (CTRL + Shift + i) for more details.') }) - aEx.on('close', (code, signal) => { + fullRepairModule.childProcess.on('close', (code, _signal) => { if(code !== 0){ - loggerLaunchSuite.error(`AssetExec exited with code ${code}, assuming error.`) + loggerLaunchSuite.error(`Full Repair Module exited with code ${code}, assuming error.`) showLaunchFailure('Error During Launch', 'See console (CTRL + Shift + i) for more details.') } }) - // Establish communications between the AssetExec and current process. - aEx.on('message', (m) => { + loggerLaunchSuite.info('Validating files.') + setLaunchDetails('Validating file integrity..') + let invalidFileCount = 0 + try { + invalidFileCount = await fullRepairModule.verifyFiles(percent => { + setLaunchPercentage(percent) + }) + setLaunchPercentage(100) + } catch (err) { + loggerLaunchSuite.error('Error during file validation.') + showLaunchFailure('Error During File Verification', err.displayable || 'See console (CTRL + Shift + i) for more details.') + return + } + - if(m.context === 'validate'){ - switch(m.data){ - case 'distribution': - setLaunchPercentage(20, 100) - loggerLaunchSuite.info('Validated distibution index.') - setLaunchDetails('Loading version information..') - break - case 'version': - setLaunchPercentage(40, 100) - loggerLaunchSuite.info('Version data loaded.') - setLaunchDetails('Validating asset integrity..') - break - case 'assets': - setLaunchPercentage(60, 100) - loggerLaunchSuite.info('Asset Validation Complete') - setLaunchDetails('Validating library integrity..') - break - case 'libraries': - setLaunchPercentage(80, 100) - loggerLaunchSuite.info('Library validation complete.') - setLaunchDetails('Validating miscellaneous file integrity..') - break - case 'files': - setLaunchPercentage(100, 100) - loggerLaunchSuite.info('File validation complete.') - setLaunchDetails('Downloading files..') - break + if(invalidFileCount > 0) { + loggerLaunchSuite.info('Downloading files.') + setLaunchDetails('Downloading files..') + setLaunchPercentage(0) + try { + await fullRepairModule.download(percent => { + setDownloadPercentage(percent) + }) + setDownloadPercentage(100) + } catch(err) { + loggerLaunchSuite.error('Error during file download.') + showLaunchFailure('Error During File Download', err.displayable || 'See console (CTRL + Shift + i) for more details.') + return + } + } else { + loggerLaunchSuite.info('No invalid files, skipping download.') + } + + // Remove download bar. + remote.getCurrentWindow().setProgressBar(-1) + + fullRepairModule.destroyReceiver() + + setLaunchDetails('Preparing to launch..') + + const mojangIndexProcessor = new MojangIndexProcessor( + ConfigManager.getCommonDirectory(), + serv.rawServer.minecraftVersion) + const distributionIndexProcessor = new DistributionIndexProcessor( + ConfigManager.getCommonDirectory(), + distro, + serv.rawServer.id + ) + + const forgeData = await distributionIndexProcessor.loadForgeVersionJson(serv) + const versionData = await mojangIndexProcessor.getVersionJson() + + if(login) { + const authUser = ConfigManager.getSelectedAccount() + loggerLaunchSuite.info(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) + let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion()) + setLaunchDetails('Launching game..') + + // const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ + const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`) + + const onLoadComplete = () => { + toggleLaunchArea(false) + if(hasRPC){ + DiscordWrapper.updateDetails('Loading game..') + proc.stdout.on('data', gameStateChange) } - } else if(m.context === 'progress'){ - switch(m.data){ - case 'assets': { - const perc = (m.value/m.total)*20 - setLaunchPercentage(40+perc, 100, parseInt(40+perc)) - break - } - case 'download': - setDownloadPercentage(m.value, m.total, m.percent) - break - case 'extract': { - // Show installing progress bar. - remote.getCurrentWindow().setProgressBar(2) + proc.stdout.removeListener('data', tempListener) + proc.stderr.removeListener('data', gameErrorListener) + } + const start = Date.now() - // Download done, extracting. - const eLStr = 'Extracting libraries' - let dotStr = '' - setLaunchDetails(eLStr) - progressListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - setLaunchDetails(eLStr + dotStr) - }, 750) - break + // Attach a temporary listener to the client output. + // Will wait for a certain bit of text meaning that + // the client application has started, and we can hide + // the progress bar stuff. + const tempListener = function(data){ + if(GAME_LAUNCH_REGEX.test(data.trim())){ + const diff = Date.now()-start + if(diff < MIN_LINGER) { + setTimeout(onLoadComplete, MIN_LINGER-diff) + } else { + onLoadComplete() } } - } else if(m.context === 'complete'){ - switch(m.data){ - case 'download': - // Download and extraction complete, remove the loading from the OS progress bar. - remote.getCurrentWindow().setProgressBar(-1) - if(progressListener != null){ - clearInterval(progressListener) - progressListener = null - } + } - setLaunchDetails('Preparing to launch..') - break + // Listener for Discord RPC. + const gameStateChange = function(data){ + data = data.trim() + if(SERVER_JOINED_REGEX.test(data)){ + DiscordWrapper.updateDetails('Exploring the Realm!') + } else if(GAME_JOINED_REGEX.test(data)){ + DiscordWrapper.updateDetails('Sailing to Westeros!') } - } else if(m.context === 'error'){ - switch(m.data){ - case 'download': - loggerLaunchSuite.error('Error while downloading:', m.error) - - if(m.error.code === 'ENOENT'){ - showLaunchFailure( - 'Download Error', - 'Could not connect to the file server. Ensure that you are connected to the internet and try again.' - ) - } else { - showLaunchFailure( - 'Download Error', - 'Check the console (CTRL + Shift + i) for more details. Please try again.' - ) - } + } - remote.getCurrentWindow().setProgressBar(-1) - - // Disconnect from AssetExec - aEx.disconnect() - break + const gameErrorListener = function(data){ + data = data.trim() + if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){ + loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.') + showLaunchFailure('Error During Launch', 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.') } - } else if(m.context === 'validateEverything'){ + } - let allGood = true + try { + // Build Minecraft process. + proc = pb.build() - // If these properties are not defined it's likely an error. - if(m.result.forgeData == null || m.result.versionData == null){ - loggerLaunchSuite.error('Error during validation:', m.result) + // Bind listeners to stdout. + proc.stdout.on('data', tempListener) + proc.stderr.on('data', gameErrorListener) - loggerLaunchSuite.error('Error during launch', m.result.error) - showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') + setLaunchDetails('Done. Enjoy the server!') - allGood = false + // Init Discord Hook + if(distro.rawDistribution.discord != null && serv.rawServerdiscord != null){ + DiscordWrapper.initRPC(distro.rawDistribution.discord, serv.rawServer.discord) + hasRPC = true + proc.on('close', (code, signal) => { + loggerLaunchSuite.info('Shutting down Discord Rich Presence..') + DiscordWrapper.shutdownRPC() + hasRPC = false + proc = null + }) } - forgeData = m.result.forgeData - versionData = m.result.versionData + } catch(err) { - if(login && allGood) { - const authUser = ConfigManager.getSelectedAccount() - loggerLaunchSuite.info(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) - let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion()) - setLaunchDetails('Launching game..') - - // const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ - const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`) - - const onLoadComplete = () => { - toggleLaunchArea(false) - if(hasRPC){ - DiscordWrapper.updateDetails('Loading game..') - } - proc.stdout.on('data', gameStateChange) - proc.stdout.removeListener('data', tempListener) - proc.stderr.removeListener('data', gameErrorListener) - } - const start = Date.now() - - // Attach a temporary listener to the client output. - // Will wait for a certain bit of text meaning that - // the client application has started, and we can hide - // the progress bar stuff. - const tempListener = function(data){ - if(GAME_LAUNCH_REGEX.test(data.trim())){ - const diff = Date.now()-start - if(diff < MIN_LINGER) { - setTimeout(onLoadComplete, MIN_LINGER-diff) - } else { - onLoadComplete() - } - } - } - - // Listener for Discord RPC. - const gameStateChange = function(data){ - data = data.trim() - if(SERVER_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Exploring the Realm!') - } else if(GAME_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails('Sailing to Westeros!') - } - } - - const gameErrorListener = function(data){ - data = data.trim() - if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){ - loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.') - showLaunchFailure('Error During Launch', 'The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We\'ll contact them and try to straighten things out.') - } - } - - try { - // Build Minecraft process. - proc = pb.build() - - // Bind listeners to stdout. - proc.stdout.on('data', tempListener) - proc.stderr.on('data', gameErrorListener) - - setLaunchDetails('Done. Enjoy the server!') - - // Init Discord Hook - const distro = DistroManager.getDistribution() - if(distro.discord != null && serv.discord != null){ - DiscordWrapper.initRPC(distro.discord, serv.discord) - hasRPC = true - proc.on('close', (code, signal) => { - loggerLaunchSuite.info('Shutting down Discord Rich Presence..') - DiscordWrapper.shutdownRPC() - hasRPC = false - proc = null - }) - } - - } catch(err) { - - loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') - - } - } - - // Disconnect from AssetExec - aEx.disconnect() + loggerLaunchSuite.error('Error during launch', err) + showLaunchFailure('Error During Launch', 'Please check the console (CTRL + Shift + i) for more details.') } - }) + } - // Begin Validations - - // Validate Forge files. - setLaunchDetails('Loading server information..') - - refreshDistributionIndex(true, (data) => { - onDistroRefresh(data) - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - }, (err) => { - loggerLaunchSuite.info('Error while fetching a fresh copy of the distribution index.', err) - refreshDistributionIndex(false, (data) => { - onDistroRefresh(data) - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - }, (err) => { - loggerLaunchSuite.error('Unable to refresh distribution index.', err) - if(DistroManager.getDistribution() == null){ - showLaunchFailure('Fatal Error', 'Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details.') - - // Disconnect from AssetExec - aEx.disconnect() - } else { - serv = data.getServer(ConfigManager.getSelectedServer()) - aEx.send({task: 'execute', function: 'validateEverything', argsArr: [ConfigManager.getSelectedServer(), DistroManager.isDevMode()]}) - } - }) - }) } /** @@ -943,7 +817,7 @@ function initNews(){ let news = {} loadNews().then(news => { - newsArr = news.articles || null + newsArr = news?.articles || null if(newsArr == null){ // News Loading Failed @@ -1089,10 +963,17 @@ function displayArticle(articleObject, index){ * Load news information from the RSS feed specified in the * distribution index. */ -function loadNews(){ - return new Promise((resolve, reject) => { - const distroData = DistroManager.getDistribution() - const newsFeed = distroData.getRSS() +async function loadNews(){ + + const distroData = await DistroAPI.getDistribution() + if(!distroData.rawDistribution.rss) { + loggerLanding.debug('No RSS feed provided.') + return null + } + + const promise = new Promise((resolve, reject) => { + + const newsFeed = distroData.rawDistribution.rss const newsHost = new URL(newsFeed).origin + '/' $.ajax({ url: newsFeed, @@ -1147,4 +1028,6 @@ function loadNews(){ }) }) }) + + return await promise } diff --git a/app/assets/js/scripts/login.js b/app/assets/js/scripts/login.js index 2b6d0c0..96ec923 100644 --- a/app/assets/js/scripts/login.js +++ b/app/assets/js/scripts/login.js @@ -193,10 +193,10 @@ loginButton.addEventListener('click', () => { $('.circle-loader').toggleClass('load-complete') $('.checkmark').toggle() setTimeout(() => { - switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => { + switchView(VIEWS.login, loginViewOnSuccess, 500, 500, async () => { // Temporary workaround if(loginViewOnSuccess === VIEWS.settings){ - prepareSettings() + await prepareSettings() } loginViewOnSuccess = VIEWS.landing // Reset this for good measure. loginCancelEnabled(false) // Reset this for good measure. diff --git a/app/assets/js/scripts/overlay.js b/app/assets/js/scripts/overlay.js index cf2c5c9..f85a28b 100644 --- a/app/assets/js/scripts/overlay.js +++ b/app/assets/js/scripts/overlay.js @@ -117,8 +117,8 @@ function toggleOverlay(toggleState, dismissable = false, content = 'overlayConte } } -function toggleServerSelection(toggleState){ - prepareServerSelectionList() +async function toggleServerSelection(toggleState){ + await prepareServerSelectionList() toggleOverlay(toggleState, true, 'serverSelectContent') } @@ -171,11 +171,11 @@ function setDismissHandler(handler){ /* Server Select View */ -document.getElementById('serverSelectConfirm').addEventListener('click', () => { +document.getElementById('serverSelectConfirm').addEventListener('click', async () => { const listings = document.getElementsByClassName('serverListing') for(let i=0; i { } // None are selected? Not possible right? Meh, handle it. if(listings.length > 0){ - const serv = DistroManager.getDistribution().getServer(listings[i].getAttribute('servid')) + const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid')) updateSelectedServer(serv) toggleOverlay(false) } }) -document.getElementById('accountSelectConfirm').addEventListener('click', () => { +document.getElementById('accountSelectConfirm').addEventListener('click', async () => { const listings = document.getElementsByClassName('accountListing') for(let i=0; i ConfigManager.save() updateSelectedAccount(authAcc) if(getCurrentView() === VIEWS.settings) { - prepareSettings() + await prepareSettings() } toggleOverlay(false) validateSelectedAccount() @@ -211,7 +211,7 @@ document.getElementById('accountSelectConfirm').addEventListener('click', () => ConfigManager.save() updateSelectedAccount(authAcc) if(getCurrentView() === VIEWS.settings) { - prepareSettings() + await prepareSettings() } toggleOverlay(false) validateSelectedAccount() @@ -267,21 +267,21 @@ function setAccountListingHandlers(){ }) } -function populateServerListings(){ - const distro = DistroManager.getDistribution() +async function populateServerListings(){ + const distro = await DistroAPI.getDistribution() const giaSel = ConfigManager.getSelectedServer() - const servers = distro.getServers() + const servers = distro.servers let htmlString = '' for(const serv of servers){ - htmlString += `