From 0a79634b8abd9a6480dbcf58d99ece57a42ed43d Mon Sep 17 00:00:00 2001 From: Daniel Scalzi Date: Sat, 7 Apr 2018 18:06:49 -0400 Subject: [PATCH] Implemented Java validations within the UI. When a user attemps to launch, the configured Java executable will be validated. If it is invalid, we will look for a valid installation. If no valid installation is found, the user will be prompted with an option to install Java. An option to decline needs to be added. If they choose to install, it will download, extract, and update the executable in the config. The game will then be launched. Also added progress tracking for asset validations, as they can potentially take a bit longer. Showing progress assures the user that the program isn't stuck or broken. --- README.md | 4 +- app/assets/css/launcher.css | 27 +++++++- app/assets/js/actionbinder.js | 113 +++++++++++++++++++++++++++++---- app/assets/js/assetexec.js | 8 +++ app/assets/js/assetguard.js | 88 +++++++++++++++++-------- app/assets/js/configmanager.js | 2 +- package-lock.json | 40 +++++------- package.json | 2 +- 8 files changed, 214 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 9e8eab4..161b2ae 100644 --- a/README.md +++ b/README.md @@ -104,4 +104,6 @@ Run either of the build scrips noted below. Note that each platform can only be If you run into any issue which cannot be resolved via a quick google search, create an issue using the tab above. -Much of the discussion regarding this launcher is done on Discord, feel free to join us there [![Discord](https://discordapp.com/api/guilds/98469309352775680/widget.png)](https://discord.gg/hqdjs3m) \ No newline at end of file +Much of the discussion regarding this launcher is done on #launcherdev in Discord, feel free to join us there. + +[![Discord](https://discordapp.com/api/guilds/98469309352775680/embed.png?style=banner2)](https://discord.gg/hqdjs3m) \ No newline at end of file diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css index 4f0a97a..246c373 100644 --- a/app/assets/css/launcher.css +++ b/app/assets/css/launcher.css @@ -1210,15 +1210,38 @@ p { display: flex; flex-direction: column; align-items: center; - justify-content: space-between; + /*justify-content: space-between;*/ width: 300px; - height: 35%; + /*height: 35%;*/ box-sizing: border-box; padding: 15px 0px; /* background-color: #424242; */ text-align: center; } +#overlayContent a { + color: rgba(202, 202, 202, 0.75); + transition: 0.25s ease; +} +#overlayContent a:hover { + color: rgba(255, 255, 255, 0.75); +} +#overlayContent a:active { + color: rgba(165, 165, 165, 0.75); +} + +#overlayContent > *:first-child { + margin-top: 0px !important; +} + +#overlayContent > *:last-child { + margin-bottom: 0px !important; +} + +#overlayContent > * { + margin: 8px 0px; +} + #overlayTitle { font-family: 'Avenir Medium'; font-size: 20px; diff --git a/app/assets/js/actionbinder.js b/app/assets/js/actionbinder.js index 5872813..22dffe2 100644 --- a/app/assets/js/actionbinder.js +++ b/app/assets/js/actionbinder.js @@ -39,6 +39,11 @@ document.addEventListener('readystatechange', function(){ if(jExe == null){ asyncSystemScan() } else { + + setLaunchDetails('Please wait..') + toggleLaunchArea(true) + setLaunchPercentage(0, 100) + AssetGuard._validateJavaBinary(jExe).then((v) => { if(v){ dlAsync() @@ -194,12 +199,13 @@ function setDownloadPercentage(value, max, percent = ((value/max)*100)){ let sysAEx let scanAt -function asyncSystemScan(){ +function asyncSystemScan(launchAfter = true){ setLaunchDetails('Please wait..') toggleLaunchArea(true) setLaunchPercentage(0, 100) + // Fork a process to run validations. sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [ ConfigManager.getGameDirectory(), ConfigManager.getJavaExecutable() @@ -207,21 +213,96 @@ function asyncSystemScan(){ sysAEx.on('message', (m) => { if(m.content === 'validateJava'){ - jPath = m.result - console.log(m.result) - sysAEx.disconnect() + + //m.result = null + + if(m.result == 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 8. Would you like us to install a copy? By installing, you accept Oracle\'s license agreement.', + 'Install Java' + ) + setOverlayHandler(() => { + setLaunchDetails('Preparing Java Download..') + sysAEx.send({task: 0, content: '_enqueueOracleJRE', argsArr: [ConfigManager.getLauncherDirectory()]}) + toggleOverlay(false) + }) + toggleOverlay(true) + + // TODO Add option to not install Java x64. + + } else { + // Java installation found, use this to launch the game. + ConfigManager.setJavaExecutable(m.result) + ConfigManager.save() + if(launchAfter){ + dlAsync() + } + sysAEx.disconnect() + } + + } else if(m.content === '_enqueueOracleJRE'){ + + if(m.result === true){ + + // Oracle JRE enqueued successfully, begin download. + setLaunchDetails('Downloading Java..') + sysAEx.send({task: 0, content: '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( + 'Yikes!
Java download failed.', + 'Unfortunately we\'ve encountered an issue while attempting to install Java. You will need to install a copy yourself. Please check out this guide for more details and instructions.', + 'Got it' + ) + setOverlayHandler(null) + toggleOverlay(true) + sysAEx.disconnect() + + } + + } else if(m.content === 'dl'){ + + if(m.task === 0){ + // Downloading.. + setDownloadPercentage(m.value, m.total, m.percent) + } else if(m.task === 1){ + // Download will be at 100%, remove the loading from the OS progress bar. + remote.getCurrentWindow().setProgressBar(-1) + + // Wait for extration to complete. + setLaunchDetails('Extracting..') + + } else if(m.task === 2){ + + // Extraction completed successfully. + ConfigManager.setJavaExecutable(m.jPath) + ConfigManager.save() + + setLaunchDetails('Java Installed!') + + if(launchAfter){ + dlAsync() + } + + sysAEx.disconnect() + } else { + console.error('Unknown download data type.', m) + } } }) + // Begin system Java scan. setLaunchDetails('Checking system info..') sysAEx.send({task: 0, content: 'validateJava', argsArr: [ConfigManager.getLauncherDirectory()]}) } -function overlayError(){ - -} - // Keep reference to Minecraft Process let proc // Is DiscordRPC enabled @@ -283,12 +364,18 @@ function dlAsync(login = true){ } else if(m.content === 'validateAssets'){ - setLaunchPercentage(60, 100) - console.log('Asset Validation Complete') + // Asset validation can *potentially* take longer, so let's track progress. + if(m.task === 0){ + const perc = (m.value/m.total)*20 + setLaunchPercentage(40+perc, 100, parseInt(40+perc)) + } else { + setLaunchPercentage(60, 100) + console.log('Asset Validation Complete') - // Begin library validation. - setLaunchDetails('Validating library integrity..') - aEx.send({task: 0, content: 'validateLibraries', argsArr: [versionData]}) + // Begin library validation. + setLaunchDetails('Validating library integrity..') + aEx.send({task: 0, content: 'validateLibraries', argsArr: [versionData]}) + } } else if(m.content === 'validateLibraries'){ diff --git a/app/assets/js/assetexec.js b/app/assets/js/assetexec.js index 5d41fed..7a75d90 100644 --- a/app/assets/js/assetexec.js +++ b/app/assets/js/assetexec.js @@ -6,6 +6,10 @@ console.log('AssetExec Started') // Temporary for debug purposes. process.on('unhandledRejection', r => console.log(r)) +tracker.on('assetVal', (data) => { + process.send({task: 0, total: data.total, value: data.acc, content: 'validateAssets'}) +}) + tracker.on('totaldlprogress', (data) => { process.send({task: 0, total: data.total, value: data.acc, percent: parseInt((data.acc/data.total)*100), content: 'dl'}) }) @@ -14,6 +18,10 @@ tracker.on('dlcomplete', () => { process.send({task: 1, content: 'dl'}) }) +tracker.on('jExtracted', (jPath) => { + process.send({task: 2, content: 'dl', jPath}) +}) + process.on('message', (msg) => { if(msg.task === 0){ const func = msg.content diff --git a/app/assets/js/assetguard.js b/app/assets/js/assetguard.js index 5a713eb..f8c5063 100644 --- a/app/assets/js/assetguard.js +++ b/app/assets/js/assetguard.js @@ -32,7 +32,8 @@ const mkpath = require('mkdirp'); const path = require('path') const Registry = require('winreg') const request = require('request') -const targz = require('targz') +const tar = require('tar-fs') +const zlib = require('zlib') // Constants const PLATFORM_MAP = { @@ -558,6 +559,23 @@ class AssetGuard extends EventEmitter { }) } + /** + * 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. + */ + 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 + } + /** * Load Mojang's launcher.json file. * @@ -583,20 +601,16 @@ class AssetGuard extends EventEmitter { * the function's code throws errors. That would indicate that the option is changed or * removed. * - * @param {string} binaryPath Path to the root of the java binary we wish to validate. + * @param {string} binaryExecPath Path to the java executable we wish to validate. * * @returns {Promise.} Resolves to false only if the test is successful and the result * is less than 64. */ - static _validateJavaBinary(binaryPath){ + static _validateJavaBinary(binaryExecPath){ return new Promise((resolve, reject) => { - let fBp = binaryPath - if(!fBp.endsWith('.exe')){ - fBp = path.join(binaryPath, 'bin', 'java.exe') - } - if(fs.existsSync(fBp)){ - child_process.exec('"' + fBp + '" -XshowSettings:properties', (err, stdout, stderr) => { + if(fs.existsSync(binaryExecPath)){ + child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => { try { // Output is stored in stderr? @@ -605,7 +619,7 @@ class AssetGuard extends EventEmitter { for(let i=0; i -1){ let arch = props[i].split('=')[1].trim() - console.log(props[i].trim() + ' for ' + binaryPath) + console.log(props[i].trim() + ' for ' + binaryExecPath) resolve(parseInt(arch) >= 64) } } @@ -770,7 +784,7 @@ class AssetGuard extends EventEmitter { * If versions are equal, JRE > JDK. * * @param {string} dataDir The base launcher directory. - * @returns {Promise.} A Promise which resolves to the root path of a valid + * @returns {Promise.} A Promise which resolves to the executable path of a valid * x64 Java installation. If none are found, null is returned. */ static async _win32JavaValidate(dataDir){ @@ -813,9 +827,10 @@ class AssetGuard extends EventEmitter { // Validate that the binary is actually x64. for(let i=0; i { AssetGuard._latestJREOracle().then(verData => { if(verData != null){ @@ -1269,24 +1288,37 @@ class AssetGuard extends EventEmitter { if(err){ resolve(false) } else { + dataDir = path.join(dataDir, 'runtime', 'x64') const name = combined.substring(combined.lastIndexOf('/')+1) - const fDir = path.join(dir, name) + const fDir = path.join(dataDir, name) const jre = new Asset(name, null, resp.headers['content-length'], opts, fDir) - this.java = new DLTracker([jre], jre.size, a => { - targz.decompress({ - src: a.to, - dest: dir - }, err => { - if(err){ - console.log(err) - } else { + this.java = new DLTracker([jre], jre.size, (a, self) => { + let h = null + fs.createReadStream(a.to) + .on('error', err => console.log(err)) + .pipe(zlib.createGunzip()) + .on('error', err => console.log(err)) + .pipe(tar.extract(dataDir, { + map: (header) => { + if(h == null){ + h = header.name + } + } + })) + .on('error', err => console.log(err)) + .on('finish', () => { fs.unlink(a.to, err => { if(err){ console.log(err) } + if(h.indexOf('/') > -1){ + h = h.substring(0, h.indexOf('/')) + } + const pos = path.join(dataDir, h) + self.emit('jExtracted', AssetGuard.javaExecFromRoot(pos)) }) - } - }) + }) + }) resolve(true) } @@ -1371,7 +1403,7 @@ class AssetGuard extends EventEmitter { writeStream.on('close', () => { //console.log('DLResults ' + asset.size + ' ' + count + ' ', asset.size === count) if(concurrentDlTracker.callback != null){ - concurrentDlTracker.callback.apply(concurrentDlTracker, [asset]) + concurrentDlTracker.callback.apply(concurrentDlTracker, [asset, self]) } cb() }) diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js index 8899d13..4ce6dc8 100644 --- a/app/assets/js/configmanager.js +++ b/app/assets/js/configmanager.js @@ -25,7 +25,7 @@ const DEFAULT_CONFIG = { java: { minRAM: '2G', maxRAM: resolveMaxRAM(), // Dynamic - executable: 'C:\\Program Files\\Java\\jdk1.8.0_152\\bin\\javaw.exe', // TODO Resolve + executable: null, jvmOptions: [ '-XX:+UseConcMarkSweepGC', '-XX:+CMSIncrementalMode', diff --git a/package-lock.json b/package-lock.json index 407b995..54267ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -246,7 +246,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { - "readable-stream": "2.3.5", + "readable-stream": "2.3.6", "safe-buffer": "5.1.1" }, "dependencies": { @@ -256,23 +256,23 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", "isarray": "1.0.0", "process-nextick-args": "2.0.0", "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", + "string_decoder": "1.1.1", "util-deprecate": "1.0.2" } }, "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "5.1.1" } @@ -2750,7 +2750,7 @@ "requires": { "bl": "1.2.2", "end-of-stream": "1.4.1", - "readable-stream": "2.3.5", + "readable-stream": "2.3.6", "xtend": "4.0.1" }, "dependencies": { @@ -2760,23 +2760,23 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", "isarray": "1.0.0", "process-nextick-args": "2.0.0", "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", + "string_decoder": "1.1.1", "util-deprecate": "1.0.2" } }, "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "5.1.1" } @@ -2788,14 +2788,6 @@ } } }, - "targz": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/targz/-/targz-1.0.1.tgz", - "integrity": "sha1-j3alI2lM3t+7XWCkB2/27uzFOY8=", - "requires": { - "tar-fs": "1.16.0" - } - }, "temp-file": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.1.1.tgz", diff --git a/package.json b/package.json index 646ea0c..0318369 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "ejs-electron": "^2.0.1", "jquery": "^3.3.1", "request-promise-native": "^1.0.5", - "targz": "^1.0.1", + "tar-fs": "^1.16.0", "uuid": "^3.2.1", "winreg": "^1.2.4" },